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