001/* 002 * JGrapes Event Driven Framework 003 * Copyright (C) 2017-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.webconsole.base; 020 021import java.io.IOException; 022import java.lang.management.ManagementFactory; 023import java.lang.ref.WeakReference; 024import java.time.ZoneId; 025import java.util.Collections; 026import java.util.HashSet; 027import java.util.List; 028import java.util.Locale; 029import java.util.Optional; 030import java.util.Set; 031import java.util.SortedMap; 032import java.util.TreeMap; 033import java.util.logging.Level; 034import java.util.logging.Logger; 035import java.util.stream.Collectors; 036import javax.management.InstanceAlreadyExistsException; 037import javax.management.MBeanRegistrationException; 038import javax.management.MBeanServer; 039import javax.management.MalformedObjectNameException; 040import javax.management.NotCompliantMBeanException; 041import javax.management.ObjectName; 042import org.jdrupes.json.JsonArray; 043import org.jdrupes.json.JsonObject; 044import org.jgrapes.core.Channel; 045import org.jgrapes.core.Component; 046import org.jgrapes.core.Components; 047import org.jgrapes.core.annotation.Handler; 048import org.jgrapes.core.events.Stop; 049import org.jgrapes.webconsole.base.Conlet.RenderMode; 050import org.jgrapes.webconsole.base.events.AddConletRequest; 051import org.jgrapes.webconsole.base.events.ConletDeleted; 052import org.jgrapes.webconsole.base.events.ConsoleConfigured; 053import org.jgrapes.webconsole.base.events.ConsoleLayoutChanged; 054import org.jgrapes.webconsole.base.events.ConsoleReady; 055import org.jgrapes.webconsole.base.events.DeleteConlet; 056import org.jgrapes.webconsole.base.events.JsonInput; 057import org.jgrapes.webconsole.base.events.NotifyConletModel; 058import org.jgrapes.webconsole.base.events.RenderConletRequest; 059import org.jgrapes.webconsole.base.events.SetLocale; 060import org.jgrapes.webconsole.base.events.SimpleConsoleCommand; 061 062/** 063 * Provides the web console component related part of the console. 064 */ 065@SuppressWarnings("PMD.GuardLogStatement") 066public class WebConsole extends Component { 067 068 @SuppressWarnings("PMD.FieldNamingConventions") 069 private static final Logger logger 070 = Logger.getLogger(WebConsole.class.getName()); 071 072 private ConsoleWeblet view; 073 074 /** 075 * @param componentChannel 076 */ 077 /* default */ WebConsole(Channel componentChannel) { 078 super(componentChannel); 079 } 080 081 /* default */ void setView(ConsoleWeblet view) { 082 this.view = view; 083 MBeanView.addConsole(this); 084 } 085 086 /** 087 * Provides access to the weblet's channel. 088 * 089 * @return the channel 090 */ 091 public Channel webletChannel() { 092 return view.channel(); 093 } 094 095 /** 096 * Handle JSON input. 097 * 098 * @param event the event 099 * @param channel the channel 100 * @throws InterruptedException the interrupted exception 101 * @throws IOException Signals that an I/O exception has occurred. 102 */ 103 @Handler 104 @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", 105 "PMD.AvoidInstantiatingObjectsInLoops", "PMD.NcssCount" }) 106 public void onJsonInput(JsonInput event, ConsoleConnection channel) 107 throws InterruptedException, IOException { 108 // Send events to web console components on console's channel 109 JsonArray params = event.request().params(); 110 switch (event.request().method()) { 111 case "consoleReady": { 112 fire(new ConsoleReady(view.renderSupport()), channel); 113 break; 114 } 115 case "addConlet": { 116 fire(new AddConletRequest(view.renderSupport(), 117 params.asString(0), params.asArray(1).stream().map( 118 value -> RenderMode.valueOf((String) value)) 119 .collect(Collectors.toSet()), 120 params.size() < 3 ? Collections.emptyMap() 121 : ((JsonObject) params.get(2)).backing()), 122 channel); 123 break; 124 } 125 case "conletsDeleted": { 126 for (var item : params.asArray(0).backing()) { 127 var conletInfo = (JsonArray) item; 128 fire( 129 new ConletDeleted(view.renderSupport(), 130 conletInfo.asString(0), 131 conletInfo.asArray(1).stream().map( 132 value -> RenderMode.valueOf((String) value)) 133 .collect(Collectors.toSet()), 134 conletInfo.size() < 3 ? Collections.emptyMap() 135 : ((JsonObject) conletInfo.get(2)).backing()), 136 channel); 137 } 138 break; 139 } 140 case "consoleLayout": { 141 List<String> previewLayout = params.asArray(0).stream().map( 142 value -> (String) value).collect(Collectors.toList()); 143 List<String> tabsLayout = params.asArray(1).stream().map( 144 value -> (String) value).collect(Collectors.toList()); 145 JsonObject xtraInfo = (JsonObject) params.get(2); 146 fire(new ConsoleLayoutChanged( 147 previewLayout, tabsLayout, xtraInfo), channel); 148 break; 149 } 150 case "renderConlet": { 151 fire(new RenderConletRequest(view.renderSupport(), 152 params.asString(0), 153 params.asArray(1).stream().map( 154 value -> RenderMode.valueOf((String) value)) 155 .collect(Collectors.toSet())), 156 channel); 157 break; 158 } 159 case "setLocale": { 160 fire(new SetLocale(view.renderSupport(), 161 Locale.forLanguageTag(params.asString(0)), 162 params.asBoolean(1)), channel); 163 break; 164 } 165 case "notifyConletModel": { 166 fire(new NotifyConletModel(view.renderSupport(), 167 params.asString(0), params.asString(1), 168 params.size() <= 2 169 ? JsonArray.EMPTY_ARRAY 170 : params.asArray(2)), 171 channel); 172 break; 173 } 174 default: 175 // Ignore unknown 176 break; 177 } 178 } 179 180 /** 181 * Handle network configured condition. 182 * 183 * @param event the event 184 * @param channel the channel 185 * @throws InterruptedException the interrupted exception 186 * @throws IOException Signals that an I/O exception has occurred. 187 */ 188 @Handler 189 public void onConsoleConfigured( 190 ConsoleConfigured event, ConsoleConnection channel) 191 throws InterruptedException, IOException { 192 channel.respond(new SimpleConsoleCommand("consoleConfigured")); 193 } 194 195 /** 196 * Fallback handler that sends a {@link DeleteConlet} event 197 * if the {@link RenderConletRequest} event has not been handled 198 * successfully. 199 * 200 * @param event the event 201 * @param channel the channel 202 */ 203 @Handler(priority = -1_000_000) 204 public void onRenderConlet( 205 RenderConletRequest event, ConsoleConnection channel) { 206 if (!event.hasBeenRendered()) { 207 channel.respond( 208 new DeleteConlet(event.conletId(), Collections.emptySet())); 209 } 210 } 211 212 /** 213 * Discard all console connections on stop. 214 * 215 * @param event the event 216 */ 217 @Handler 218 public void onStop(Stop event) { 219 for (ConsoleConnection ps : ConsoleConnection.byConsole(this)) { 220 ps.discard(); 221 } 222 } 223 224 /** 225 * The MBeans view of a console. 226 */ 227 @SuppressWarnings({ "PMD.CommentRequired", "PMD.AvoidDuplicateLiterals" }) 228 public interface ConsoleMXBean { 229 230 @SuppressWarnings("PMD.CommentRequired") 231 class ConsoleSessionInfo { 232 233 private final ConsoleConnection session; 234 235 public ConsoleSessionInfo(ConsoleConnection session) { 236 super(); 237 this.session = session; 238 } 239 240 public String getChannel() { 241 return session.upstreamChannel().toString(); 242 } 243 244 public String getExpiresAt() { 245 return session.expiresAt().atZone(ZoneId.systemDefault()) 246 .toString(); 247 } 248 } 249 250 String getComponentPath(); 251 252 String getPrefix(); 253 254 boolean isUseMinifiedResources(); 255 256 void setUseMinifiedResources(boolean useMinifiedResources); 257 258 SortedMap<String, ConsoleSessionInfo> getConsoleSessions(); 259 } 260 261 @SuppressWarnings("PMD.CommentRequired") 262 public static class WebConsoleInfo implements ConsoleMXBean { 263 264 private static MBeanServer mbs 265 = ManagementFactory.getPlatformMBeanServer(); 266 267 private ObjectName mbeanName; 268 private final WeakReference<WebConsole> consoleRef; 269 270 @SuppressWarnings("PMD.GuardLogStatement") 271 public WebConsoleInfo(WebConsole console) { 272 try { 273 mbeanName = new ObjectName("org.jgrapes.webconsole:type=" 274 + WebConsole.class.getSimpleName() + ",name=" 275 + ObjectName.quote(Components.simpleObjectName(console) 276 + " (" + console.view.prefix().toString() + ")")); 277 } catch (MalformedObjectNameException e) { 278 // Should not happen 279 logger.log(Level.WARNING, e.getMessage(), e); 280 } 281 consoleRef = new WeakReference<>(console); 282 try { 283 mbs.unregisterMBean(mbeanName); 284 } catch (Exception e) { // NOPMD 285 // Just in case, should not work 286 } 287 try { 288 mbs.registerMBean(this, mbeanName); 289 } catch (InstanceAlreadyExistsException | MBeanRegistrationException 290 | NotCompliantMBeanException e) { 291 // Should not happen 292 logger.log(Level.WARNING, e.getMessage(), e); 293 } 294 } 295 296 public Optional<WebConsole> console() { 297 WebConsole console = consoleRef.get(); 298 if (console == null) { 299 try { 300 mbs.unregisterMBean(mbeanName); 301 } catch (Exception e) { // NOPMD 302 // Should work. 303 } 304 } 305 return Optional.ofNullable(console); 306 } 307 308 @Override 309 public String getComponentPath() { 310 return console().map(mgr -> mgr.componentPath()) 311 .orElse("<removed>"); 312 } 313 314 @Override 315 public String getPrefix() { 316 return console().map( 317 console -> console.view.prefix().toString()) 318 .orElse("<unknown>"); 319 } 320 321 @Override 322 public boolean isUseMinifiedResources() { 323 return console().map( 324 console -> console.view.useMinifiedResources()) 325 .orElse(false); 326 } 327 328 @Override 329 public void setUseMinifiedResources(boolean useMinifiedResources) { 330 console().ifPresent(console -> console.view.setUseMinifiedResources( 331 useMinifiedResources)); 332 } 333 334 @Override 335 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 336 public SortedMap<String, ConsoleSessionInfo> getConsoleSessions() { 337 SortedMap<String, ConsoleSessionInfo> result = new TreeMap<>(); 338 console().ifPresent(console -> { 339 for (ConsoleConnection ps : ConsoleConnection 340 .byConsole(console)) { 341 result.put(Components.simpleObjectName(ps), 342 new ConsoleSessionInfo(ps)); 343 } 344 }); 345 return result; 346 } 347 } 348 349 /** 350 * An MBean interface for getting information about all consoles. 351 * 352 * There is currently no summary information. However, the (periodic) 353 * invocation of {@link WebConsoleSummaryMXBean#getConsoles()} ensures 354 * that entries for removed {@link WebConsole}s are unregistered. 355 */ 356 @SuppressWarnings("PMD.CommentRequired") 357 public interface WebConsoleSummaryMXBean { 358 359 Set<ConsoleMXBean> getConsoles(); 360 361 } 362 363 /** 364 * Provides an MBean view of the console. 365 */ 366 @SuppressWarnings("PMD.CommentRequired") 367 private static class MBeanView implements WebConsoleSummaryMXBean { 368 369 private static Set<WebConsoleInfo> consoleInfos = new HashSet<>(); 370 371 public static void addConsole(WebConsole console) { 372 synchronized (consoleInfos) { 373 consoleInfos.add(new WebConsoleInfo(console)); 374 } 375 } 376 377 @Override 378 public Set<ConsoleMXBean> getConsoles() { 379 Set<WebConsoleInfo> expired = new HashSet<>(); 380 synchronized (consoleInfos) { 381 for (WebConsoleInfo consoleInfo : consoleInfos) { 382 if (!consoleInfo.console().isPresent()) { 383 expired.add(consoleInfo); 384 } 385 } 386 consoleInfos.removeAll(expired); 387 } 388 @SuppressWarnings("unchecked") 389 Set<ConsoleMXBean> result 390 = (Set<ConsoleMXBean>) (Object) consoleInfos; 391 return result; 392 } 393 } 394 395 static { 396 try { 397 MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); 398 ObjectName mxbeanName 399 = new ObjectName("org.jgrapes.webconsole:type=" 400 + WebConsole.class.getSimpleName() + "s"); 401 mbs.registerMBean(new MBeanView(), mxbeanName); 402 } catch (MalformedObjectNameException | InstanceAlreadyExistsException 403 | MBeanRegistrationException | NotCompliantMBeanException e) { 404 // Should not happen 405 logger.log(Level.WARNING, e.getMessage(), e); 406 } 407 } 408}