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