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