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