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 .setFrontendRequest(), 123 channel); 124 break; 125 } 126 case "conletsDeleted": { 127 for (var item : params.asArray(0).backing()) { 128 var conletInfo = (JsonArray) item; 129 fire( 130 new ConletDeleted(view.renderSupport(), 131 conletInfo.asString(0), 132 conletInfo.asArray(1).stream().map( 133 value -> RenderMode.valueOf((String) value)) 134 .collect(Collectors.toSet()), 135 conletInfo.size() < 3 ? Collections.emptyMap() 136 : ((JsonObject) conletInfo.get(2)).backing()), 137 channel); 138 } 139 break; 140 } 141 case "consoleLayout": { 142 List<String> previewLayout = params.asArray(0).stream().map( 143 value -> (String) value).collect(Collectors.toList()); 144 List<String> tabsLayout = params.asArray(1).stream().map( 145 value -> (String) value).collect(Collectors.toList()); 146 JsonObject xtraInfo = (JsonObject) params.get(2); 147 fire(new ConsoleLayoutChanged( 148 previewLayout, tabsLayout, xtraInfo), channel); 149 break; 150 } 151 case "renderConlet": { 152 fire(new RenderConletRequest(view.renderSupport(), 153 params.asString(0), 154 params.asArray(1).stream().map( 155 value -> RenderMode.valueOf((String) value)) 156 .collect(Collectors.toSet())), 157 channel); 158 break; 159 } 160 case "setLocale": { 161 fire(new SetLocale(view.renderSupport(), 162 Locale.forLanguageTag(params.asString(0)), 163 params.asBoolean(1)), channel); 164 break; 165 } 166 case "notifyConletModel": { 167 fire(new NotifyConletModel(view.renderSupport(), 168 params.asString(0), params.asString(1), 169 params.size() <= 2 170 ? JsonArray.EMPTY_ARRAY 171 : params.asArray(2)), 172 channel); 173 break; 174 } 175 default: 176 // Ignore unknown 177 break; 178 } 179 } 180 181 /** 182 * Handle network configured condition. 183 * 184 * @param event the event 185 * @param channel the channel 186 * @throws InterruptedException the interrupted exception 187 * @throws IOException Signals that an I/O exception has occurred. 188 */ 189 @Handler 190 public void onConsoleConfigured( 191 ConsoleConfigured event, ConsoleConnection channel) 192 throws InterruptedException, IOException { 193 channel.respond(new SimpleConsoleCommand("consoleConfigured")); 194 } 195 196 /** 197 * Fallback handler that sends a {@link DeleteConlet} event 198 * if the {@link RenderConletRequest} event has not been handled 199 * successfully. 200 * 201 * @param event the event 202 * @param channel the channel 203 */ 204 @Handler(priority = -1_000_000) 205 public void onRenderConlet( 206 RenderConletRequest event, ConsoleConnection channel) { 207 if (!event.hasBeenRendered()) { 208 channel.respond( 209 new DeleteConlet(event.conletId(), Collections.emptySet())); 210 } 211 } 212 213 /** 214 * Discard all console connections on stop. 215 * 216 * @param event the event 217 */ 218 @Handler 219 public void onStop(Stop event) { 220 for (ConsoleConnection ps : ConsoleConnection.byConsole(this)) { 221 ps.close(); 222 } 223 } 224 225 /** 226 * The MBeans view of a console. 227 */ 228 @SuppressWarnings({ "PMD.CommentRequired", "PMD.AvoidDuplicateLiterals" }) 229 public interface ConsoleMXBean { 230 231 @SuppressWarnings("PMD.CommentRequired") 232 class ConsoleConnectionInfo { 233 234 private final ConsoleConnection connection; 235 236 public ConsoleConnectionInfo(ConsoleConnection connection) { 237 super(); 238 this.connection = connection; 239 } 240 241 public String getChannel() { 242 return connection.upstreamChannel().toString(); 243 } 244 245 public String getExpiresAt() { 246 return connection.expiresAt().atZone(ZoneId.systemDefault()) 247 .toString(); 248 } 249 } 250 251 String getComponentPath(); 252 253 String getPrefix(); 254 255 boolean isUseMinifiedResources(); 256 257 void setUseMinifiedResources(boolean useMinifiedResources); 258 259 SortedMap<String, ConsoleConnectionInfo> getConsoleConnections(); 260 } 261 262 @SuppressWarnings("PMD.CommentRequired") 263 public static class WebConsoleInfo implements ConsoleMXBean { 264 265 private static MBeanServer mbs 266 = ManagementFactory.getPlatformMBeanServer(); 267 268 private ObjectName mbeanName; 269 private final WeakReference<WebConsole> consoleRef; 270 271 @SuppressWarnings("PMD.GuardLogStatement") 272 public WebConsoleInfo(WebConsole console) { 273 try { 274 mbeanName = new ObjectName("org.jgrapes.webconsole:type=" 275 + WebConsole.class.getSimpleName() + ",name=" 276 + ObjectName.quote(Components.simpleObjectName(console) 277 + " (" + console.view.prefix().toString() + ")")); 278 } catch (MalformedObjectNameException e) { 279 // Should not happen 280 logger.log(Level.WARNING, e.getMessage(), e); 281 } 282 consoleRef = new WeakReference<>(console); 283 try { 284 mbs.unregisterMBean(mbeanName); 285 } catch (Exception e) { // NOPMD 286 // Just in case, should not work 287 } 288 try { 289 mbs.registerMBean(this, mbeanName); 290 } catch (InstanceAlreadyExistsException | MBeanRegistrationException 291 | NotCompliantMBeanException e) { 292 // Should not happen 293 logger.log(Level.WARNING, e.getMessage(), e); 294 } 295 } 296 297 public Optional<WebConsole> console() { 298 WebConsole console = consoleRef.get(); 299 if (console == null) { 300 try { 301 mbs.unregisterMBean(mbeanName); 302 } catch (Exception e) { // NOPMD 303 // Should work. 304 } 305 } 306 return Optional.ofNullable(console); 307 } 308 309 @Override 310 public String getComponentPath() { 311 return console().map(Component::componentPath) 312 .orElse("<removed>"); 313 } 314 315 @Override 316 public String getPrefix() { 317 return console().map( 318 console -> console.view.prefix().toString()) 319 .orElse("<unknown>"); 320 } 321 322 @Override 323 public boolean isUseMinifiedResources() { 324 return console().map( 325 console -> console.view.useMinifiedResources()) 326 .orElse(false); 327 } 328 329 @Override 330 public void setUseMinifiedResources(boolean useMinifiedResources) { 331 console().ifPresent(console -> console.view.setUseMinifiedResources( 332 useMinifiedResources)); 333 } 334 335 @Override 336 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 337 public SortedMap<String, ConsoleConnectionInfo> 338 getConsoleConnections() { 339 SortedMap<String, ConsoleConnectionInfo> result = new TreeMap<>(); 340 console().ifPresent(console -> { 341 for (ConsoleConnection ps : ConsoleConnection 342 .byConsole(console)) { 343 result.put(Components.simpleObjectName(ps), 344 new ConsoleConnectionInfo(ps)); 345 } 346 }); 347 return result; 348 } 349 } 350 351 /** 352 * An MBean interface for getting information about all consoles. 353 * 354 * There is currently no summary information. However, the (periodic) 355 * invocation of {@link WebConsoleSummaryMXBean#getConsoles()} ensures 356 * that entries for removed {@link WebConsole}s are unregistered. 357 */ 358 @SuppressWarnings("PMD.CommentRequired") 359 public interface WebConsoleSummaryMXBean { 360 361 Set<ConsoleMXBean> getConsoles(); 362 363 } 364 365 /** 366 * Provides an MBean view of the console. 367 */ 368 @SuppressWarnings("PMD.CommentRequired") 369 private static final class MBeanView implements WebConsoleSummaryMXBean { 370 371 private static Set<WebConsoleInfo> consoleInfos = new HashSet<>(); 372 373 public static void addConsole(WebConsole console) { 374 synchronized (consoleInfos) { 375 consoleInfos.add(new WebConsoleInfo(console)); 376 } 377 } 378 379 @Override 380 public Set<ConsoleMXBean> getConsoles() { 381 Set<WebConsoleInfo> expired = new HashSet<>(); 382 synchronized (consoleInfos) { 383 for (WebConsoleInfo consoleInfo : consoleInfos) { 384 if (!consoleInfo.console().isPresent()) { 385 expired.add(consoleInfo); 386 } 387 } 388 consoleInfos.removeAll(expired); 389 } 390 @SuppressWarnings("unchecked") 391 Set<ConsoleMXBean> result 392 = (Set<ConsoleMXBean>) (Object) consoleInfos; 393 return result; 394 } 395 } 396 397 static { 398 try { 399 MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); 400 ObjectName mxbeanName 401 = new ObjectName("org.jgrapes.webconsole:type=" 402 + WebConsole.class.getSimpleName() + "s"); 403 mbs.registerMBean(new MBeanView(), mxbeanName); 404 } catch (MalformedObjectNameException | InstanceAlreadyExistsException 405 | MBeanRegistrationException | NotCompliantMBeanException e) { 406 // Should not happen 407 logger.log(Level.WARNING, e.getMessage(), e); 408 } 409 } 410}