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