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.portlets.bundles;
020
021import freemarker.core.ParseException;
022import freemarker.template.MalformedTemplateNameException;
023import freemarker.template.Template;
024import freemarker.template.TemplateNotFoundException;
025
026import java.io.IOException;
027import java.io.Serializable;
028import java.time.Instant;
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.Dictionary;
032import java.util.Enumeration;
033import java.util.HashMap;
034import java.util.List;
035import java.util.Locale;
036import java.util.Map;
037import java.util.Optional;
038import java.util.ResourceBundle;
039import java.util.Set;
040import java.util.TreeMap;
041import java.util.logging.Level;
042import java.util.logging.Logger;
043import java.util.stream.Collectors;
044
045import org.jgrapes.core.Channel;
046import org.jgrapes.core.Event;
047import org.jgrapes.core.Manager;
048import org.jgrapes.core.annotation.Handler;
049import org.jgrapes.http.Session;
050import org.jgrapes.portal.base.PortalSession;
051import org.jgrapes.portal.base.PortalUtils;
052
053import static org.jgrapes.portal.base.Portlet.RenderMode;
054
055import org.jgrapes.portal.base.events.AddPageResources.ScriptResource;
056import org.jgrapes.portal.base.events.AddPortletRequest;
057import org.jgrapes.portal.base.events.AddPortletType;
058import org.jgrapes.portal.base.events.DeletePortlet;
059import org.jgrapes.portal.base.events.DeletePortletRequest;
060import org.jgrapes.portal.base.events.NotifyPortletModel;
061import org.jgrapes.portal.base.events.NotifyPortletView;
062import org.jgrapes.portal.base.events.PortalReady;
063import org.jgrapes.portal.base.events.RenderPortletRequest;
064import org.jgrapes.portal.base.events.RenderPortletRequestBase;
065import org.jgrapes.portal.base.freemarker.FreeMarkerPortlet;
066
067import org.osgi.framework.Bundle;
068import org.osgi.framework.BundleContext;
069import org.osgi.framework.BundleEvent;
070import org.osgi.framework.BundleException;
071import org.osgi.framework.BundleListener;
072import org.osgi.framework.startlevel.BundleStartLevel;
073import org.osgi.framework.wiring.BundleRevision;
074
075/**
076 * 
077 */
078public class BundleListPortlet extends FreeMarkerPortlet
079        implements BundleListener {
080
081    private static final Logger logger
082        = Logger.getLogger(BundleListPortlet.class.getName());
083
084    private static final Set<RenderMode> MODES = RenderMode.asSet(
085        RenderMode.DeleteablePreview, RenderMode.View);
086    private final BundleContext context;
087
088    /**
089     * Creates a new component with its channel set to the given channel.
090     * 
091     * @param componentChannel the channel that the component's handlers listen
092     *            on by default and that {@link Manager#fire(Event, Channel...)}
093     *            sends the event to
094     */
095    public BundleListPortlet(Channel componentChannel, BundleContext context,
096            Map<Object, Object> properties) {
097        super(componentChannel, true);
098        this.context = context;
099        context.addBundleListener(this);
100    }
101
102    /**
103     * On {@link PortalReady}, fire the {@link AddPortletType}.
104     *
105     * @param event the event
106     * @param channel the channel
107     * @throws TemplateNotFoundException the template not found exception
108     * @throws MalformedTemplateNameException the malformed template name
109     *             exception
110     * @throws ParseException the parse exception
111     * @throws IOException Signals that an I/O exception has occurred.
112     */
113    @Handler
114    public void onPortalReady(PortalReady event, PortalSession channel)
115            throws TemplateNotFoundException, MalformedTemplateNameException,
116            ParseException, IOException {
117        ResourceBundle resourceBundle = resourceBundle(channel.locale());
118        // Add portlet resources to page
119        channel.respond(new AddPortletType(type())
120            .setDisplayName(resourceBundle.getString("portletName"))
121            .addScript(new ScriptResource()
122                .setRequires(new String[] { "vuejs.org" })
123                .setScriptUri(event.renderSupport().portletResource(
124                    type(), "Bundles-functions.ftl.js")))
125            .addCss(event.renderSupport(),
126                PortalUtils.uriFromPath("Bundles-style.css"))
127            .setInstantiable());
128    }
129
130    /*
131     * (non-Javadoc)
132     * 
133     * @see org.jgrapes.portal.AbstractPortlet#generatePortletId()
134     */
135    @Override
136    protected String generatePortletId() {
137        return type() + "-" + super.generatePortletId();
138    }
139
140    /*
141     * (non-Javadoc)
142     * 
143     * @see org.jgrapes.portal.AbstractPortlet#modelFromSession
144     */
145    @SuppressWarnings("unchecked")
146    @Override
147    protected <T extends Serializable> Optional<T> stateFromSession(
148            Session session, String portletId, Class<T> type) {
149        if (portletId.startsWith(type() + "-")) {
150            return Optional.of((T) new BundleListModel(portletId));
151        }
152        return Optional.empty();
153    }
154
155    /*
156     * (non-Javadoc)
157     * 
158     * @see org.jgrapes.portal.AbstractPortlet#doAddPortlet
159     */
160    @Override
161    @SuppressWarnings("PMD.AvoidDuplicateLiterals")
162    protected String doAddPortlet(AddPortletRequest event,
163            PortalSession channel)
164            throws Exception {
165        BundleListModel portletModel = new BundleListModel(generatePortletId());
166        Template tpl
167            = freemarkerConfig().getTemplate("Bundles-preview.ftl.html");
168        channel.respond(new RenderPortletFromTemplate(event,
169            BundleListPortlet.class, portletModel.getPortletId(),
170            tpl, fmModel(event, channel, portletModel))
171                .setRenderMode(RenderMode.DeleteablePreview)
172                .setSupportedModes(MODES)
173                .setForeground(true));
174        List<Map<String, Object>> bundleInfos
175            = Arrays.stream(context.getBundles())
176                .map(bndl -> createBundleInfo(bndl, channel.locale()))
177                .collect(Collectors.toList());
178        channel.respond(new NotifyPortletView(type(),
179            portletModel.getPortletId(), "bundleUpdates", bundleInfos,
180            "preview", true));
181        return portletModel.getPortletId();
182    }
183
184    /*
185     * (non-Javadoc)
186     * 
187     * @see org.jgrapes.portal.AbstractPortlet#doRenderPortlet
188     */
189    @Override
190    protected void doRenderPortlet(RenderPortletRequest event,
191            PortalSession channel, String portletId,
192            Serializable retrievedState)
193            throws Exception {
194        BundleListModel portletModel = (BundleListModel) retrievedState;
195        renderPortlet(event, channel, portletModel);
196    }
197
198    private void renderPortlet(RenderPortletRequestBase<?> event,
199            PortalSession channel,
200            BundleListModel portletModel) throws TemplateNotFoundException,
201            MalformedTemplateNameException, ParseException, IOException {
202        switch (event.renderMode()) {
203        case Preview:
204        case DeleteablePreview: {
205            Template tpl
206                = freemarkerConfig().getTemplate("Bundles-preview.ftl.html");
207            channel.respond(new RenderPortletFromTemplate(event,
208                BundleListPortlet.class, portletModel.getPortletId(),
209                tpl, fmModel(event, channel, portletModel))
210                    .setRenderMode(RenderMode.DeleteablePreview)
211                    .setSupportedModes(MODES)
212                    .setForeground(event.isForeground()));
213            List<Map<String, Object>> bundleInfos
214                = Arrays.stream(context.getBundles())
215                    .map(bndl -> createBundleInfo(bndl, channel.locale()))
216                    .collect(Collectors.toList());
217            channel.respond(new NotifyPortletView(type(),
218                portletModel.getPortletId(), "bundleUpdates", bundleInfos,
219                "preview", true));
220            break;
221        }
222        case View: {
223            Template tpl
224                = freemarkerConfig().getTemplate("Bundles-view.ftl.html");
225            channel.respond(new RenderPortletFromTemplate(event,
226                BundleListPortlet.class, portletModel.getPortletId(),
227                tpl, fmModel(event, channel, portletModel))
228                    .setRenderMode(RenderMode.View).setSupportedModes(MODES)
229                    .setForeground(event.isForeground()));
230            List<Map<String, Object>> bundleInfos
231                = Arrays.stream(context.getBundles())
232                    .map(bndl -> createBundleInfo(bndl, channel.locale()))
233                    .collect(Collectors.toList());
234            channel.respond(new NotifyPortletView(type(),
235                portletModel.getPortletId(), "bundleUpdates", bundleInfos,
236                "view", true));
237            break;
238        }
239        default:
240            break;
241        }
242    }
243
244    private Map<String, Object> createBundleInfo(Bundle bundle, Locale locale) {
245        @SuppressWarnings("PMD.UseConcurrentHashMap")
246        Map<String, Object> result = new HashMap<>();
247        result.put("id", bundle.getBundleId());
248        result.put("name",
249            Optional.ofNullable(bundle.getHeaders(locale.toString())
250                .get("Bundle-Name")).orElse(bundle.getSymbolicName()));
251        result.put("symbolicName", bundle.getSymbolicName());
252        result.put("version", bundle.getVersion().toString());
253        result.put("category",
254            Optional.ofNullable(bundle.getHeaders(locale.toString())
255                .get("Bundle-Category")).orElse(""));
256        ResourceBundle resources = resourceBundle(locale);
257        result.put("state",
258            resources.getString("bundleState_" + bundle.getState()));
259        result.put("startable", false);
260        result.put("stoppable", false);
261        if ((bundle.getState()
262            & (Bundle.RESOLVED | Bundle.INSTALLED | Bundle.ACTIVE)) != 0) {
263            boolean isFragment = (bundle.adapt(BundleRevision.class).getTypes()
264                & BundleRevision.TYPE_FRAGMENT) != 0;
265            result.put("startable", !isFragment
266                && (bundle.getState() == Bundle.INSTALLED
267                    || bundle.getState() == Bundle.RESOLVED));
268            result.put("stoppable",
269                !isFragment && bundle.getState() == Bundle.ACTIVE);
270        }
271        result.put("uninstallable", (bundle.getState()
272            & (Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE)) != 0);
273        result.put("uninstalled", bundle.getState() == Bundle.UNINSTALLED);
274        return result;
275    }
276
277    /*
278     * (non-Javadoc)
279     * 
280     * @see org.jgrapes.portal.AbstractPortlet#doDeletePortlet
281     */
282    @Override
283    protected void doDeletePortlet(DeletePortletRequest event,
284            PortalSession channel, String portletId,
285            Serializable retrievedState) throws Exception {
286        channel.respond(new DeletePortlet(portletId));
287    }
288
289    /*
290     * (non-Javadoc)
291     * 
292     * @see org.jgrapes.portal.AbstractPortlet#doNotifyPortletModel
293     */
294    @Override
295    protected void doNotifyPortletModel(NotifyPortletModel event,
296            PortalSession channel, Serializable portletState)
297            throws Exception {
298        event.stop();
299        Bundle bundle = context.getBundle(event.params().asInt(0));
300        if (bundle == null) {
301            return;
302        }
303        try {
304            switch (event.method()) {
305            case "stop":
306                bundle.stop();
307                break;
308            case "start":
309                bundle.start();
310                break;
311            case "refresh":
312                break;
313            case "update":
314                bundle.update();
315                break;
316            case "uninstall":
317                bundle.uninstall();
318                break;
319            case "sendDetails":
320                sendBundleDetails(event.portletId(), channel, bundle);
321                break;
322            default:
323                // ignore
324                break;
325            }
326        } catch (BundleException e) {
327            // ignore
328            logger.log(Level.WARNING, "Cannot update bundle state", e);
329        }
330    }
331
332    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
333    private void sendBundleDetails(String portletId, PortalSession channel,
334            Bundle bundle) {
335        Locale locale = channel.locale();
336        ResourceBundle resources = resourceBundle(locale);
337        List<Object> data = new ArrayList<>();
338        data.add(new Object[] { resources.getString("bundleSymbolicName"),
339            bundle.getSymbolicName() });
340        data.add(new Object[] { resources.getString("bundleVersion"),
341            bundle.getVersion().toString() });
342        data.add(new Object[] { resources.getString("bundleLocation"),
343            bundle.getLocation().replace(".", ".&#x200b;") });
344        data.add(new Object[] { resources.getString("bundleLastModification"),
345            Instant.ofEpochMilli(bundle.getLastModified()).toString(),
346            "dateTime" });
347        data.add(new Object[] { resources.getString("bundleStartLevel"),
348            bundle.adapt(BundleStartLevel.class).getStartLevel() });
349        Dictionary<String, String> dict = bundle.getHeaders(locale.toString());
350        @SuppressWarnings("PMD.UseConcurrentHashMap")
351        Map<String, String> headers = new TreeMap<>();
352        for (Enumeration<String> e = dict.keys(); e.hasMoreElements();) {
353            String key = e.nextElement();
354            headers.put(key, dict.get(key));
355        }
356        List<Object> headerList = new ArrayList<>();
357        for (Map.Entry<String, String> e : headers.entrySet()) {
358            headerList.add(new Object[] { e.getKey(),
359                e.getKey().contains("Package")
360                    ? e.getValue().replace(".", ".&#x200b;")
361                    : e.getValue() });
362        }
363        data.add(
364            new Object[] { resources.getString("manifestHeaders"), headerList,
365                "table" });
366        channel.respond(new NotifyPortletView(type(),
367            portletId, "bundleDetails", bundle.getBundleId(), data));
368    }
369
370    /**
371     * Translates the OSGi {@link BundleEvent} to a JGrapes event and fires it
372     * on all known portal session channels.
373     *
374     * @param event the event
375     */
376    @Override
377    public void bundleChanged(BundleEvent event) {
378        fire(new BundleChanged(event), trackedSessions());
379    }
380
381    /**
382     * Handles a {@link BundleChanged} event by updating the information in the
383     * portal sessions.
384     *
385     * @param event the event
386     */
387    @Handler
388    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
389    public void onBundleChanged(BundleChanged event,
390            PortalSession portalSession) {
391        for (String portletId : portletIds(portalSession)) {
392            portalSession.respond(new NotifyPortletView(type(), portletId,
393                "bundleUpdates",
394                (Object) new Object[] { createBundleInfo(
395                    event.bundleEvent().getBundle(), portalSession.locale()) },
396                "*", false));
397        }
398    }
399
400    /**
401     * Wraps an OSGi {@link BundleEvent}.
402     */
403    public static class BundleChanged extends Event<Void> {
404        private final BundleEvent bundleEvent;
405
406        /**
407         * Instantiates a new event.
408         *
409         * @param bundleEvent the OSGi bundle event
410         */
411        public BundleChanged(BundleEvent bundleEvent) {
412            this.bundleEvent = bundleEvent;
413        }
414
415        /**
416         * Return the OSGi bundle event.
417         *
418         * @return the bundle event
419         */
420        public BundleEvent bundleEvent() {
421            return bundleEvent;
422        }
423
424    }
425
426    /**
427     * The bundle's model.
428     */
429    @SuppressWarnings("serial")
430    public class BundleListModel extends PortletBaseModel {
431
432        /**
433         * Instantiates a new bundle list model.
434         *
435         * @param portletId the portlet id
436         */
437        public BundleListModel(String portletId) {
438            super(portletId);
439        }
440
441        /**
442         * Return the bundles.
443         *
444         * @return the bundle[]
445         */
446        public Bundle[] bundles() {
447            return context.getBundles();
448        }
449
450    }
451}