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.webconlet.logviewer;
020
021import freemarker.core.ParseException;
022import freemarker.template.MalformedTemplateNameException;
023import freemarker.template.Template;
024import freemarker.template.TemplateNotFoundException;
025import java.io.ByteArrayOutputStream;
026import java.io.IOException;
027import java.io.PrintWriter;
028import java.io.Serializable;
029import java.util.HashMap;
030import java.util.HashSet;
031import java.util.Map;
032import java.util.Optional;
033import java.util.Set;
034import java.util.logging.LogRecord;
035import org.jgrapes.core.Channel;
036import org.jgrapes.core.Event;
037import org.jgrapes.core.Manager;
038import org.jgrapes.core.annotation.Handler;
039import org.jgrapes.webconsole.base.Conlet.RenderMode;
040import org.jgrapes.webconsole.base.ConsoleConnection;
041import org.jgrapes.webconsole.base.WebConsoleUtils;
042import org.jgrapes.webconsole.base.events.AddConletType;
043import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource;
044import org.jgrapes.webconsole.base.events.ConsoleReady;
045import org.jgrapes.webconsole.base.events.NotifyConletModel;
046import org.jgrapes.webconsole.base.events.NotifyConletView;
047import org.jgrapes.webconsole.base.events.RenderConlet;
048import org.jgrapes.webconsole.base.events.RenderConletRequestBase;
049import org.jgrapes.webconsole.base.events.SetLocale;
050import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
051
052/**
053 * A conlet for displaying records from java.util.logging. The class
054 * {@link LogViewerHandler} has to be registered as a handler in the
055 * logging configuration (e.g. with 
056 * `-Djava.util.logging.config.file=logging.properties` and
057 * `logging.properties`:
058 * ```
059 * org.jgrapes.webconlet.logviewer.LogViewerHandler.level=CONFIG
060 * ```
061 * 
062 * The handler implements a ring buffer for the last 100 
063 * {@link LogRecord}s. When the conlet is displayed, it obtains
064 * the initially shown messages from the ring buffer. All subsequently
065 * published {@link LogRecord}s are forwarded to the conlet.
066 * In order to limit the memory required in the browser, the conlet
067 * also retains only the 100 most recent messages. 
068 */
069public class LogViewerConlet extends FreeMarkerConlet<Serializable> {
070
071    private static final Set<RenderMode> MODES
072        = RenderMode.asSet(RenderMode.View);
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    public LogViewerConlet(Channel componentChannel) {
082        super(componentChannel);
083    }
084
085    /**
086     * On {@link ConsoleReady}, fire the {@link AddConletType}.
087     *
088     * @param event the event
089     * @param channel the channel
090     * @throws TemplateNotFoundException the template not found exception
091     * @throws MalformedTemplateNameException the malformed template name
092     *             exception
093     * @throws ParseException the parse exception
094     * @throws IOException Signals that an I/O exception has occurred.
095     */
096    @Handler
097    public void onConsoleReady(ConsoleReady event, ConsoleConnection channel)
098            throws TemplateNotFoundException, MalformedTemplateNameException,
099            ParseException, IOException {
100        // Add conlet resources to page
101        channel.respond(new AddConletType(type())
102            .setDisplayNames(
103                localizations(channel.supportedLocales(), "conletName"))
104            .addRenderMode(RenderMode.View)
105            .addScript(new ScriptResource().setScriptType("module")
106                .setScriptUri(event.renderSupport().conletResource(
107                    type(), "LogViewer-functions.ftl.js")))
108            .addCss(event.renderSupport(),
109                WebConsoleUtils.uriFromPath("LogViewer-style.css")));
110    }
111
112    @Override
113    protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
114            ConsoleConnection channel, String conletId,
115            Serializable conletState)
116            throws Exception {
117        Set<RenderMode> renderedAs = new HashSet<>();
118        if (event.renderAs().contains(RenderMode.View)) {
119            Template tpl
120                = freemarkerConfig().getTemplate("LogViewer-view.ftl.html");
121            channel.respond(new RenderConlet(type(), conletId,
122                processTemplate(event, tpl,
123                    fmModel(event, channel, conletId, conletState)))
124                        .setRenderAs(
125                            RenderMode.View.addModifiers(event.renderAs()))
126                        .setSupportedModes(MODES));
127            sendAllEntries(channel, conletId);
128            renderedAs.add(RenderMode.View);
129        }
130        return renderedAs;
131    }
132
133    private void sendAllEntries(ConsoleConnection channel, String conletId) {
134        channel.respond(new NotifyConletView(type(),
135            conletId, "entries",
136            (Object) LogViewerHandler.setConlet(this).stream()
137                .map(entry -> logEntryAsMap(entry)).toArray()));
138    }
139
140    /* default */ void addEntry(LogRecord entry) {
141        for (ConsoleConnection connection : trackedConnections()) {
142            for (String conletId : conletIds(connection)) {
143                connection.respond(new NotifyConletView(type(),
144                    conletId, "addEntry", logEntryAsMap(entry))
145                        .disableTracking());
146            }
147        }
148    }
149
150    private Map<String, Object> logEntryAsMap(LogRecord record) {
151        @SuppressWarnings("PMD.UseConcurrentHashMap")
152        Map<String, Object> result = new HashMap<>();
153        result.put("exception", Optional.ofNullable(record.getThrown())
154            .map(exc -> exc.getMessage()).orElse(""));
155        result.put("stacktrace", Optional.ofNullable(record.getThrown())
156            .map(exc -> {
157                ByteArrayOutputStream out = new ByteArrayOutputStream();
158                PrintWriter printWriter = new PrintWriter(out);
159                exc.printStackTrace(printWriter);
160                printWriter.close();
161                return out.toString();
162            }).orElse(""));
163        result.put("loggerName", record.getLoggerName());
164        result.put("source",
165            record.getSourceClassName() + "::" + record.getSourceMethodName());
166        result.put("logLevel", record.getLevel().toString());
167        result.put("message", record.getMessage());
168        result.put("time", record.getInstant().toEpochMilli());
169        result.put("sequence", record.getSequenceNumber());
170        return result;
171    }
172
173    /*
174     * (non-Javadoc)
175     * 
176     * @see org.jgrapes.console.AbstractConlet#doNotifyConletModel
177     */
178    @Override
179    @SuppressWarnings({ "PMD.SwitchStmtsShouldHaveDefault",
180        "PMD.TooFewBranchesForASwitchStatement" })
181    protected void doUpdateConletState(NotifyConletModel event,
182            ConsoleConnection channel, Serializable conletState)
183            throws Exception {
184        event.stop();
185        switch (event.method()) {
186        case "resync":
187            sendAllEntries(channel, event.conletId());
188            break;
189        }
190    }
191
192    @Override
193    protected boolean doSetLocale(SetLocale event, ConsoleConnection channel,
194            String conletId) throws Exception {
195        return true;
196    }
197}