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.bundles; 020 021import freemarker.core.ParseException; 022import freemarker.template.MalformedTemplateNameException; 023import freemarker.template.Template; 024import freemarker.template.TemplateNotFoundException; 025import java.io.IOException; 026import java.time.Instant; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Dictionary; 030import java.util.Enumeration; 031import java.util.HashMap; 032import java.util.HashSet; 033import java.util.List; 034import java.util.Locale; 035import java.util.Map; 036import java.util.Optional; 037import java.util.Set; 038import java.util.TreeMap; 039import java.util.logging.Level; 040import java.util.logging.Logger; 041import java.util.stream.Collectors; 042import org.jgrapes.core.Channel; 043import org.jgrapes.core.Event; 044import org.jgrapes.core.Manager; 045import org.jgrapes.core.annotation.Handler; 046import org.jgrapes.webconsole.base.Conlet.RenderMode; 047import org.jgrapes.webconsole.base.ConletBaseModel; 048import org.jgrapes.webconsole.base.ConsoleConnection; 049import org.jgrapes.webconsole.base.WebConsoleUtils; 050import org.jgrapes.webconsole.base.events.AddConletRequest; 051import org.jgrapes.webconsole.base.events.AddConletType; 052import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource; 053import org.jgrapes.webconsole.base.events.ConsoleReady; 054import org.jgrapes.webconsole.base.events.NotifyConletModel; 055import org.jgrapes.webconsole.base.events.NotifyConletView; 056import org.jgrapes.webconsole.base.events.RenderConlet; 057import org.jgrapes.webconsole.base.events.RenderConletRequestBase; 058import org.jgrapes.webconsole.base.events.SetLocale; 059import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet; 060import org.osgi.framework.Bundle; 061import org.osgi.framework.BundleContext; 062import org.osgi.framework.BundleEvent; 063import org.osgi.framework.BundleException; 064import org.osgi.framework.BundleListener; 065import org.osgi.framework.startlevel.BundleStartLevel; 066import org.osgi.framework.wiring.BundleRevision; 067 068/** 069 * 070 */ 071public class BundleListConlet 072 extends FreeMarkerConlet<BundleListConlet.BundleListModel> 073 implements BundleListener { 074 075 private static final Logger LOG 076 = Logger.getLogger(BundleListConlet.class.getName()); 077 078 private static final Set<RenderMode> MODES = RenderMode.asSet( 079 RenderMode.Preview, RenderMode.View); 080 private final BundleContext context; 081 082 /** 083 * Creates a new component with its channel set to the given channel. 084 * 085 * @param componentChannel the channel that the component's handlers listen 086 * on by default and that {@link Manager#fire(Event, Channel...)} 087 * sends the event to 088 */ 089 @SuppressWarnings("PMD.UnusedFormalParameter") 090 public BundleListConlet(Channel componentChannel, BundleContext context, 091 Map<?, ?> properties) { 092 super(componentChannel); 093 this.context = context; 094 context.addBundleListener(this); 095 } 096 097 /** 098 * On {@link ConsoleReady}, fire the {@link AddConletType}. 099 * 100 * @param event the event 101 * @param channel the channel 102 * @throws TemplateNotFoundException the template not found exception 103 * @throws MalformedTemplateNameException the malformed template name 104 * exception 105 * @throws ParseException the parse exception 106 * @throws IOException Signals that an I/O exception has occurred. 107 */ 108 @Handler 109 public void onConsoleReady(ConsoleReady event, ConsoleConnection channel) 110 throws TemplateNotFoundException, MalformedTemplateNameException, 111 ParseException, IOException { 112 // Add conlet resources to page 113 channel.respond(new AddConletType(type()) 114 .addRenderMode(RenderMode.Preview).setDisplayNames( 115 localizations(channel.supportedLocales(), "conletName")) 116 .addScript(new ScriptResource() 117 .setScriptUri(event.renderSupport().conletResource( 118 type(), "Bundles-functions.ftl.js")) 119 .setScriptType("module")) 120 .addCss(event.renderSupport(), 121 WebConsoleUtils.uriFromPath("Bundles-style.css"))); 122 } 123 124 @Override 125 protected Optional<BundleListModel> createNewState(AddConletRequest event, 126 ConsoleConnection channel, String conletId) throws Exception { 127 BundleListModel conletModel = new BundleListModel(conletId); 128 return Optional.of(conletModel); 129 } 130 131 @Override 132 protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event, 133 ConsoleConnection channel, String conletId, 134 BundleListModel conletModel) throws Exception { 135 Set<RenderMode> renderedAs = new HashSet<>(); 136 if (event.renderAs().contains(RenderMode.Preview)) { 137 Template tpl 138 = freemarkerConfig().getTemplate("Bundles-preview.ftl.html"); 139 channel.respond(new RenderConlet(type(), conletId, 140 processTemplate(event, tpl, 141 fmModel(event, channel, conletId, conletModel))) 142 .setRenderAs( 143 RenderMode.Preview.addModifiers(event.renderAs())) 144 .setSupportedModes(MODES)); 145 List<Map<String, Object>> bundleInfos 146 = Arrays.stream(context.getBundles()) 147 .map(bndl -> createBundleInfo(bndl, channel.locale())) 148 .collect(Collectors.toList()); 149 channel.respond(new NotifyConletView(type(), 150 conletId, "bundleUpdates", bundleInfos, "preview", true)); 151 renderedAs.add(RenderMode.Preview); 152 } 153 if (event.renderAs().contains(RenderMode.View)) { 154 Template tpl 155 = freemarkerConfig().getTemplate("Bundles-view.ftl.html"); 156 channel.respond(new RenderConlet(type(), conletId, 157 processTemplate(event, tpl, 158 fmModel(event, channel, conletId, conletModel))) 159 .setRenderAs( 160 RenderMode.View.addModifiers(event.renderAs()))); 161 List<Map<String, Object>> bundleInfos 162 = Arrays.stream(context.getBundles()) 163 .map(bndl -> createBundleInfo(bndl, channel.locale())) 164 .collect(Collectors.toList()); 165 channel.respond(new NotifyConletView(type(), 166 conletId, "bundleUpdates", bundleInfos, "view", true)); 167 renderedAs.add(RenderMode.View); 168 } 169 return renderedAs; 170 } 171 172 private Map<String, Object> createBundleInfo(Bundle bundle, Locale locale) { 173 @SuppressWarnings("PMD.UseConcurrentHashMap") 174 Map<String, Object> result = new HashMap<>(); 175 result.put("id", bundle.getBundleId()); 176 result.put("name", 177 Optional.ofNullable(bundle.getHeaders(locale.toString()) 178 .get("Bundle-Name")).orElse(bundle.getSymbolicName())); 179 result.put("symbolicName", bundle.getSymbolicName()); 180 result.put("version", bundle.getVersion().toString()); 181 result.put("category", 182 Optional.ofNullable(bundle.getHeaders(locale.toString()) 183 .get("Bundle-Category")).orElse("")); 184 result.put("state", "bundleState_" + bundle.getState()); 185 result.put("startable", false); 186 result.put("stoppable", false); 187 if ((bundle.getState() 188 & (Bundle.RESOLVED | Bundle.INSTALLED | Bundle.ACTIVE)) != 0) { 189 boolean isFragment = (bundle.adapt(BundleRevision.class).getTypes() 190 & BundleRevision.TYPE_FRAGMENT) != 0; 191 result.put("startable", !isFragment 192 && (bundle.getState() == Bundle.INSTALLED 193 || bundle.getState() == Bundle.RESOLVED)); 194 result.put("stoppable", 195 !isFragment && bundle.getState() == Bundle.ACTIVE); 196 } 197 result.put("uninstallable", (bundle.getState() 198 & (Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE)) != 0); 199 result.put("uninstalled", bundle.getState() == Bundle.UNINSTALLED); 200 return result; 201 } 202 203 @Override 204 protected void doUpdateConletState(NotifyConletModel event, 205 ConsoleConnection channel, BundleListModel conletState) 206 throws Exception { 207 event.stop(); 208 Bundle bundle = context.getBundle((int) event.param(0)); 209 if (bundle == null) { 210 return; 211 } 212 try { 213 switch (event.method()) { 214 case "stop": 215 bundle.stop(); 216 break; 217 case "start": 218 bundle.start(); 219 break; 220 case "refresh": 221 break; 222 case "update": 223 bundle.update(); 224 break; 225 case "uninstall": 226 bundle.uninstall(); 227 break; 228 case "sendDetails": 229 sendBundleDetails(event.conletId(), channel, bundle); 230 break; 231 default:// ignore 232 break; 233 } 234 } catch (BundleException e) { 235 // ignore 236 LOG.log(Level.WARNING, "Cannot update bundle state", e); 237 } 238 } 239 240 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 241 private void sendBundleDetails(String conletId, ConsoleConnection channel, 242 Bundle bundle) { 243 Locale locale = channel.locale(); 244 List<Object> data = new ArrayList<>(); 245 data.add( 246 new Object[] { "bundleSymbolicName", bundle.getSymbolicName() }); 247 data.add( 248 new Object[] { "bundleVersion", bundle.getVersion().toString() }); 249 data.add(new Object[] { "bundleLocation", 250 bundle.getLocation().replace(".", ".​") }); 251 data.add(new Object[] { "bundleLastModification", 252 Instant.ofEpochMilli(bundle.getLastModified()).toString(), 253 "dateTime" }); 254 data.add(new Object[] { "bundleStartLevel", 255 bundle.adapt(BundleStartLevel.class).getStartLevel() }); 256 Dictionary<String, String> dict = bundle.getHeaders(locale.toString()); 257 @SuppressWarnings("PMD.UseConcurrentHashMap") 258 Map<String, String> headers = new TreeMap<>(); 259 for (Enumeration<String> e = dict.keys(); e.hasMoreElements();) { 260 String key = e.nextElement(); 261 headers.put(key, dict.get(key)); 262 } 263 List<Object> headerList = new ArrayList<>(); 264 for (Map.Entry<String, String> e : headers.entrySet()) { 265 headerList.add(new Object[] { e.getKey(), 266 e.getKey().contains("Package") 267 ? e.getValue().replace(".", ".​") 268 : e.getValue() }); 269 } 270 data.add(new Object[] { "manifestHeaders", headerList, "table" }); 271 channel.respond(new NotifyConletView(type(), 272 conletId, "bundleDetails", bundle.getBundleId(), data)); 273 } 274 275 /** 276 * Translates the OSGi {@link BundleEvent} to a JGrapes event and fires it 277 * on all known console session channels. 278 * 279 * @param event the event 280 */ 281 @Override 282 public void bundleChanged(BundleEvent event) { 283 fire(new BundleChanged(event), trackedConnections()); 284 } 285 286 /** 287 * Handles a {@link BundleChanged} event by updating the information in the 288 * console sessions. 289 * 290 * @param event the event 291 */ 292 @Handler 293 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 294 public void onBundleChanged(BundleChanged event, 295 ConsoleConnection channel) { 296 for (String conletId : conletIds(channel)) { 297 channel.respond(new NotifyConletView(type(), conletId, 298 "bundleUpdates", 299 (Object) new Object[] { createBundleInfo( 300 event.bundleEvent().getBundle(), 301 channel.locale()) }, 302 "*", false)); 303 } 304 } 305 306 @Override 307 protected boolean doSetLocale(SetLocale event, ConsoleConnection channel, 308 String conletId) throws Exception { 309 return true; 310 } 311 312 /** 313 * Wraps an OSGi {@link BundleEvent}. 314 */ 315 public static class BundleChanged extends Event<Void> { 316 private final BundleEvent bundleEvent; 317 318 /** 319 * Instantiates a new event. 320 * 321 * @param bundleEvent the OSGi bundle event 322 */ 323 public BundleChanged(BundleEvent bundleEvent) { 324 this.bundleEvent = bundleEvent; 325 } 326 327 /** 328 * Return the OSGi bundle event. 329 * 330 * @return the bundle event 331 */ 332 public BundleEvent bundleEvent() { 333 return bundleEvent; 334 } 335 336 } 337 338 /** 339 * The bundle's model. 340 */ 341 public class BundleListModel extends ConletBaseModel { 342 343 /** 344 * Instantiates a new bundle list model. 345 * 346 * @param conletId the web console component id 347 */ 348 public BundleListModel(String conletId) { 349 super(conletId); 350 } 351 352 /** 353 * Return the bundles. 354 * 355 * @return the bundle[] 356 */ 357 public Bundle[] bundles() { 358 return context.getBundles(); 359 } 360 361 } 362}