001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2016, 2018  Michael N. Lipp
004 *
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jgrapes.osgi.webconlet.bundles;
020
021import freemarker.core.ParseException;
022import freemarker.template.MalformedTemplateNameException;
023import freemarker.template.Template;
024import freemarker.template.TemplateNotFoundException;
025import java.io.IOException;
026import java.time.Instant;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.Dictionary;
030import java.util.Enumeration;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.List;
034import java.util.Locale;
035import java.util.Map;
036import java.util.Optional;
037import java.util.Set;
038import java.util.TreeMap;
039import java.util.logging.Level;
040import java.util.logging.Logger;
041import java.util.stream.Collectors;
042import org.jgrapes.core.Channel;
043import org.jgrapes.core.Event;
044import org.jgrapes.core.Manager;
045import org.jgrapes.core.annotation.Handler;
046import org.jgrapes.http.Session;
047import org.jgrapes.webconsole.base.AbstractConlet;
048import org.jgrapes.webconsole.base.Conlet.RenderMode;
049import org.jgrapes.webconsole.base.ConsoleSession;
050import org.jgrapes.webconsole.base.WebConsoleUtils;
051import org.jgrapes.webconsole.base.events.AddConletRequest;
052import org.jgrapes.webconsole.base.events.AddConletType;
053import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource;
054import org.jgrapes.webconsole.base.events.ConsoleReady;
055import org.jgrapes.webconsole.base.events.NotifyConletModel;
056import org.jgrapes.webconsole.base.events.NotifyConletView;
057import org.jgrapes.webconsole.base.events.RenderConletRequest;
058import org.jgrapes.webconsole.base.events.RenderConletRequestBase;
059import org.jgrapes.webconsole.base.events.SetLocale;
060import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
061import org.osgi.framework.Bundle;
062import org.osgi.framework.BundleContext;
063import org.osgi.framework.BundleEvent;
064import org.osgi.framework.BundleException;
065import org.osgi.framework.BundleListener;
066import org.osgi.framework.startlevel.BundleStartLevel;
067import org.osgi.framework.wiring.BundleRevision;
068
069/**
070 * 
071 */
072public class BundleListConlet
073        extends FreeMarkerConlet<BundleListConlet.BundleListModel>
074        implements BundleListener {
075
076    private static final Logger LOG
077        = Logger.getLogger(BundleListConlet.class.getName());
078
079    private static final Set<RenderMode> MODES = RenderMode.asSet(
080        RenderMode.Preview, RenderMode.View);
081    private final BundleContext context;
082
083    /**
084     * Creates a new component with its channel set to the given channel.
085     * 
086     * @param componentChannel the channel that the component's handlers listen
087     *            on by default and that {@link Manager#fire(Event, Channel...)}
088     *            sends the event to
089     */
090    @SuppressWarnings("PMD.UnusedFormalParameter")
091    public BundleListConlet(Channel componentChannel, BundleContext context,
092            Map<Object, Object> properties) {
093        super(componentChannel);
094        this.context = context;
095        context.addBundleListener(this);
096    }
097
098    /**
099     * On {@link ConsoleReady}, fire the {@link AddConletType}.
100     *
101     * @param event the event
102     * @param channel the channel
103     * @throws TemplateNotFoundException the template not found exception
104     * @throws MalformedTemplateNameException the malformed template name
105     *             exception
106     * @throws ParseException the parse exception
107     * @throws IOException Signals that an I/O exception has occurred.
108     */
109    @Handler
110    public void onConsoleReady(ConsoleReady event, ConsoleSession channel)
111            throws TemplateNotFoundException, MalformedTemplateNameException,
112            ParseException, IOException {
113        // Add conlet resources to page
114        channel.respond(new AddConletType(type())
115            .setDisplayNames(
116                localizations(channel.supportedLocales(), "conletName"))
117            .addScript(new ScriptResource()
118                .setScriptUri(event.renderSupport().conletResource(
119                    type(), "Bundles-functions.ftl.js"))
120                .setScriptType("module"))
121            .addCss(event.renderSupport(),
122                WebConsoleUtils.uriFromPath("Bundles-style.css")));
123    }
124
125    @Override
126    protected String generateConletId() {
127        return type() + "-" + super.generateConletId();
128    }
129
130    @Override
131    protected Optional<BundleListModel> stateFromSession(
132            Session session, String conletId) {
133        if (conletId.startsWith(type() + "-")) {
134            return Optional.of(new BundleListModel(conletId));
135        }
136        return Optional.empty();
137    }
138
139    @Override
140    @SuppressWarnings("PMD.AvoidDuplicateLiterals")
141    protected ConletTrackingInfo doAddConlet(AddConletRequest event,
142            ConsoleSession channel)
143            throws Exception {
144        BundleListModel conletModel = new BundleListModel(generateConletId());
145        Template tpl
146            = freemarkerConfig().getTemplate("Bundles-preview.ftl.html");
147        channel.respond(new RenderConletFromTemplate(event,
148            type(), conletModel.getConletId(), tpl,
149            fmModel(event, channel, conletModel))
150                .setRenderAs(RenderMode.Preview)
151                .setSupportedModes(MODES));
152        List<Map<String, Object>> bundleInfos
153            = Arrays.stream(context.getBundles())
154                .map(bndl -> createBundleInfo(bndl, channel.locale()))
155                .collect(Collectors.toList());
156        channel.respond(new NotifyConletView(type(),
157            conletModel.getConletId(), "bundleUpdates", bundleInfos,
158            "preview", true));
159        return new ConletTrackingInfo(conletModel.getConletId())
160            .addModes(RenderMode.asSet(RenderMode.Preview));
161    }
162
163    @Override
164    protected Set<RenderMode> doRenderConlet(RenderConletRequest event,
165            ConsoleSession channel, String conletId,
166            BundleListModel conletModel)
167            throws Exception {
168        return renderConlet(event, channel, conletModel);
169    }
170
171    private Set<RenderMode> renderConlet(RenderConletRequestBase<?> event,
172            ConsoleSession channel,
173            BundleListModel conletModel) throws TemplateNotFoundException,
174            MalformedTemplateNameException, ParseException, IOException {
175        Set<RenderMode> renderedAs = new HashSet<>();
176        if (event.renderAs().contains(RenderMode.Preview)) {
177            Template tpl
178                = freemarkerConfig().getTemplate("Bundles-preview.ftl.html");
179            channel.respond(new RenderConletFromTemplate(event, type(),
180                conletModel.getConletId(),
181                tpl, fmModel(event, channel, conletModel))
182                    .setRenderAs(
183                        RenderMode.Preview.addModifiers(event.renderAs()))
184                    .setSupportedModes(MODES));
185            List<Map<String, Object>> bundleInfos
186                = Arrays.stream(context.getBundles())
187                    .map(bndl -> createBundleInfo(bndl, channel.locale()))
188                    .collect(Collectors.toList());
189            channel.respond(new NotifyConletView(type(),
190                conletModel.getConletId(), "bundleUpdates", bundleInfos,
191                "preview", true));
192            renderedAs.add(RenderMode.Preview);
193        }
194        if (event.renderAs().contains(RenderMode.View)) {
195            Template tpl
196                = freemarkerConfig().getTemplate("Bundles-view.ftl.html");
197            channel.respond(new RenderConletFromTemplate(event,
198                type(), conletModel.getConletId(), tpl,
199                fmModel(event, channel, conletModel))
200                    .setRenderAs(
201                        RenderMode.View.addModifiers(event.renderAs())));
202            List<Map<String, Object>> bundleInfos
203                = Arrays.stream(context.getBundles())
204                    .map(bndl -> createBundleInfo(bndl, channel.locale()))
205                    .collect(Collectors.toList());
206            channel.respond(new NotifyConletView(type(),
207                conletModel.getConletId(), "bundleUpdates", bundleInfos,
208                "view", true));
209            renderedAs.add(RenderMode.View);
210        }
211        return renderedAs;
212    }
213
214    private Map<String, Object> createBundleInfo(Bundle bundle, Locale locale) {
215        @SuppressWarnings("PMD.UseConcurrentHashMap")
216        Map<String, Object> result = new HashMap<>();
217        result.put("id", bundle.getBundleId());
218        result.put("name",
219            Optional.ofNullable(bundle.getHeaders(locale.toString())
220                .get("Bundle-Name")).orElse(bundle.getSymbolicName()));
221        result.put("symbolicName", bundle.getSymbolicName());
222        result.put("version", bundle.getVersion().toString());
223        result.put("category",
224            Optional.ofNullable(bundle.getHeaders(locale.toString())
225                .get("Bundle-Category")).orElse(""));
226        result.put("state", "bundleState_" + bundle.getState());
227        result.put("startable", false);
228        result.put("stoppable", false);
229        if ((bundle.getState()
230            & (Bundle.RESOLVED | Bundle.INSTALLED | Bundle.ACTIVE)) != 0) {
231            boolean isFragment = (bundle.adapt(BundleRevision.class).getTypes()
232                & BundleRevision.TYPE_FRAGMENT) != 0;
233            result.put("startable", !isFragment
234                && (bundle.getState() == Bundle.INSTALLED
235                    || bundle.getState() == Bundle.RESOLVED));
236            result.put("stoppable",
237                !isFragment && bundle.getState() == Bundle.ACTIVE);
238        }
239        result.put("uninstallable", (bundle.getState()
240            & (Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE)) != 0);
241        result.put("uninstalled", bundle.getState() == Bundle.UNINSTALLED);
242        return result;
243    }
244
245    @Override
246    protected void doNotifyConletModel(NotifyConletModel event,
247            ConsoleSession channel, BundleListModel conletState)
248            throws Exception {
249        event.stop();
250        Bundle bundle = context.getBundle(event.params().asInt(0));
251        if (bundle == null) {
252            return;
253        }
254        try {
255            switch (event.method()) {
256            case "stop":
257                bundle.stop();
258                break;
259            case "start":
260                bundle.start();
261                break;
262            case "refresh":
263                break;
264            case "update":
265                bundle.update();
266                break;
267            case "uninstall":
268                bundle.uninstall();
269                break;
270            case "sendDetails":
271                sendBundleDetails(event.conletId(), channel, bundle);
272                break;
273            default:// ignore
274                break;
275            }
276        } catch (BundleException e) {
277            // ignore
278            LOG.log(Level.WARNING, "Cannot update bundle state", e);
279        }
280    }
281
282    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
283    private void sendBundleDetails(String conletId, ConsoleSession channel,
284            Bundle bundle) {
285        Locale locale = channel.locale();
286        List<Object> data = new ArrayList<>();
287        data.add(
288            new Object[] { "bundleSymbolicName", bundle.getSymbolicName() });
289        data.add(
290            new Object[] { "bundleVersion", bundle.getVersion().toString() });
291        data.add(new Object[] { "bundleLocation",
292            bundle.getLocation().replace(".", ".&#x200b;") });
293        data.add(new Object[] { "bundleLastModification",
294            Instant.ofEpochMilli(bundle.getLastModified()).toString(),
295            "dateTime" });
296        data.add(new Object[] { "bundleStartLevel",
297            bundle.adapt(BundleStartLevel.class).getStartLevel() });
298        Dictionary<String, String> dict = bundle.getHeaders(locale.toString());
299        @SuppressWarnings("PMD.UseConcurrentHashMap")
300        Map<String, String> headers = new TreeMap<>();
301        for (Enumeration<String> e = dict.keys(); e.hasMoreElements();) {
302            String key = e.nextElement();
303            headers.put(key, dict.get(key));
304        }
305        List<Object> headerList = new ArrayList<>();
306        for (Map.Entry<String, String> e : headers.entrySet()) {
307            headerList.add(new Object[] { e.getKey(),
308                e.getKey().contains("Package")
309                    ? e.getValue().replace(".", ".&#x200b;")
310                    : e.getValue() });
311        }
312        data.add(new Object[] { "manifestHeaders", headerList, "table" });
313        channel.respond(new NotifyConletView(type(),
314            conletId, "bundleDetails", bundle.getBundleId(), data));
315    }
316
317    /**
318     * Translates the OSGi {@link BundleEvent} to a JGrapes event and fires it
319     * on all known console session channels.
320     *
321     * @param event the event
322     */
323    @Override
324    public void bundleChanged(BundleEvent event) {
325        fire(new BundleChanged(event), trackedSessions());
326    }
327
328    /**
329     * Handles a {@link BundleChanged} event by updating the information in the
330     * console sessions.
331     *
332     * @param event the event
333     */
334    @Handler
335    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
336    public void onBundleChanged(BundleChanged event,
337            ConsoleSession consoleSession) {
338        for (String conletId : conletIds(consoleSession)) {
339            consoleSession.respond(new NotifyConletView(type(), conletId,
340                "bundleUpdates",
341                (Object) new Object[] { createBundleInfo(
342                    event.bundleEvent().getBundle(), consoleSession.locale()) },
343                "*", false));
344        }
345    }
346
347    @Override
348    protected boolean doSetLocale(SetLocale event, ConsoleSession channel,
349            String conletId) throws Exception {
350        return true;
351    }
352
353    /**
354     * Wraps an OSGi {@link BundleEvent}.
355     */
356    public static class BundleChanged extends Event<Void> {
357        private final BundleEvent bundleEvent;
358
359        /**
360         * Instantiates a new event.
361         *
362         * @param bundleEvent the OSGi bundle event
363         */
364        public BundleChanged(BundleEvent bundleEvent) {
365            this.bundleEvent = bundleEvent;
366        }
367
368        /**
369         * Return the OSGi bundle event.
370         *
371         * @return the bundle event
372         */
373        public BundleEvent bundleEvent() {
374            return bundleEvent;
375        }
376
377    }
378
379    /**
380     * The bundle's model.
381     */
382    @SuppressWarnings("serial")
383    public class BundleListModel extends AbstractConlet.ConletBaseModel {
384
385        /**
386         * Instantiates a new bundle list model.
387         *
388         * @param conletId the web console component id
389         */
390        public BundleListModel(String conletId) {
391            super(conletId);
392        }
393
394        /**
395         * Return the bundles.
396         *
397         * @return the bundle[]
398         */
399        public Bundle[] bundles() {
400            return context.getBundles();
401        }
402
403    }
404}