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.net.URI; 023import java.net.URISyntaxException; 024import java.nio.CharBuffer; 025import java.nio.charset.StandardCharsets; 026import java.text.ParseException; 027import java.time.Duration; 028import java.util.ArrayList; 029import java.util.HashMap; 030import java.util.List; 031import java.util.Locale; 032import java.util.Map; 033import java.util.Optional; 034import java.util.ResourceBundle; 035import java.util.UUID; 036import java.util.concurrent.ConcurrentHashMap; 037import java.util.function.Supplier; 038import org.jdrupes.httpcodec.protocols.http.HttpConstants.HttpStatus; 039import org.jdrupes.httpcodec.protocols.http.HttpField; 040import org.jdrupes.httpcodec.protocols.http.HttpResponse; 041import org.jdrupes.httpcodec.types.Converters; 042import org.jgrapes.core.Channel; 043import org.jgrapes.core.ClassChannel; 044import org.jgrapes.core.Component; 045import org.jgrapes.core.EventPipeline; 046import org.jgrapes.core.Manager; 047import org.jgrapes.core.annotation.Handler; 048import org.jgrapes.core.annotation.HandlerDefinition.ChannelReplacements; 049import org.jgrapes.http.LanguageSelector.Selection; 050import org.jgrapes.http.ResourcePattern; 051import org.jgrapes.http.ResponseCreationSupport; 052import org.jgrapes.http.Session; 053import org.jgrapes.http.annotation.RequestHandler; 054import org.jgrapes.http.events.DiscardSession; 055import org.jgrapes.http.events.ProtocolSwitchAccepted; 056import org.jgrapes.http.events.Request; 057import org.jgrapes.http.events.Request.In.Get; 058import org.jgrapes.http.events.Response; 059import org.jgrapes.http.events.Upgraded; 060import org.jgrapes.io.IOSubchannel; 061import org.jgrapes.io.events.Close; 062import org.jgrapes.io.events.Closed; 063import org.jgrapes.io.events.Input; 064import org.jgrapes.io.events.Output; 065import org.jgrapes.io.util.CharBufferWriter; 066import org.jgrapes.io.util.LinkedIOSubchannel; 067import org.jgrapes.webconsole.base.events.ConletResourceRequest; 068import org.jgrapes.webconsole.base.events.ConsoleCommand; 069import org.jgrapes.webconsole.base.events.JsonInput; 070import org.jgrapes.webconsole.base.events.PageResourceRequest; 071import org.jgrapes.webconsole.base.events.ResourceRequestCompleted; 072import org.jgrapes.webconsole.base.events.SetLocale; 073import org.jgrapes.webconsole.base.events.SetLocaleCompleted; 074import org.jgrapes.webconsole.base.events.SimpleConsoleCommand; 075 076/** 077 * The server side base class for the web console single page 078 * application (SPA). Its main tasks are to provide resources using 079 * {@link Request}/{@link Response} events (see {@link #onGet} 080 * for details about the different kinds of resources), to create 081 * the {@link ConsoleConnection}s for new WebSocket connections 082 * (see {@link #onUpgraded}) and to convert the JSON RPC messages 083 * received from the browser via the web socket to {@link JsonInput} 084 * events and fire them on the corresponding {@link ConsoleConnection} 085 * channel. 086 * 087 * The class has a counter part in the browser, the `jgconsole` 088 * JavaScript module (see 089 * <a href="jsdoc/classes/Console.html">functions</a>) 090 * that can be loaded as `console-base-resource/jgconsole.js` 091 * (relative to the configured prefix). 092 * 093 * The class also provides handlers for some console related events 094 * (i.e. fired on the attached {@link WebConsole}'s channel) that 095 * affect the console representation in the browser. These handlers 096 * are declared with class channel {@link ConsoleConnection} which 097 * is replaced using the {@link ChannelReplacements} mechanism. 098 */ 099@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.TooManyMethods", 100 "PMD.GodClass", "PMD.CouplingBetweenObjects" }) 101public abstract class ConsoleWeblet extends Component { 102 103 private static final String CONSOLE_SESSION_IDS 104 = ConsoleWeblet.class.getName() + ".consoleConnectionId"; 105 private static final String UTF_8 = "utf-8"; 106 107 private URI prefix; 108 private final WebConsole console; 109 private ResourcePattern requestPattern; 110 111 private final RenderSupport renderSupport = new RenderSupportImpl(); 112 private boolean useMinifiedResources = true; 113 private long csNetworkTimeout = 45_000; 114 private long csRefreshInterval = 30_000; 115 private long csInactivityTimeout = -1; 116 117 private List<Class<?>> consoleResourceSearchSeq; 118 private final List<Class<?>> resourceClasses = new ArrayList<>(); 119 private final ResourceBundle.Control resourceControl 120 = new ConsoleResourceBundleControl(resourceClasses); 121 @SuppressWarnings("PMD.UseConcurrentHashMap") 122 private final Map<Locale, ResourceBundle> supportedLocales 123 = new HashMap<>(); 124 125 /** 126 * The class used in handler annotations to represent the 127 * console channel. 128 */ 129 protected class ConsoleChannel extends ClassChannel { 130 } 131 132 /** 133 * Instantiates a new console weblet. The weblet handles 134 * {@link Get} events for URIs that start with the 135 * specified prefix (see 136 * {@link #onGet(org.jgrapes.http.events.Request.In.Get, IOSubchannel)}). 137 * 138 * @param webletChannel the weblet channel 139 * @param consoleChannel the console channel 140 * @param consolePrefix the console prefix 141 */ 142 public ConsoleWeblet(Channel webletChannel, Channel consoleChannel, 143 URI consolePrefix) { 144 this(webletChannel, new WebConsole(consoleChannel)); 145 146 prefix = URI.create(consolePrefix.getPath().endsWith("/") 147 ? consolePrefix.getPath() 148 : consolePrefix.getPath() + "/"); 149 console.setView(this); 150 151 String consolePath = prefix.getPath(); 152 if (consolePath.endsWith("/")) { 153 consolePath = consolePath.substring(0, consolePath.length() - 1); 154 } 155 consolePath = consolePath + "|**"; 156 try { 157 requestPattern = new ResourcePattern(consolePath); 158 } catch (ParseException e) { 159 throw new IllegalArgumentException(e); 160 } 161 consoleResourceSearchSeq = consoleHierarchy(); 162 163 resourceClasses.addAll(consoleHierarchy()); 164 updateSupportedLocales(); 165 166 RequestHandler.Evaluator.add(this, "onGet", prefix + "**"); 167 RequestHandler.Evaluator.add(this, "onGetRedirect", 168 prefix.getPath().substring( 169 0, prefix.getPath().length() - 1)); 170 } 171 172 @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") 173 private ConsoleWeblet(Channel webletChannel, WebConsole console) { 174 super(webletChannel, ChannelReplacements.create() 175 .add(ConsoleChannel.class, console.channel())); 176 this.console = console; 177 attach(console); 178 } 179 180 /** 181 * Return the list of classes that form the current console 182 * weblet implementation. This consists of all classes from 183 * `getClass()` up to `ConsoleWeblet.class`. 184 * 185 * @return the list 186 */ 187 protected final List<Class<?>> consoleHierarchy() { 188 List<Class<?>> result = new ArrayList<>(); 189 Class<?> derivative = getClass(); 190 while (true) { 191 result.add(derivative); 192 if (derivative.equals(ConsoleWeblet.class)) { 193 break; 194 } 195 derivative = derivative.getSuperclass(); 196 } 197 return result; 198 } 199 200 /** 201 * Returns the name of the styling library or toolkit used by the console. 202 * This value is informative. It may, however, be used by 203 * a {@link PageResourceProviderFactory} to influence the creation 204 * of {@link PageResourceProvider}s. 205 * 206 * @return the value 207 */ 208 public abstract String styling(); 209 210 /** 211 * @return the prefix 212 */ 213 public URI prefix() { 214 return prefix; 215 } 216 217 /** 218 * Returns the automatically generated {@link WebConsole} component. 219 * 220 * @return the console 221 */ 222 public WebConsole console() { 223 return console; 224 } 225 226 /** 227 * Sets the console connection network timeout. The console connection 228 * will be removed if no messages have been received from the 229 * console page for the given duration. The value defaults to 45 seconds. 230 * 231 * @param timeout the timeout in milli seconds 232 * @return the console view for easy chaining 233 */ 234 public ConsoleWeblet setConnectionNetworkTimeout(Duration timeout) { 235 csNetworkTimeout = timeout.toMillis(); 236 return this; 237 } 238 239 /** 240 * Returns the console connection network timeout. 241 * 242 * @return the timeout 243 */ 244 public Duration connectionNetworkTimeout() { 245 return Duration.ofMillis(csNetworkTimeout); 246 } 247 248 /** 249 * Sets the console connection refresh interval. The console code in the 250 * browser will send a keep alive packet if there has been no user 251 * activity for more than the given period. The value 252 * defaults to 30 seconds. 253 * 254 * @param interval the interval 255 * @return the console view for easy chaining 256 */ 257 public ConsoleWeblet setConnectionRefreshInterval(Duration interval) { 258 csRefreshInterval = interval.toMillis(); 259 return this; 260 } 261 262 /** 263 * Returns the console connection refresh interval. 264 * 265 * @return the interval 266 */ 267 public Duration connectionRefreshInterval() { 268 return Duration.ofMillis(csRefreshInterval); 269 } 270 271 /** 272 * Sets the console connection inactivity timeout. If there has been no 273 * user activity for more than the given duration the 274 * console code stops sending keep alive packets and displays a 275 * message to the user. The value defaults to -1 (no timeout). 276 * 277 * @param timeout the timeout 278 * @return the console view for easy chaining 279 */ 280 public ConsoleWeblet setConnectionInactivityTimeout(Duration timeout) { 281 csInactivityTimeout = timeout.toMillis(); 282 return this; 283 } 284 285 /** 286 * Returns the console connection inactivity timeout. 287 * 288 * @return the timeout 289 */ 290 public Duration connectionInactivityTimeout() { 291 return Duration.ofMillis(csInactivityTimeout); 292 } 293 294 /** 295 * Returns whether resources are minified. 296 * 297 * @return the useMinifiedResources 298 */ 299 public boolean useMinifiedResources() { 300 return useMinifiedResources; 301 } 302 303 /** 304 * Determines if resources should be minified. 305 * 306 * @param useMinifiedResources the useMinifiedResources to set 307 */ 308 public void setUseMinifiedResources(boolean useMinifiedResources) { 309 this.useMinifiedResources = useMinifiedResources; 310 } 311 312 /** 313 * Provides the render support. 314 * 315 * @return the render support 316 */ 317 protected RenderSupport renderSupport() { 318 return renderSupport; 319 } 320 321 /** 322 * Prepends a class to the list of classes used to lookup console 323 * resources. See {@link ConsoleResourceBundleControl#newBundle}. 324 * Affects the content of the resource bundle returned by 325 * {@link #consoleResourceBundle(Locale)}. 326 * 327 * @param cls the class to prepend. 328 * @return the console weblet for easy chaining 329 */ 330 public ConsoleWeblet prependResourceBundleProvider(Class<?> cls) { 331 resourceClasses.add(0, cls); 332 updateSupportedLocales(); 333 return this; 334 } 335 336 /** 337 * Update the supported locales. 338 */ 339 @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") 340 protected final void updateSupportedLocales() { 341 supportedLocales.clear(); 342 ResourceBundle.clearCache(ConsoleWeblet.class.getClassLoader()); 343 for (Locale locale : Locale.getAvailableLocales()) { 344 if ("".equals(locale.getLanguage())) { 345 continue; 346 } 347 ResourceBundle bundle = ResourceBundle.getBundle("l10n", locale, 348 ConsoleWeblet.class.getClassLoader(), resourceControl); 349 if (bundle.getLocale().equals(locale)) { 350 supportedLocales.put(locale, bundle); 351 } 352 } 353 } 354 355 /** 356 * Return the console resources for a given locale. 357 * 358 * @param locale the locale 359 * @return the resource bundle 360 */ 361 public ResourceBundle consoleResourceBundle(Locale locale) { 362 return ResourceBundle.getBundle("l10n", locale, 363 ConsoleWeblet.class.getClassLoader(), resourceControl); 364 } 365 366 /** 367 * Returns the supported locales and their resource bundles. 368 * 369 * @return the set of locales supported by the console and their 370 * resource bundles 371 */ 372 protected Map<Locale, ResourceBundle> supportedLocales() { 373 return supportedLocales; 374 } 375 376 /** 377 * Redirects `GET` requests without trailing slash. 378 * 379 * @param event the event 380 * @param channel the channel 381 * @throws InterruptedException the interrupted exception 382 * @throws IOException Signals that an I/O exception has occurred. 383 * @throws ParseException the parse exception 384 */ 385 @RequestHandler(dynamic = true) 386 public void onGetRedirect(Request.In.Get event, IOSubchannel channel) 387 throws InterruptedException, IOException, ParseException { 388 HttpResponse response = event.httpRequest().response().get(); 389 response.setStatus(HttpStatus.MOVED_PERMANENTLY) 390 .setContentType("text", "plain", UTF_8) 391 .setField(HttpField.LOCATION, prefix); 392 channel.respond(new Response(response)); 393 channel.respond(Output.from(prefix.toString() 394 .getBytes(StandardCharsets.UTF_8), true)); 395 event.setResult(true); 396 event.stop(); 397 } 398 399 /** 400 * Handle the `GET` requests for the various resources. The requests 401 * have to start with the prefix passed to the constructor. Further 402 * processing depends on the next path segment: 403 * 404 * * `.../console-base-resource`: Provide a resource associated 405 * with this class. The resources are: 406 * 407 * * `jgconsole.js`: The JavaScript module with helper classes 408 * * `console.css`: Some basic styles for conlets 409 * 410 * * `.../console-resource`: Invokes {@link #provideConsoleResource} 411 * with the remainder of the path. 412 * 413 * * `.../page-resource`: Invokes {@link #providePageResource} 414 * with the remainder of the path. 415 * 416 * * `.../conlet-resource`: Invokes {@link #provideConletResource} 417 * with the remainder of the path. 418 * 419 * * `.../console-connection`: Handled by this class. Used 420 * e.g. for initiating the web socket connection. 421 * 422 * @param event the event 423 * @param channel the channel 424 * @throws InterruptedException the interrupted exception 425 * @throws IOException Signals that an I/O exception has occurred. 426 * @throws ParseException the parse exception 427 */ 428 @RequestHandler(dynamic = true) 429 public void onGet(Request.In.Get event, IOSubchannel channel) 430 throws InterruptedException, IOException, ParseException { 431 // Already fulfilled? 432 if (event.fulfilled()) { 433 return; 434 } 435 436 // Request for console? (Only valid with session) 437 URI requestUri = event.requestUri(); 438 int prefixSegs = requestPattern.matches(requestUri); 439 if (prefixSegs < 0) { 440 return; 441 } 442 443 // Normalize and evaluate 444 String requestPath = ResourcePattern.removeSegments( 445 requestUri.getPath(), prefixSegs + 1); 446 String[] requestParts = ResourcePattern.split(requestPath, 1); 447 switch (requestParts[0]) { 448 case "": 449 // Because language is changed via websocket, locale cookie 450 // may be out-dated 451// event.associated(Selection.class) 452// .ifPresent(selection -> selection.prefer(selection.get()[0])); 453 // This is a console connection now (can be connected to) 454 Session session = Session.from(event); 455 UUID consoleConnectionId = UUID.randomUUID(); 456 @SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" }) 457 Map<URI, UUID> knownIds = (Map<URI, UUID>) session.computeIfAbsent( 458 CONSOLE_SESSION_IDS, 459 newKey -> new ConcurrentHashMap<>()); 460 knownIds.put(prefix, consoleConnectionId); 461 // Finally render 462 renderConsole(event, channel, consoleConnectionId); 463 return; 464 case "console-resource": 465 provideConsoleResource(event, ResourcePattern.removeSegments( 466 requestUri.getPath(), prefixSegs + 2), channel); 467 return; 468 case "console-base-resource": 469 ResponseCreationSupport.sendStaticContent(event, channel, 470 path -> ConsoleWeblet.class.getResource(requestParts[1]), 471 null); 472 return; 473 case "page-resource": 474 providePageResource(event, channel, requestParts[1]); 475 return; 476 case "console-connection": 477 handleSessionRequest(event, channel, requestParts[1]); 478 return; 479 case "conlet-resource": 480 provideConletResource(event, channel, URI.create(requestParts[1])); 481 return; 482 default: 483 break; 484 } 485 } 486 487 /** 488 * Render the console page. 489 * 490 * @param event the event 491 * @param channel the channel 492 * @throws IOException Signals that an I/O exception has occurred. 493 * @throws InterruptedException the interrupted exception 494 */ 495 protected abstract void renderConsole(Request.In.Get event, 496 IOSubchannel channel, UUID consoleConnectionId) 497 throws IOException, InterruptedException; 498 499 /** 500 * Provide a console resource. The implementation tries to load the 501 * resource using {@link Class#getResource(String)} for each class 502 * in the class hierarchy, starting with the finally derived class. 503 * 504 * @param event the event 505 * @param requestPath the request path relativized to the 506 * common part for console resources 507 * @param channel the channel 508 */ 509 protected void provideConsoleResource(Request.In.Get event, 510 String requestPath, IOSubchannel channel) { 511 for (Class<?> cls : consoleResourceSearchSeq) { 512 if (ResponseCreationSupport.sendStaticContent(event, channel, 513 path -> cls.getResource(requestPath), 514 null)) { 515 break; 516 } 517 } 518 } 519 520 /** 521 * Prepends the given class to the list of classes searched by 522 * {@link #provideConsoleResource(Request.In.Get, String, IOSubchannel)}. 523 * 524 * @param cls the class to prepend 525 * @return the console weblet for easy chaining 526 */ 527 public ConsoleWeblet prependConsoleResourceProvider(Class<?> cls) { 528 consoleResourceSearchSeq.add(0, cls); 529 return this; 530 } 531 532 private void providePageResource(Request.In.Get event, IOSubchannel channel, 533 String resource) throws InterruptedException { 534 // Send events to providers on console's channel 535 PageResourceRequest pageResourceRequest = new PageResourceRequest( 536 WebConsoleUtils.uriFromPath(resource), 537 event.httpRequest().findValue(HttpField.IF_MODIFIED_SINCE, 538 Converters.DATE_TIME).orElse(null), 539 event.httpRequest(), channel, Session.from(event), renderSupport()); 540 event.setResult(true); 541 event.stop(); 542 fire(pageResourceRequest, consoleChannel(channel)); 543 } 544 545 @SuppressWarnings("PMD.EmptyCatchBlock") 546 private void provideConletResource(Request.In.Get event, 547 IOSubchannel channel, 548 URI resource) throws InterruptedException { 549 try { 550 String resPath = resource.getPath(); 551 int sep = resPath.indexOf('/'); 552 // Send events to web console components on console's channel 553 ConletResourceRequest conletRequest 554 = new ConletResourceRequest( 555 resPath.substring(0, sep), 556 new URI(null, null, resPath.substring(sep + 1), 557 event.requestUri().getQuery(), 558 event.requestUri().getFragment()), 559 event.httpRequest().findValue(HttpField.IF_MODIFIED_SINCE, 560 Converters.DATE_TIME).orElse(null), 561 event.httpRequest(), channel, 562 Session.from(event), renderSupport()); 563 // Make session available (associate with event, this is not 564 // a websocket request). 565 event.associated(Session.class, Supplier.class).ifPresent( 566 supplier -> conletRequest.setAssociated(Session.class, 567 supplier)); 568 event.setResult(true); 569 event.stop(); 570 fire(conletRequest, consoleChannel(channel)); 571 } catch (URISyntaxException e) { 572 // Won't happen, new URI derived from existing 573 } 574 } 575 576 /** 577 * The console channel for getting resources. Resource providers 578 * respond on the same event pipeline as they receive, because 579 * handling is just a mapping to {@link ResourceRequestCompleted}. 580 * 581 * @param channel the channel 582 * @return the IO subchannel 583 */ 584 private IOSubchannel consoleChannel(IOSubchannel channel) { 585 @SuppressWarnings("unchecked") 586 Optional<LinkedIOSubchannel> consoleChannel 587 = (Optional<LinkedIOSubchannel>) LinkedIOSubchannel 588 .downstreamChannel(console, channel); 589 return consoleChannel.orElseGet( 590 () -> new ConsoleResourceChannel( 591 console, channel, activeEventPipeline())); 592 } 593 594 /** 595 * Handles the {@link ResourceRequestCompleted} event. 596 * 597 * @param event the event 598 * @param channel the channel 599 * @throws IOException Signals that an I/O exception has occurred. 600 * @throws InterruptedException the interrupted exception 601 */ 602 @Handler(channels = ConsoleChannel.class) 603 public void onResourceRequestCompleted( 604 ResourceRequestCompleted event, ConsoleResourceChannel channel) 605 throws IOException, InterruptedException { 606 event.stop(); 607 if (event.event().get() == null) { 608 ResponseCreationSupport.sendResponse(event.event().httpRequest(), 609 event.event().httpChannel(), HttpStatus.NOT_FOUND); 610 return; 611 } 612 event.event().get().process(); 613 } 614 615 private void handleSessionRequest(Request.In.Get event, 616 IOSubchannel channel, String consoleConnectionId) 617 throws InterruptedException, IOException, ParseException { 618 // Must be WebSocket request. 619 if (!event.httpRequest().findField( 620 HttpField.UPGRADE, Converters.STRING_LIST) 621 .map(fld -> fld.value().containsIgnoreCase("websocket")) 622 .orElse(false)) { 623 return; 624 } 625 626 // Can only connect to sessions that have been prepared 627 // by loading the console. (Prevents using a newly created 628 // browser session from being (re-)connected to after a 629 // long disconnect or restart and, of course, CSF). 630 final Session browserSession = Session.from(event); 631 @SuppressWarnings("unchecked") 632 Map<URI, UUID> knownIds = (Map<URI, UUID>) browserSession 633 .computeIfAbsent(CONSOLE_SESSION_IDS, 634 newKey -> new ConcurrentHashMap<>()); 635 if (!UUID.fromString(consoleConnectionId) // NOPMD, note negation 636 .equals(knownIds.get(prefix))) { 637 channel.setAssociated(this, new String[2]); 638 } else { 639 channel.setAssociated(this, new String[] { 640 consoleConnectionId, 641 Optional.ofNullable(event.httpRequest().queryData() 642 .get("was")).map(vals -> vals.get(0)).orElse(null) 643 }); 644 } 645 channel.respond(new ProtocolSwitchAccepted(event, "websocket")); 646 event.stop(); 647 } 648 649 /** 650 * Handles a change of Locale for the console. 651 * 652 * @param event the event 653 * @param channel the channel 654 * @throws InterruptedException the interrupted exception 655 * @throws IOException Signals that an I/O exception has occurred. 656 */ 657 @Handler(channels = ConsoleChannel.class, priority = 10_000) 658 public void onSetLocale(SetLocale event, ConsoleConnection channel) 659 throws InterruptedException, IOException { 660 channel.setLocale(event.locale()); 661 Optional.ofNullable(channel.session()).flatMap( 662 s -> Optional.ofNullable((Selection) s.get(Selection.class))) 663 .ifPresent(selection -> { 664 supportedLocales.keySet().stream() 665 .filter(lang -> lang.equals(event.locale())).findFirst() 666 .ifPresent(selection::prefer); 667 channel.respond(new SimpleConsoleCommand("setLocalesCookie", 668 Converters.SET_COOKIE_STRING 669 .get(selection.getCookieSameSite()) 670 .asFieldValue(selection.getCookie()))); 671 }); 672 if (event.reload()) { 673 channel.respond(new SimpleConsoleCommand("reload")); 674 } 675 } 676 677 /** 678 * Sends a reload if the change of locale could not be handled by 679 * all conlets. 680 * 681 * @param event the event 682 * @param channel the channel 683 */ 684 @Handler(channels = ConsoleChannel.class) 685 public void onSetLocaleCompleted(SetLocaleCompleted event, 686 ConsoleConnection channel) { 687 if (event.event().reload()) { 688 channel.respond(new SimpleConsoleCommand("reload")); 689 } 690 } 691 692 /** 693 * Called when the connection has been upgraded. 694 * 695 * @param event the event 696 * @param wsChannel the ws channel 697 * @throws IOException Signals that an I/O exception has occurred. 698 */ 699 @Handler 700 public void onUpgraded(Upgraded event, IOSubchannel wsChannel) 701 throws IOException { 702 Optional<String[]> passedIn 703 = wsChannel.associated(this, String[].class); 704 if (!passedIn.isPresent()) { 705 return; 706 } 707 708 // Check if reload required 709 String[] connectionIds = passedIn.get(); 710 if (connectionIds[0] == null) { 711 @SuppressWarnings({ "resource", "PMD.CloseResource" }) 712 CharBufferWriter out = new CharBufferWriter(wsChannel, 713 wsChannel.responsePipeline()).suppressClose(); 714 new SimpleConsoleCommand("reload").emitJson(out); 715 out.close(); 716 event.stop(); 717 return; 718 } 719 720 // Get session 721 @SuppressWarnings("unchecked") 722 final Supplier<Optional<Session>> sessionSupplier 723 = (Supplier<Optional<Session>>) wsChannel 724 .associated(Session.class, Supplier.class).get(); 725 // Reuse old console connection if still available 726 ConsoleConnection connection 727 = Optional.ofNullable(connectionIds[1]) 728 .flatMap(ConsoleConnection::lookup) 729 .map(conn -> conn.replaceId(connectionIds[0])) 730 .orElseGet( 731 () -> ConsoleConnection.lookupOrCreate(connectionIds[0], 732 console, supportedLocales.keySet(), csNetworkTimeout)) 733 .setUpstreamChannel(wsChannel) 734 .setSessionSupplier(sessionSupplier); 735 wsChannel.setAssociated(ConsoleConnection.class, connection); 736 // Channel now used as JSON input 737 wsChannel.setAssociated(this, new WebSocketInputSink( 738 event.processedBy().get(), connection)); 739 // From now on, only consoleConnection.respond may be used to send on 740 // the upstream channel. 741 connection.upstreamChannel().responsePipeline() 742 .restrictEventSource(connection.responsePipeline()); 743 } 744 745 /** 746 * Discard the session referenced in the event. 747 * 748 * @param event the event 749 */ 750 @Handler(channels = Channel.class) 751 public void onDiscardSession(DiscardSession event) { 752 final Session session = event.session(); 753 ConsoleConnection.byConsole(console).stream() 754 .filter(cs -> cs != null && cs.session().equals(session)) 755 .forEach(cs -> { 756 cs.responsePipeline().fire(new Close(), cs.upstreamChannel()); 757 }); 758 } 759 760 /** 761 * Handles network input (JSON data). 762 * 763 * @param event the event 764 * @param wsChannel the ws channel 765 * @throws IOException Signals that an I/O exception has occurred. 766 */ 767 @Handler 768 public void onInput(Input<CharBuffer> event, IOSubchannel wsChannel) 769 throws IOException { 770 Optional<WebSocketInputSink> optWsInputReader 771 = wsChannel.associated(this, WebSocketInputSink.class); 772 if (optWsInputReader.isPresent()) { 773 optWsInputReader.get().feed(event.buffer()); 774 } 775 } 776 777 /** 778 * Handles the closed event from the web socket. 779 * 780 * @param event the event 781 * @param wsChannel the WebSocket channel 782 * @throws IOException Signals that an I/O exception has occurred. 783 */ 784 @Handler 785 public void onClosed( 786 Closed<?> event, IOSubchannel wsChannel) throws IOException { 787 Optional<WebSocketInputSink> optWsInputReader 788 = wsChannel.associated(this, WebSocketInputSink.class); 789 if (optWsInputReader.isPresent()) { 790 wsChannel.setAssociated(this, null); 791 optWsInputReader.get().feed(null); 792 } 793 wsChannel.associated(ConsoleConnection.class).ifPresent(connection -> { 794 // Restore channel to normal mode, see onConsoleReady 795 connection.responsePipeline().restrictEventSource(null); 796 connection.disconnected(); 797 }); 798 } 799 800 /** 801 * Sends a command to the console. 802 * 803 * @param event the event 804 * @param channel the channel 805 * @throws InterruptedException the interrupted exception 806 * @throws IOException Signals that an I/O exception has occurred. 807 */ 808 @Handler(channels = ConsoleChannel.class, priority = -1000) 809 public void onConsoleCommand( 810 ConsoleCommand event, ConsoleConnection channel) 811 throws InterruptedException, IOException { 812 IOSubchannel upstream = channel.upstreamChannel(); 813 @SuppressWarnings({ "resource", "PMD.CloseResource" }) 814 CharBufferWriter out = new CharBufferWriter(upstream, 815 upstream.responsePipeline()).suppressClose(); 816 event.emitJson(out); 817 } 818 819 /** 820 * The channel used to send {@link PageResourceRequest}s and 821 * {@link ConletResourceRequest}s to the web console components (via the 822 * console). 823 */ 824 public class ConsoleResourceChannel extends LinkedIOSubchannel { 825 826 /** 827 * Instantiates a new console resource channel. 828 * 829 * @param hub the hub 830 * @param upstreamChannel the upstream channel 831 * @param responsePipeline the response pipeline 832 */ 833 public ConsoleResourceChannel(Manager hub, 834 IOSubchannel upstreamChannel, EventPipeline responsePipeline) { 835 super(hub, hub.channel(), upstreamChannel, responsePipeline); 836 } 837 } 838 839 /** 840 * The implementation of {@link RenderSupport} used by this class. 841 */ 842 private final class RenderSupportImpl implements RenderSupport { 843 844 @Override 845 public URI consoleBaseResource(URI uri) { 846 return prefix 847 .resolve(WebConsoleUtils.uriFromPath("console-base-resource/")) 848 .resolve(uri); 849 } 850 851 @Override 852 public URI consoleResource(URI uri) { 853 return prefix 854 .resolve(WebConsoleUtils.uriFromPath("console-resource/")) 855 .resolve(uri); 856 } 857 858 @Override 859 public URI conletResource(String conletType, URI uri) { 860 return prefix.resolve(WebConsoleUtils.uriFromPath( 861 "conlet-resource/" + conletType + "/")).resolve(uri); 862 } 863 864 @Override 865 public URI pageResource(URI uri) { 866 return prefix.resolve(WebConsoleUtils.uriFromPath( 867 "page-resource/")).resolve(uri); 868 } 869 870 /* 871 * (non-Javadoc) 872 * 873 * @see 874 * org.jgrapes.webconsole.base.base.RenderSupport#useMinifiedResources() 875 */ 876 @Override 877 public boolean useMinifiedResources() { 878 return useMinifiedResources; 879 } 880 881 } 882 883}