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}