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.services;
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.util.Arrays;
029import java.util.HashMap;
030import java.util.List;
031import java.util.Locale;
032import java.util.Map;
033import java.util.Optional;
034import java.util.ResourceBundle;
035import java.util.Set;
036import java.util.stream.Collectors;
037
038import org.jgrapes.core.Channel;
039import org.jgrapes.core.Event;
040import org.jgrapes.core.Manager;
041import org.jgrapes.core.annotation.Handler;
042import org.jgrapes.http.Session;
043import org.jgrapes.portal.base.PortalSession;
044import org.jgrapes.portal.base.PortalUtils;
045import org.jgrapes.portal.base.Portlet.RenderMode;
046
047import static org.jgrapes.portal.base.Portlet.RenderMode.DeleteablePreview;
048import static org.jgrapes.portal.base.Portlet.RenderMode.View;
049
050import org.jgrapes.portal.base.events.AddPageResources.ScriptResource;
051import org.jgrapes.portal.base.events.AddPortletRequest;
052import org.jgrapes.portal.base.events.AddPortletType;
053import org.jgrapes.portal.base.events.DeletePortlet;
054import org.jgrapes.portal.base.events.DeletePortletRequest;
055import org.jgrapes.portal.base.events.NotifyPortletView;
056import org.jgrapes.portal.base.events.PortalReady;
057import org.jgrapes.portal.base.events.RenderPortletRequest;
058import org.jgrapes.portal.base.events.RenderPortletRequestBase;
059import org.jgrapes.portal.base.freemarker.FreeMarkerPortlet;
060import org.osgi.framework.Bundle;
061import org.osgi.framework.BundleContext;
062import org.osgi.framework.Constants;
063import org.osgi.framework.InvalidSyntaxException;
064import org.osgi.framework.ServiceEvent;
065import org.osgi.framework.ServiceListener;
066import org.osgi.framework.ServiceReference;
067import org.osgi.service.component.runtime.ServiceComponentRuntime;
068import org.osgi.service.component.runtime.dto.ComponentDescriptionDTO;
069
070/**
071 * A portlet for inspecting the services in an OSGi runtime.
072 */
073public class ServiceListPortlet extends FreeMarkerPortlet
074        implements ServiceListener {
075
076    private final ServiceComponentRuntime scr;
077    private static final Set<RenderMode> MODES = RenderMode.asSet(
078        DeleteablePreview, View);
079    private final BundleContext context;
080
081    /**
082     * Creates a new component with its channel set to the given channel.
083     * 
084     * @param componentChannel the channel that the component's handlers listen
085     *            on by default and that {@link Manager#fire(Event, Channel...)}
086     *            sends the event to
087     */
088    public ServiceListPortlet(Channel componentChannel, BundleContext context,
089            ServiceComponentRuntime scr) {
090        super(componentChannel, true);
091        this.context = context;
092        this.scr = scr;
093        context.addServiceListener(this);
094    }
095
096    /**
097     * On {@link PortalReady}, fire the {@link AddPortletType}.
098     *
099     * @param event the event
100     * @param channel the channel
101     * @throws TemplateNotFoundException the template not found exception
102     * @throws MalformedTemplateNameException the malformed template name
103     *             exception
104     * @throws ParseException the parse exception
105     * @throws IOException Signals that an I/O exception has occurred.
106     */
107    @Handler
108    public void onPortalReady(PortalReady event, PortalSession channel)
109            throws TemplateNotFoundException, MalformedTemplateNameException,
110            ParseException, IOException {
111        ResourceBundle resourceBundle = resourceBundle(channel.locale());
112        // Add portlet resources to page
113        channel.respond(new AddPortletType(type())
114            .setDisplayName(resourceBundle.getString("portletName"))
115            .addScript(new ScriptResource()
116                .setRequires(new String[] { "vuejs.org" })
117                .setScriptUri(event.renderSupport().portletResource(
118                    type(), "Services-functions.ftl.js")))
119            .addCss(event.renderSupport(),
120                PortalUtils.uriFromPath("Services-style.css"))
121            .setInstantiable());
122    }
123
124    /*
125     * (non-Javadoc)
126     * 
127     * @see org.jgrapes.portal.AbstractPortlet#generatePortletId()
128     */
129    @Override
130    protected String generatePortletId() {
131        return type() + "-" + super.generatePortletId();
132    }
133
134    /*
135     * (non-Javadoc)
136     * 
137     * @see org.jgrapes.portal.AbstractPortlet#modelFromSession
138     */
139    @SuppressWarnings("unchecked")
140    @Override
141    protected <T extends Serializable> Optional<T> stateFromSession(
142            Session session, String portletId, Class<T> type) {
143        if (portletId.startsWith(type() + "-")) {
144            return Optional.of((T) new ServiceListModel(portletId));
145        }
146        return Optional.empty();
147    }
148
149    /*
150     * (non-Javadoc)
151     * 
152     * @see org.jgrapes.portal.AbstractPortlet#doAddPortlet
153     */
154    @Override
155    protected String doAddPortlet(AddPortletRequest event,
156            PortalSession channel)
157            throws Exception {
158        ServiceListModel portletModel
159            = new ServiceListModel(generatePortletId());
160        renderPortlet(event, channel, portletModel);
161        return portletModel.getPortletId();
162    }
163
164    /*
165     * (non-Javadoc)
166     * 
167     * @see org.jgrapes.portal.AbstractPortlet#doRenderPortlet
168     */
169    @Override
170    protected void doRenderPortlet(RenderPortletRequest event,
171            PortalSession channel, String portletId,
172            Serializable retrievedState)
173            throws Exception {
174        ServiceListModel portletModel = (ServiceListModel) retrievedState;
175        renderPortlet(event, channel, portletModel);
176    }
177
178    private void renderPortlet(RenderPortletRequestBase<?> event,
179            PortalSession channel,
180            ServiceListModel portletModel) throws TemplateNotFoundException,
181            MalformedTemplateNameException, ParseException, IOException,
182            InvalidSyntaxException {
183        switch (event.renderMode()) {
184        case Preview:
185        case DeleteablePreview: {
186            Template tpl
187                = freemarkerConfig().getTemplate("Services-preview.ftl.html");
188            channel.respond(new RenderPortletFromTemplate(event,
189                ServiceListPortlet.class, portletModel.getPortletId(),
190                tpl, fmModel(event, channel, portletModel))
191                    .setRenderMode(DeleteablePreview).setSupportedModes(MODES)
192                    .setForeground(event.isForeground()));
193            List<Map<String, Object>> serviceInfos = Arrays.stream(
194                context.getAllServiceReferences(null, null))
195                .map(svc -> createServiceInfo(svc, channel.locale()))
196                .collect(Collectors.toList());
197            channel.respond(new NotifyPortletView(type(),
198                portletModel.getPortletId(), "serviceUpdates", serviceInfos,
199                "preview", true));
200            break;
201        }
202        case View: {
203            Template tpl
204                = freemarkerConfig().getTemplate("Services-view.ftl.html");
205            channel.respond(new RenderPortletFromTemplate(event,
206                ServiceListPortlet.class, portletModel.getPortletId(),
207                tpl, fmModel(event, channel, portletModel))
208                    .setSupportedModes(MODES)
209                    .setForeground(event.isForeground()));
210            List<Map<String, Object>> serviceInfos = Arrays.stream(
211                context.getAllServiceReferences(null, null))
212                .map(svc -> createServiceInfo(svc, channel.locale()))
213                .collect(Collectors.toList());
214            channel.respond(new NotifyPortletView(type(),
215                portletModel.getPortletId(), "serviceUpdates", serviceInfos,
216                "view", true));
217            break;
218        }
219        default:
220            break;
221        }
222    }
223
224    @SuppressWarnings({ "PMD.NcssCount", "PMD.ConfusingTernary" })
225    private Map<String, Object>
226            createServiceInfo(ServiceReference<?> serviceRef, Locale locale) {
227        @SuppressWarnings("PMD.UseConcurrentHashMap")
228        Map<String, Object> result = new HashMap<>();
229        result.put("id",
230            serviceRef.getProperty(Constants.SERVICE_ID));
231        String[] interfaces
232            = (String[]) serviceRef.getProperty(Constants.OBJECTCLASS);
233        result.put("type", String.join(", ", interfaces));
234        Long bundleId
235            = (Long) serviceRef.getProperty(Constants.SERVICE_BUNDLEID);
236        result.put("bundleId", bundleId.toString());
237        Bundle bundle = context.getBundle(bundleId);
238        if (bundle == null) {
239            result.put("bundleName", "");
240        } else {
241            result
242                .put(
243                    "bundleName", Optional
244                        .ofNullable(bundle.getHeaders(locale.toString())
245                            .get("Bundle-Name"))
246                        .orElse(bundle.getSymbolicName()));
247        }
248        String scope;
249        switch ((String) serviceRef.getProperty(Constants.SERVICE_SCOPE)) {
250        case Constants.SCOPE_BUNDLE:
251            scope = "serviceScopeBundle";
252            break;
253        case Constants.SCOPE_PROTOTYPE:
254            scope = "serviceScopePrototype";
255            break;
256        case Constants.SCOPE_SINGLETON:
257            scope = "serviceScopeSingleton";
258            break;
259        default:
260            scope = "";
261            break;
262        }
263        result.put("scope", scope);
264        Integer ranking
265            = (Integer) serviceRef.getProperty(Constants.SERVICE_RANKING);
266        result.put("ranking", ranking == null ? "" : ranking.toString());
267        String componentName
268            = (String) serviceRef.getProperty("component.name");
269        ComponentDescriptionDTO dto;
270        if (componentName != null && bundle != null
271            && (dto = scr.getComponentDescriptionDTO(bundle,
272                componentName)) != null) {
273            result.put("dsScope", "serviceScope"
274                + dto.scope.substring(0, 1).toUpperCase(Locale.US)
275                + dto.scope.substring(1));
276            result.put("implementationClass", dto.implementationClass);
277        } else {
278            Object service = context.getService(serviceRef);
279            if (service != null) {
280                result.put("implementationClass", service.getClass().getName());
281                context.ungetService(serviceRef);
282            } else {
283                result.put("implementationClass", "");
284            }
285        }
286        return result;
287    }
288
289    /*
290     * (non-Javadoc)
291     * 
292     * @see org.jgrapes.portal.AbstractPortlet#doDeletePortlet
293     */
294    @Override
295    protected void doDeletePortlet(DeletePortletRequest event,
296            PortalSession channel,
297            String portletId, Serializable portletState) throws Exception {
298        channel.respond(new DeletePortlet(portletId));
299    }
300
301    /**
302     * Translates the OSGi {@link ServiceEvent} to a JGrapes event and fires it
303     * on all known portal session channels.
304     *
305     * @param event the event
306     */
307    @Override
308    public void serviceChanged(ServiceEvent event) {
309        fire(new ServiceChanged(event), trackedSessions());
310    }
311
312    /**
313     * Handles a {@link ServiceChanged} event by updating the information in the
314     * portal sessions.
315     *
316     * @param event the event
317     */
318    @Handler
319    @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops",
320        "PMD.DataflowAnomalyAnalysis" })
321    public void onServiceChanged(ServiceChanged event,
322            PortalSession portalSession) {
323        Map<String, Object> info = createServiceInfo(
324            event.serviceEvent().getServiceReference(), portalSession.locale());
325        if (event.serviceEvent().getType() == ServiceEvent.UNREGISTERING) {
326            info.put("updateType", "unregistering");
327        }
328        for (String portletId : portletIds(portalSession)) {
329            portalSession.respond(new NotifyPortletView(
330                type(), portletId, "serviceUpdates",
331                (Object) new Object[] { info }, "*", false));
332        }
333    }
334
335    /**
336     * Wraps an OSGi {@link ServiceEvent}.
337     */
338    public static class ServiceChanged extends Event<Void> {
339        private final ServiceEvent serviceEvent;
340
341        /**
342         * Instantiates a new event.
343         *
344         * @param serviceEvent the service event
345         */
346        public ServiceChanged(ServiceEvent serviceEvent) {
347            this.serviceEvent = serviceEvent;
348        }
349
350        public ServiceEvent serviceEvent() {
351            return serviceEvent;
352        }
353    }
354
355    /**
356     * The portlet's model.
357     */
358    @SuppressWarnings("serial")
359    public class ServiceListModel extends PortletBaseModel {
360
361        /**
362         * Instantiates a new service list model.
363         *
364         * @param portletId the portlet id
365         */
366        public ServiceListModel(String portletId) {
367            super(portletId);
368        }
369
370    }
371}