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}