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.webconsole.base.Conlet.RenderMode;
047import org.jgrapes.webconsole.base.ConletBaseModel;
048import org.jgrapes.webconsole.base.ConsoleConnection;
049import org.jgrapes.webconsole.base.WebConsoleUtils;
050import org.jgrapes.webconsole.base.events.AddConletRequest;
051import org.jgrapes.webconsole.base.events.AddConletType;
052import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource;
053import org.jgrapes.webconsole.base.events.ConsoleReady;
054import org.jgrapes.webconsole.base.events.NotifyConletModel;
055import org.jgrapes.webconsole.base.events.NotifyConletView;
056import org.jgrapes.webconsole.base.events.RenderConlet;
057import org.jgrapes.webconsole.base.events.RenderConletRequestBase;
058import org.jgrapes.webconsole.base.events.SetLocale;
059import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
060import org.osgi.framework.Bundle;
061import org.osgi.framework.BundleContext;
062import org.osgi.framework.BundleEvent;
063import org.osgi.framework.BundleException;
064import org.osgi.framework.BundleListener;
065import org.osgi.framework.startlevel.BundleStartLevel;
066import org.osgi.framework.wiring.BundleRevision;
067
068/**
069 * 
070 */
071public class BundleListConlet
072        extends FreeMarkerConlet<BundleListConlet.BundleListModel>
073        implements BundleListener {
074
075    private static final Logger LOG
076        = Logger.getLogger(BundleListConlet.class.getName());
077
078    private static final Set<RenderMode> MODES = RenderMode.asSet(
079        RenderMode.Preview, RenderMode.View);
080    private final BundleContext context;
081
082    /**
083     * Creates a new component with its channel set to the given channel.
084     * 
085     * @param componentChannel the channel that the component's handlers listen
086     *            on by default and that {@link Manager#fire(Event, Channel...)}
087     *            sends the event to
088     */
089    @SuppressWarnings("PMD.UnusedFormalParameter")
090    public BundleListConlet(Channel componentChannel, BundleContext context,
091            Map<?, ?> properties) {
092        super(componentChannel);
093        this.context = context;
094        context.addBundleListener(this);
095    }
096
097    /**
098     * On {@link ConsoleReady}, fire the {@link AddConletType}.
099     *
100     * @param event the event
101     * @param channel the channel
102     * @throws TemplateNotFoundException the template not found exception
103     * @throws MalformedTemplateNameException the malformed template name
104     *             exception
105     * @throws ParseException the parse exception
106     * @throws IOException Signals that an I/O exception has occurred.
107     */
108    @Handler
109    public void onConsoleReady(ConsoleReady event, ConsoleConnection channel)
110            throws TemplateNotFoundException, MalformedTemplateNameException,
111            ParseException, IOException {
112        // Add conlet resources to page
113        channel.respond(new AddConletType(type())
114            .addRenderMode(RenderMode.Preview).setDisplayNames(
115                localizations(channel.supportedLocales(), "conletName"))
116            .addScript(new ScriptResource()
117                .setScriptUri(event.renderSupport().conletResource(
118                    type(), "Bundles-functions.ftl.js"))
119                .setScriptType("module"))
120            .addCss(event.renderSupport(),
121                WebConsoleUtils.uriFromPath("Bundles-style.css")));
122    }
123
124    @Override
125    protected Optional<BundleListModel> createNewState(AddConletRequest event,
126            ConsoleConnection channel, String conletId) throws Exception {
127        BundleListModel conletModel = new BundleListModel(conletId);
128        return Optional.of(conletModel);
129    }
130
131    @Override
132    protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
133            ConsoleConnection channel, String conletId,
134            BundleListModel conletModel) throws Exception {
135        Set<RenderMode> renderedAs = new HashSet<>();
136        if (event.renderAs().contains(RenderMode.Preview)) {
137            Template tpl
138                = freemarkerConfig().getTemplate("Bundles-preview.ftl.html");
139            channel.respond(new RenderConlet(type(), conletId,
140                processTemplate(event, tpl,
141                    fmModel(event, channel, conletId, conletModel)))
142                        .setRenderAs(
143                            RenderMode.Preview.addModifiers(event.renderAs()))
144                        .setSupportedModes(MODES));
145            List<Map<String, Object>> bundleInfos
146                = Arrays.stream(context.getBundles())
147                    .map(bndl -> createBundleInfo(bndl, channel.locale()))
148                    .collect(Collectors.toList());
149            channel.respond(new NotifyConletView(type(),
150                conletId, "bundleUpdates", bundleInfos, "preview", true));
151            renderedAs.add(RenderMode.Preview);
152        }
153        if (event.renderAs().contains(RenderMode.View)) {
154            Template tpl
155                = freemarkerConfig().getTemplate("Bundles-view.ftl.html");
156            channel.respond(new RenderConlet(type(), conletId,
157                processTemplate(event, tpl,
158                    fmModel(event, channel, conletId, conletModel)))
159                        .setRenderAs(
160                            RenderMode.View.addModifiers(event.renderAs())));
161            List<Map<String, Object>> bundleInfos
162                = Arrays.stream(context.getBundles())
163                    .map(bndl -> createBundleInfo(bndl, channel.locale()))
164                    .collect(Collectors.toList());
165            channel.respond(new NotifyConletView(type(),
166                conletId, "bundleUpdates", bundleInfos, "view", true));
167            renderedAs.add(RenderMode.View);
168        }
169        return renderedAs;
170    }
171
172    private Map<String, Object> createBundleInfo(Bundle bundle, Locale locale) {
173        @SuppressWarnings("PMD.UseConcurrentHashMap")
174        Map<String, Object> result = new HashMap<>();
175        result.put("id", bundle.getBundleId());
176        result.put("name",
177            Optional.ofNullable(bundle.getHeaders(locale.toString())
178                .get("Bundle-Name")).orElse(bundle.getSymbolicName()));
179        result.put("symbolicName", bundle.getSymbolicName());
180        result.put("version", bundle.getVersion().toString());
181        result.put("category",
182            Optional.ofNullable(bundle.getHeaders(locale.toString())
183                .get("Bundle-Category")).orElse(""));
184        result.put("state", "bundleState_" + bundle.getState());
185        result.put("startable", false);
186        result.put("stoppable", false);
187        if ((bundle.getState()
188            & (Bundle.RESOLVED | Bundle.INSTALLED | Bundle.ACTIVE)) != 0) {
189            boolean isFragment = (bundle.adapt(BundleRevision.class).getTypes()
190                & BundleRevision.TYPE_FRAGMENT) != 0;
191            result.put("startable", !isFragment
192                && (bundle.getState() == Bundle.INSTALLED
193                    || bundle.getState() == Bundle.RESOLVED));
194            result.put("stoppable",
195                !isFragment && bundle.getState() == Bundle.ACTIVE);
196        }
197        result.put("uninstallable", (bundle.getState()
198            & (Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE)) != 0);
199        result.put("uninstalled", bundle.getState() == Bundle.UNINSTALLED);
200        return result;
201    }
202
203    @Override
204    protected void doUpdateConletState(NotifyConletModel event,
205            ConsoleConnection channel, BundleListModel conletState)
206            throws Exception {
207        event.stop();
208        Bundle bundle = context.getBundle(event.params().asInt(0));
209        if (bundle == null) {
210            return;
211        }
212        try {
213            switch (event.method()) {
214            case "stop":
215                bundle.stop();
216                break;
217            case "start":
218                bundle.start();
219                break;
220            case "refresh":
221                break;
222            case "update":
223                bundle.update();
224                break;
225            case "uninstall":
226                bundle.uninstall();
227                break;
228            case "sendDetails":
229                sendBundleDetails(event.conletId(), channel, bundle);
230                break;
231            default:// ignore
232                break;
233            }
234        } catch (BundleException e) {
235            // ignore
236            LOG.log(Level.WARNING, "Cannot update bundle state", e);
237        }
238    }
239
240    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
241    private void sendBundleDetails(String conletId, ConsoleConnection channel,
242            Bundle bundle) {
243        Locale locale = channel.locale();
244        List<Object> data = new ArrayList<>();
245        data.add(
246            new Object[] { "bundleSymbolicName", bundle.getSymbolicName() });
247        data.add(
248            new Object[] { "bundleVersion", bundle.getVersion().toString() });
249        data.add(new Object[] { "bundleLocation",
250            bundle.getLocation().replace(".", ".&#x200b;") });
251        data.add(new Object[] { "bundleLastModification",
252            Instant.ofEpochMilli(bundle.getLastModified()).toString(),
253            "dateTime" });
254        data.add(new Object[] { "bundleStartLevel",
255            bundle.adapt(BundleStartLevel.class).getStartLevel() });
256        Dictionary<String, String> dict = bundle.getHeaders(locale.toString());
257        @SuppressWarnings("PMD.UseConcurrentHashMap")
258        Map<String, String> headers = new TreeMap<>();
259        for (Enumeration<String> e = dict.keys(); e.hasMoreElements();) {
260            String key = e.nextElement();
261            headers.put(key, dict.get(key));
262        }
263        List<Object> headerList = new ArrayList<>();
264        for (Map.Entry<String, String> e : headers.entrySet()) {
265            headerList.add(new Object[] { e.getKey(),
266                e.getKey().contains("Package")
267                    ? e.getValue().replace(".", ".&#x200b;")
268                    : e.getValue() });
269        }
270        data.add(new Object[] { "manifestHeaders", headerList, "table" });
271        channel.respond(new NotifyConletView(type(),
272            conletId, "bundleDetails", bundle.getBundleId(), data));
273    }
274
275    /**
276     * Translates the OSGi {@link BundleEvent} to a JGrapes event and fires it
277     * on all known console session channels.
278     *
279     * @param event the event
280     */
281    @Override
282    public void bundleChanged(BundleEvent event) {
283        fire(new BundleChanged(event), trackedConnections());
284    }
285
286    /**
287     * Handles a {@link BundleChanged} event by updating the information in the
288     * console sessions.
289     *
290     * @param event the event
291     */
292    @Handler
293    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
294    public void onBundleChanged(BundleChanged event,
295            ConsoleConnection channel) {
296        for (String conletId : conletIds(channel)) {
297            channel.respond(new NotifyConletView(type(), conletId,
298                "bundleUpdates",
299                (Object) new Object[] { createBundleInfo(
300                    event.bundleEvent().getBundle(),
301                    channel.locale()) },
302                "*", false));
303        }
304    }
305
306    @Override
307    protected boolean doSetLocale(SetLocale event, ConsoleConnection channel,
308            String conletId) throws Exception {
309        return true;
310    }
311
312    /**
313     * Wraps an OSGi {@link BundleEvent}.
314     */
315    public static class BundleChanged extends Event<Void> {
316        private final BundleEvent bundleEvent;
317
318        /**
319         * Instantiates a new event.
320         *
321         * @param bundleEvent the OSGi bundle event
322         */
323        public BundleChanged(BundleEvent bundleEvent) {
324            this.bundleEvent = bundleEvent;
325        }
326
327        /**
328         * Return the OSGi bundle event.
329         *
330         * @return the bundle event
331         */
332        public BundleEvent bundleEvent() {
333            return bundleEvent;
334        }
335
336    }
337
338    /**
339     * The bundle's model.
340     */
341    public class BundleListModel extends ConletBaseModel {
342
343        /**
344         * Instantiates a new bundle list model.
345         *
346         * @param conletId the web console component id
347         */
348        public BundleListModel(String conletId) {
349            super(conletId);
350        }
351
352        /**
353         * Return the bundles.
354         *
355         * @return the bundle[]
356         */
357        public Bundle[] bundles() {
358            return context.getBundles();
359        }
360
361    }
362}