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.services;
020
021import freemarker.core.ParseException;
022import freemarker.template.MalformedTemplateNameException;
023import freemarker.template.Template;
024import freemarker.template.TemplateNotFoundException;
025import java.io.IOException;
026import java.io.Serializable;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.HashMap;
030import java.util.HashSet;
031import java.util.List;
032import java.util.Locale;
033import java.util.Map;
034import java.util.Optional;
035import java.util.Set;
036import java.util.stream.Collectors;
037import org.jgrapes.core.Channel;
038import org.jgrapes.core.Event;
039import org.jgrapes.core.Manager;
040import org.jgrapes.core.annotation.Handler;
041import org.jgrapes.webconsole.base.Conlet.RenderMode;
042import org.jgrapes.webconsole.base.ConsoleConnection;
043import org.jgrapes.webconsole.base.WebConsoleUtils;
044import org.jgrapes.webconsole.base.events.AddConletType;
045import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource;
046import org.jgrapes.webconsole.base.events.ConsoleReady;
047import org.jgrapes.webconsole.base.events.NotifyConletView;
048import org.jgrapes.webconsole.base.events.RenderConlet;
049import org.jgrapes.webconsole.base.events.RenderConletRequestBase;
050import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
051import org.osgi.framework.Bundle;
052import org.osgi.framework.BundleContext;
053import org.osgi.framework.Constants;
054import org.osgi.framework.ServiceEvent;
055import org.osgi.framework.ServiceListener;
056import org.osgi.framework.ServiceReference;
057import org.osgi.service.component.runtime.ServiceComponentRuntime;
058import org.osgi.service.component.runtime.dto.ComponentDescriptionDTO;
059
060/**
061 * A conlet for inspecting the services in an OSGi runtime.
062 */
063@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
064public class ServiceListConlet
065        extends FreeMarkerConlet<Serializable> implements ServiceListener {
066
067    private final ServiceComponentRuntime scr;
068    private static final Set<RenderMode> MODES = RenderMode.asSet(
069        RenderMode.Preview, RenderMode.View);
070    private final BundleContext context;
071
072    /**
073     * Creates a new component with its channel set to the given channel.
074     * 
075     * @param componentChannel the channel that the component's handlers listen
076     *            on by default and that {@link Manager#fire(Event, Channel...)}
077     *            sends the event to
078     */
079    public ServiceListConlet(Channel componentChannel, BundleContext context,
080            ServiceComponentRuntime scr) {
081        super(componentChannel);
082        this.context = context;
083        this.scr = scr;
084        context.addServiceListener(this);
085    }
086
087    /**
088     * On {@link ConsoleReady}, fire the {@link AddConletType}.
089     *
090     * @param event the event
091     * @param channel the channel
092     * @throws TemplateNotFoundException the template not found exception
093     * @throws MalformedTemplateNameException the malformed template name
094     *             exception
095     * @throws ParseException the parse exception
096     * @throws IOException Signals that an I/O exception has occurred.
097     */
098    @Handler
099    public void onConsoleReady(ConsoleReady event, ConsoleConnection channel)
100            throws TemplateNotFoundException, MalformedTemplateNameException,
101            ParseException, IOException {
102        // Add conlet resources to page
103        channel.respond(new AddConletType(type())
104            .addRenderMode(RenderMode.Preview).setDisplayNames(
105                localizations(channel.supportedLocales(), "conletName"))
106            .addScript(new ScriptResource()
107                .setScriptUri(event.renderSupport().conletResource(
108                    type(), "Services-functions.ftl.js"))
109                .setScriptType("module"))
110            .addCss(event.renderSupport(),
111                WebConsoleUtils.uriFromPath("Services-style.css")));
112    }
113
114    @Override
115    protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
116            ConsoleConnection channel, String conletId,
117            Serializable conletState)
118            throws Exception {
119        Set<RenderMode> renderedAs = new HashSet<>();
120        if (event.renderAs().contains(RenderMode.Preview)) {
121            Template tpl
122                = freemarkerConfig().getTemplate("Services-preview.ftl.html");
123            channel.respond(new RenderConlet(type(), conletId,
124                processTemplate(event, tpl,
125                    fmModel(event, channel, conletId, conletState)))
126                        .setRenderAs(
127                            RenderMode.Preview.addModifiers(event.renderAs()))
128                        .setSupportedModes(MODES));
129            List<Map<String, Object>> serviceInfos = Arrays.stream(
130                context.getAllServiceReferences(null, null))
131                .map(svc -> createServiceInfo(svc, channel.locale()))
132                .collect(Collectors.toList());
133            channel.respond(new NotifyConletView(type(),
134                conletId, "serviceUpdates", serviceInfos, "preview", true));
135            renderedAs.add(RenderMode.Preview);
136        }
137        if (event.renderAs().contains(RenderMode.View)) {
138            Template tpl
139                = freemarkerConfig().getTemplate("Services-view.ftl.html");
140            channel.respond(new RenderConlet(type(), conletId,
141                processTemplate(event, tpl,
142                    fmModel(event, channel, conletId, conletState)))
143                        .setRenderAs(
144                            RenderMode.View.addModifiers(event.renderAs())));
145            List<Map<String, Object>> serviceInfos = Arrays.stream(
146                context.getAllServiceReferences(null, null))
147                .map(svc -> createServiceInfo(svc, channel.locale()))
148                .collect(Collectors.toList());
149            channel.respond(new NotifyConletView(type(),
150                conletId, "serviceUpdates", serviceInfos, "view", true));
151            renderedAs.add(RenderMode.View);
152        }
153        return renderedAs;
154    }
155
156    @SuppressWarnings({ "PMD.NcssCount", "PMD.ConfusingTernary",
157        "PMD.NPathComplexity", "PMD.AssignmentInOperand",
158        "PMD.CognitiveComplexity" })
159    private Map<String, Object>
160            createServiceInfo(ServiceReference<?> serviceRef, Locale locale) {
161        @SuppressWarnings("PMD.UseConcurrentHashMap")
162        Map<String, Object> result = new HashMap<>();
163        result.put("id", serviceRef.getProperty(Constants.SERVICE_ID));
164        String[] interfaces
165            = (String[]) serviceRef.getProperty(Constants.OBJECTCLASS);
166        result.put("type", String.join(", ", interfaces));
167        Long bundleId
168            = (Long) serviceRef.getProperty(Constants.SERVICE_BUNDLEID);
169        result.put("bundleId", bundleId.toString());
170        Bundle bundle = context.getBundle(bundleId);
171        if (bundle == null) {
172            result.put("bundleName", "");
173        } else {
174            result.put("bundleName", Optional
175                .ofNullable(bundle.getHeaders(locale.toString())
176                    .get("Bundle-Name"))
177                .orElse(bundle.getSymbolicName()));
178        }
179        String scope;
180        switch ((String) serviceRef.getProperty(Constants.SERVICE_SCOPE)) {
181        case Constants.SCOPE_BUNDLE:
182            scope = "serviceScopeBundle";
183            break;
184        case Constants.SCOPE_PROTOTYPE:
185            scope = "serviceScopePrototype";
186            break;
187        case Constants.SCOPE_SINGLETON:
188            scope = "serviceScopeSingleton";
189            break;
190        default:
191            scope = "";
192            break;
193        }
194        result.put("scope", scope);
195        Integer ranking
196            = (Integer) serviceRef.getProperty(Constants.SERVICE_RANKING);
197        result.put("ranking", ranking == null ? "" : ranking.toString());
198        String componentName
199            = (String) serviceRef.getProperty("component.name");
200        ComponentDescriptionDTO dto;
201        if (componentName != null && bundle != null
202            && (dto = scr.getComponentDescriptionDTO(bundle,
203                componentName)) != null) {
204            if (dto.scope != null) {
205                result.put("dsScope", "serviceScope"
206                    + dto.scope.substring(0, 1).toUpperCase(Locale.US)
207                    + dto.scope.substring(1));
208            }
209            result.put("implementationClass", dto.implementationClass);
210        } else {
211            Object service = context.getService(serviceRef);
212            if (service != null) {
213                result.put("implementationClass", service.getClass().getName());
214                context.ungetService(serviceRef);
215            } else {
216                result.put("implementationClass", "");
217            }
218        }
219        @SuppressWarnings("PMD.UseConcurrentHashMap")
220        Map<String, Object> properties = new HashMap<>();
221        for (String property : serviceRef.getPropertyKeys()) {
222            properties.put(property, serviceRef.getProperty(property));
223        }
224        result.put("properties", properties);
225        if (serviceRef.getUsingBundles() != null) {
226            List<String> using = new ArrayList<>();
227            for (Bundle bdl : serviceRef.getUsingBundles()) {
228                using
229                    .add(
230                        bdl.getSymbolicName() + " (" + bdl.getBundleId() + ")");
231            }
232            result.put("usingBundles", using);
233        }
234        return result;
235    }
236
237    /**
238     * Translates the OSGi {@link ServiceEvent} to a JGrapes event and fires it
239     * on all known console session channels.
240     *
241     * @param event the event
242     */
243    @Override
244    public void serviceChanged(ServiceEvent event) {
245        fire(new ServiceChanged(event), trackedConnections());
246    }
247
248    /**
249     * Handles a {@link ServiceChanged} event by updating the information in the
250     * console sessions.
251     *
252     * @param event the event
253     */
254    @Handler
255    @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops",
256        "PMD.DataflowAnomalyAnalysis" })
257    public void onServiceChanged(ServiceChanged event,
258            ConsoleConnection channel) {
259        Map<String, Object> info = createServiceInfo(
260            event.serviceEvent().getServiceReference(),
261            channel.locale());
262        if (event.serviceEvent().getType() == ServiceEvent.UNREGISTERING) {
263            info.put("updateType", "unregistering");
264        }
265        for (String conletId : conletIds(channel)) {
266            channel.respond(new NotifyConletView(
267                type(), conletId, "serviceUpdates",
268                (Object) new Object[] { info }, "*", false));
269        }
270    }
271
272    /**
273     * Wraps an OSGi {@link ServiceEvent}.
274     */
275    public static class ServiceChanged extends Event<Void> {
276        private final ServiceEvent serviceEvent;
277
278        /**
279         * Instantiates a new event.
280         *
281         * @param serviceEvent the service event
282         */
283        public ServiceChanged(ServiceEvent serviceEvent) {
284            this.serviceEvent = serviceEvent;
285        }
286
287        public ServiceEvent serviceEvent() {
288            return serviceEvent;
289        }
290    }
291}