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