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