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}