001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2016, 2020  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.logviewer;
020
021import de.mnl.osgi.coreutils.ServiceCollector;
022import freemarker.core.ParseException;
023import freemarker.template.MalformedTemplateNameException;
024import freemarker.template.Template;
025import freemarker.template.TemplateNotFoundException;
026import java.io.ByteArrayOutputStream;
027import java.io.IOException;
028import java.io.PrintWriter;
029import java.io.Serializable;
030import java.util.Collections;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.Map;
034import java.util.Optional;
035import java.util.Set;
036import org.jgrapes.core.Channel;
037import org.jgrapes.core.Event;
038import org.jgrapes.core.Manager;
039import org.jgrapes.core.annotation.Handler;
040import org.jgrapes.core.events.Stop;
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.NotifyConletModel;
048import org.jgrapes.webconsole.base.events.NotifyConletView;
049import org.jgrapes.webconsole.base.events.RenderConlet;
050import org.jgrapes.webconsole.base.events.RenderConletRequestBase;
051import org.jgrapes.webconsole.base.events.SetLocale;
052import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
053import org.osgi.framework.BundleContext;
054import org.osgi.service.log.LogEntry;
055import org.osgi.service.log.LogListener;
056import org.osgi.service.log.LogReaderService;
057
058/**
059 * A conlet for displaying the OSGi log.
060 */
061public class LogViewerConlet extends FreeMarkerConlet<Serializable> {
062
063    private static final Set<RenderMode> MODES
064        = RenderMode.asSet(RenderMode.View);
065    private final ServiceCollector<LogReaderService,
066            LogReaderService> logReaderCollector;
067    private LogReaderService logReaderResolved;
068    private final LogListener logReaderListener;
069
070    /**
071     * Creates a new component with its channel set to the given channel.
072     * 
073     * @param componentChannel the channel that the component's handlers listen
074     *            on by default and that {@link Manager#fire(Event, Channel...)}
075     *            sends the event to
076     */
077    @SuppressWarnings("PMD.UnusedFormalParameter")
078    public LogViewerConlet(Channel componentChannel, BundleContext context) {
079        super(componentChannel);
080        logReaderListener = new LogListener() {
081            @Override
082            public void logged(LogEntry entry) {
083                addEntry(entry);
084            }
085        };
086        logReaderCollector
087            = new ServiceCollector<>(context, LogReaderService.class);
088        logReaderCollector.setOnBound((ref, svc) -> subscribeTo(svc))
089            .setOnModfied((ref, svc) -> subscribeTo(svc))
090            .setOnUnbinding((ref, svc) -> subscribeTo(svc));
091        logReaderCollector.open();
092    }
093
094    private void subscribeTo(LogReaderService logReaderService) {
095        if (logReaderResolved != null
096            && logReaderResolved.equals(logReaderService)) {
097            return;
098        }
099        // Got a new log reader service.
100        if (logReaderResolved != null) {
101            logReaderResolved.removeLogListener(logReaderListener);
102        }
103        logReaderResolved = logReaderService;
104        if (logReaderResolved != null) {
105            logReaderResolved.addLogListener(logReaderListener);
106        }
107
108    }
109
110    /**
111     * Detach from OSGi framework.
112     *
113     * @param event the event
114     */
115    @Handler(channels = Channel.class)
116    public void onStop(Stop event) {
117        logReaderCollector.close();
118    }
119
120    /**
121     * On {@link ConsoleReady}, fire the {@link AddConletType}.
122     *
123     * @param event the event
124     * @param channel the channel
125     * @throws TemplateNotFoundException the template not found exception
126     * @throws MalformedTemplateNameException the malformed template name
127     *             exception
128     * @throws ParseException the parse exception
129     * @throws IOException Signals that an I/O exception has occurred.
130     */
131    @Handler
132    public void onConsoleReady(ConsoleReady event, ConsoleConnection channel)
133            throws TemplateNotFoundException, MalformedTemplateNameException,
134            ParseException, IOException {
135        // Add conlet resources to page
136        channel.respond(new AddConletType(type())
137            .setDisplayNames(
138                localizations(channel.supportedLocales(), "conletName"))
139            .addRenderMode(RenderMode.View)
140            .addScript(new ScriptResource()
141                .setScriptUri(event.renderSupport().conletResource(
142                    type(), "LogViewer-functions.ftl.js"))
143                .setScriptType("module"))
144            .addCss(event.renderSupport(),
145                WebConsoleUtils.uriFromPath("LogViewer-style.css")));
146    }
147
148    @Override
149    protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
150            ConsoleConnection channel, String conletId,
151            Serializable conletState)
152            throws Exception {
153        Set<RenderMode> renderedAs = new HashSet<>();
154        if (event.renderAs().contains(RenderMode.View)) {
155            Template tpl
156                = freemarkerConfig().getTemplate("LogViewer-view.ftl.html");
157            channel.respond(new RenderConlet(type(), conletId,
158                processTemplate(event, tpl,
159                    fmModel(event, channel, conletId, conletState)))
160                        .setRenderAs(
161                            RenderMode.View.addModifiers(event.renderAs()))
162                        .setSupportedModes(MODES));
163            sendAllEntries(channel, conletId);
164            renderedAs.add(RenderMode.View);
165        }
166        return renderedAs;
167    }
168
169    private void sendAllEntries(ConsoleConnection channel, String conletId) {
170        final LogReaderService logReader = logReaderResolved;
171        if (logReader == null) {
172            return;
173        }
174        channel.respond(new NotifyConletView(type(),
175            conletId, "entries",
176            (Object) Collections.list(logReader.getLog()).stream()
177                .map(entry -> logEntryAsMap(entry)).toArray()));
178    }
179
180    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
181    private void addEntry(LogEntry entry) {
182        for (ConsoleConnection connection : trackedConnections()) {
183            for (String conletId : conletIds(connection)) {
184                connection.respond(new NotifyConletView(type(),
185                    conletId, "addEntry", logEntryAsMap(entry))
186                        .disableTracking());
187            }
188        }
189    }
190
191    private Map<String, Object> logEntryAsMap(LogEntry entry) {
192        @SuppressWarnings("PMD.UseConcurrentHashMap")
193        Map<String, Object> result = new HashMap<>();
194        result.put("bundle",
195            Optional
196                .ofNullable(entry.getBundle().getHeaders().get("Bundle-Name"))
197                .orElse(entry.getBundle().getSymbolicName()));
198        result.put("exception", Optional.ofNullable(entry.getException())
199            .map(exc -> exc.getMessage()).orElse(""));
200        result.put("stacktrace", Optional.ofNullable(entry.getException())
201            .map(exc -> {
202                ByteArrayOutputStream out = new ByteArrayOutputStream();
203                PrintWriter printWriter = new PrintWriter(out);
204                exc.printStackTrace(printWriter);
205                printWriter.close();
206                return out.toString();
207            }).orElse(""));
208        result.put("location", Optional.ofNullable(entry.getLocation())
209            .map(loc -> loc.toString()).orElse(""));
210        result.put("loggerName", entry.getLoggerName());
211        result.put("logLevel", entry.getLogLevel().toString());
212        result.put("message", entry.getMessage());
213        result.put("sequence", entry.getSequence());
214        result.put("service", Optional.ofNullable(entry.getServiceReference())
215            .map(Object::toString).orElse(""));
216        result.put("threadInfo", entry.getThreadInfo());
217        result.put("time", entry.getTime());
218        return result;
219    }
220
221    /*
222     * (non-Javadoc)
223     * 
224     * @see org.jgrapes.console.AbstractConlet#doNotifyConletModel
225     */
226    @Override
227    @SuppressWarnings({ "PMD.SwitchStmtsShouldHaveDefault",
228        "PMD.TooFewBranchesForASwitchStatement" })
229    protected void doUpdateConletState(NotifyConletModel event,
230            ConsoleConnection channel, Serializable conletState)
231            throws Exception {
232        event.stop();
233        switch (event.method()) {
234        case "resync":
235            sendAllEntries(channel, event.conletId());
236            break;
237        }
238    }
239
240    @Override
241    protected boolean doSetLocale(SetLocale event, ConsoleConnection channel,
242            String conletId) throws Exception {
243        return true;
244    }
245}