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