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