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