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.beans.ConstructorProperties;
022import java.io.BufferedReader;
023import java.io.IOException;
024import java.io.Reader;
025import java.io.Serializable;
026import java.io.StringWriter;
027import java.net.URL;
028import java.nio.CharBuffer;
029import java.time.Duration;
030import java.time.Instant;
031import java.util.Collection;
032import java.util.Collections;
033import java.util.HashMap;
034import java.util.HashSet;
035import java.util.Iterator;
036import java.util.Locale;
037import java.util.Map;
038import java.util.Optional;
039import java.util.ResourceBundle;
040import java.util.Set;
041import java.util.UUID;
042import java.util.WeakHashMap;
043import java.util.concurrent.ConcurrentHashMap;
044import java.util.concurrent.Future;
045import java.util.function.Supplier;
046
047import org.jgrapes.core.Channel;
048import org.jgrapes.core.Component;
049import org.jgrapes.core.Components;
050import org.jgrapes.core.Components.Timer;
051import org.jgrapes.core.Event;
052import org.jgrapes.core.annotation.Handler;
053import org.jgrapes.core.annotation.HandlerDefinition.ChannelReplacements;
054import org.jgrapes.http.Session;
055import org.jgrapes.io.IOSubchannel;
056import org.jgrapes.io.events.Closed;
057import org.jgrapes.portal.base.events.AddPortletRequest;
058import org.jgrapes.portal.base.events.AddPortletType;
059import org.jgrapes.portal.base.events.DeletePortlet;
060import org.jgrapes.portal.base.events.DeletePortletRequest;
061import org.jgrapes.portal.base.events.NotifyPortletModel;
062import org.jgrapes.portal.base.events.NotifyPortletView;
063import org.jgrapes.portal.base.events.PortalReady;
064import org.jgrapes.portal.base.events.PortletResourceRequest;
065import org.jgrapes.portal.base.events.RenderPortlet;
066import org.jgrapes.portal.base.events.RenderPortletRequest;
067import org.jgrapes.portal.base.events.RenderPortletRequestBase;
068
069/**
070 * Provides a base class for implementing portlet components.
071 * In addition to translating events to invocations of abstract
072 * methods, this class manages the state information of a
073 * portlet instance.
074 * 
075 * # Event handling
076 * 
077 * The following diagrams show the events exchanged between
078 * the {@link Portal} and a portlet from the portlet's 
079 * perspective. If applicable, they also show how the events 
080 * are translated by the {@link AbstractPortlet} to invocations 
081 * of the abstract methods that have to be implemented by the
082 * derived class (the portlet component that provides
083 * a specific portlet type).
084 * 
085 * ## PortalReady
086 * 
087 * ![Add portlet type handling](AddPortletTypeHandling.svg)
088 * 
089 * From the portal page point of view, a portlet consists of
090 * CSS and JavaScript that is added to the portal page by
091 * {@link AddPortletType} events and HTML that is provided by 
092 * {@link RenderPortlet} events (see below). These events must 
093 * therefore be generated by a portlet component. With respect to
094 * the firing of the initial {@link AddPortletType}, 
095 * the {@link AbstractPortlet} does not provide any support. The
096 * handler for the {@link PortalReady} must be implemented by
097 * the derived class itself.
098 *
099 * ## AddPortletRequest
100 * 
101 * ![Add portlet handling](AddPortletHandling.svg)
102 * 
103 * The {@link AddPortletRequest} indicates that a new portlet
104 * instance of a given type should be added to the page. The
105 * {@link AbstractPortlet} checks the type requested, and if
106 * it matches, invokes {@link #doAddPortlet doAddPortlet}. The
107 * derived class generates a new unique portlet id (optionally 
108 * using {@link #generatePortletId generatePortletId}) 
109 * and a state (model) for the instance. The derived class 
110 * calls {@link #putInSession putInSession} to make the
111 * state known to the {@link AbstractPortlet}. Eventually,
112 * it fires the {@link RenderPortlet} event and returns the
113 * new portlet id. The {@link RenderPortlet} event delivers
114 * the HTML that represents the portlet on the page to the portal
115 * session. The portlet state may be used to generate HTML that represents
116 * the state. Alternatively, state independent HTML may be delivered
117 * followed by a {@link NotifyPortletView} event that updates
118 * the HTML using JavaScript in the portal page.
119 * 
120 * ## RenderPortlet
121 * 
122 * ![Render portlet handling](RenderPortletHandling.svg)
123 * 
124 * A {@link RenderPortlet} event indicates that the portal page
125 * needs the HTML for displaying a portlet. This may be cause
126 * by e.g. a refresh or by requesting a full page view from
127 * the preview.
128 * 
129 * Upon receiving such an event, the {@link AbstractPortlet}
130 * checks if it has state information for the portlet id
131 * requested. If so, it invokes 
132 * {@link #doRenderPortlet doRenderPortlet}
133 * with the state information. This method has to fire
134 * the {@link RenderPortlet} event that delivers the HTML.
135 * 
136 * ## DeletePortletRequest
137 * 
138 * ![Delete portlet handling](DeletePortletHandling.svg)
139 * 
140 * When the {@link AbstractPortlet} receives a {@link DeletePortletRequest},
141 * it checks if state information for the portlet id exists. If so,
142 * it deletes the state information from the session and
143 * invokes 
144 * {@link #doDeletePortlet(DeletePortletRequest, PortalSession, String, Serializable)}
145 * with the state information. This method fires the {@link DeletePortlet}
146 * event that confirms the deletion of the portlet.
147 *
148 * ## NotifyPortletModel
149 * 
150 * ![Notify portlet model handling](NotifyPortletModelHandling.svg)
151 * 
152 * If the portlet display includes input elements, actions on these
153 * elements may result in {@link NotifyPortletModel} events from
154 * the portal page to the portal. When the {@link AbstractPortlet}
155 * receives such events, it checks if state information for the 
156 * portlet id exists. If so, it invokes 
157 * {@link #doNotifyPortletModel doNotifyPortletModel} with the
158 * retrieved information. The portal component usually responds with
159 * a {@link NotifyPortletView} event. However, it can also
160 * re-render the complete portelt display.
161 *
162 * Support for unsolicited updates
163 * -------------------------------
164 * 
165 * In addition, the class provides support for tracking the 
166 * relationship between {@link PortalSession}s and the ids 
167 * of portlets displayed in the portal session and support for
168 * unsolicited updates.
169 * 
170 * @startuml AddPortletTypeHandling.svg
171 * hide footbox
172 * 
173 * activate Portal
174 * Portal -> PortletComponent: PortalReady
175 * deactivate Portal
176 * activate PortletComponent
177 * PortletComponent -> Portal: AddPortletType 
178 * deactivate PortletComponent
179 * activate Portal
180 * deactivate Portal
181 * @enduml
182 * 
183 * @startuml AddPortletHandling.svg
184 * hide footbox
185 * 
186 * activate Portal
187 * Portal -> PortletComponent: AddPortletRequest
188 * deactivate Portal
189 * activate PortletComponent
190 * PortletComponent -> PortletComponent: doAddPortlet
191 * activate PortletComponent
192 * opt
193 *         PortletComponent -> PortletComponent: generatePortletId
194 * end opt
195 * PortletComponent -> PortletComponent: putInSession
196 * PortletComponent -> Portal: RenderPortlet
197 * opt 
198 *     PortletComponent -> Portal: NotifyPortletView
199 * end opt 
200 * deactivate PortletComponent
201 * deactivate PortletComponent
202 * activate Portal
203 * deactivate Portal
204 * @enduml
205 * 
206 * @startuml RenderPortletHandling.svg
207 * hide footbox
208 * 
209 * activate Portal
210 * Portal -> PortletComponent: RenderPortlet
211 * deactivate Portal
212 * activate PortletComponent
213 * PortletComponent -> PortletComponent: doRenderPortlet
214 * activate PortletComponent
215 * PortletComponent -> Portal: RenderPortlet 
216 * opt 
217 *     PortletComponent -> Portal: NotifyPortletView
218 * end opt 
219 * deactivate PortletComponent
220 * deactivate PortletComponent
221 * activate Portal
222 * deactivate Portal
223 * @enduml
224 * 
225 * @startuml NotifyPortletModelHandling.svg
226 * hide footbox
227 * 
228 * activate Portal
229 * Portal -> PortletComponent: NotifyPortletModel
230 * deactivate Portal
231 * activate PortletComponent
232 * PortletComponent -> PortletComponent: doNotifyPortletModel
233 * activate PortletComponent
234 * opt
235 *     PortletComponent -> Portal: RenderPortlet
236 * end opt 
237 * opt 
238 *     PortletComponent -> Portal: NotifyPortletView
239 * end opt 
240 * deactivate PortletComponent
241 * deactivate PortletComponent
242 * activate Portal
243 * deactivate Portal
244 * @enduml
245 * 
246 * @startuml DeletePortletHandling.svg
247 * hide footbox
248 * 
249 * activate Portal
250 * Portal -> PortletComponent: DeletePortletRequest
251 * deactivate Portal
252 * activate PortletComponent
253 * PortletComponent -> PortletComponent: doDeletePortlet
254 * activate PortletComponent
255 * PortletComponent -> Portal: DeletePortlet 
256 * deactivate PortletComponent
257 * deactivate PortletComponent
258 * activate Portal
259 * deactivate Portal
260 * @enduml
261 */
262@SuppressWarnings({ "PMD.TooManyMethods",
263    "PMD.EmptyMethodInAbstractClassShouldBeAbstract" })
264public abstract class AbstractPortlet extends Component {
265
266    private Map<PortalSession, Set<String>> portletIdsByPortalSession;
267    private Duration refreshInterval;
268    private Supplier<Event<?>> refreshEventSupplier;
269    private Timer refreshTimer;
270
271    /**
272     * Creates a new component that listens for new events
273     * on the given channel.
274     * 
275     * @param channel the channel to listen on
276     * @param trackPortalSessions if set, track the relationship between
277     * portal sessions and portlet ids
278     */
279    public AbstractPortlet(Channel channel, boolean trackPortalSessions) {
280        this(channel, null, trackPortalSessions);
281    }
282
283    /**
284     * Like {@link #AbstractPortlet(Channel, boolean)}, but supports
285     * the specification of channel replacements.
286     *
287     * @param channel the channel to listen on
288     * @param channelReplacements the channel replacements (see
289     * {@link Component})
290     * @param trackPortalSessions if set, track the relationship between
291     * portal sessions and portlet ids
292     */
293    public AbstractPortlet(Channel channel,
294            ChannelReplacements channelReplacements,
295            boolean trackPortalSessions) {
296        super(channel, channelReplacements);
297        if (trackPortalSessions) {
298            portletIdsByPortalSession = Collections.synchronizedMap(
299                new WeakHashMap<>());
300        }
301    }
302
303    /**
304     * If set to a value different from `null` causes an event
305     * from the given supplier to be fired on all tracked portal
306     * sessions periodically.
307     * 
308     * @param interval the refresh interval
309     * @return the portlet for easy chaining
310     */
311    public AbstractPortlet setPeriodicRefresh(
312            Duration interval, Supplier<Event<?>> supplier) {
313        refreshInterval = interval;
314        refreshEventSupplier = supplier;
315        if (refreshTimer != null) {
316            refreshTimer.cancel();
317            refreshTimer = null;
318        }
319        updateRefresh();
320        return this;
321    }
322
323    private void updateRefresh() {
324        if (refreshInterval == null || portletIdsByPortalSession().isEmpty()) {
325            // At least one of the prerequisites is missing, terminate
326            if (refreshTimer != null) {
327                refreshTimer.cancel();
328                refreshTimer = null;
329            }
330            return;
331        }
332        if (refreshTimer != null) {
333            // Already running.
334            return;
335        }
336        refreshTimer = Components.schedule(tmr -> {
337            tmr.reschedule(tmr.scheduledFor().plus(refreshInterval));
338            fire(refreshEventSupplier.get(), trackedSessions());
339        }, Instant.now().plus(refreshInterval));
340    }
341
342    /**
343     * Returns the portlet type. The default implementation
344     * returns the class' name.
345     * 
346     * @return the type
347     */
348    protected String type() {
349        return getClass().getName();
350    }
351
352    /**
353     * A default handler for resource requests. Checks that the request
354     * is directed at this portlet, and calls {@link #doGetResource}.
355     * 
356     * @param event the resource request event
357     * @param channel the channel that the request was recived on
358     */
359    @Handler
360    public final void onResourceRequest(
361            PortletResourceRequest event, IOSubchannel channel) {
362        // For me?
363        if (!event.portletClass().equals(type())) {
364            return;
365        }
366        doGetResource(event, channel);
367    }
368
369    /**
370     * The default implementation searches for a file with the 
371     * requested resource URI in the portlet's class path and sets 
372     * its {@link URL} as result if found.
373     * 
374     * @param event the event. The result will be set to
375     * `true` on success
376     * @param channel the channel
377     */
378    protected void doGetResource(PortletResourceRequest event,
379            IOSubchannel channel) {
380        URL resourceUrl = this.getClass().getResource(
381            event.resourceUri().getPath());
382        if (resourceUrl == null) {
383            return;
384        }
385        event.setResult(new ResourceByUrl(event, resourceUrl));
386        event.stop();
387    }
388
389    /**
390     * Provides a resource bundle for localization.
391     * The default implementation looks up a bundle using the
392     * package name plus "l10n" as base name.
393     * 
394     * @return the resource bundle
395     */
396    protected ResourceBundle resourceBundle(Locale locale) {
397        return ResourceBundle.getBundle(
398            getClass().getPackage().getName() + ".l10n", locale,
399            getClass().getClassLoader(),
400            ResourceBundle.Control.getNoFallbackControl(
401                ResourceBundle.Control.FORMAT_DEFAULT));
402    }
403
404    /**
405     * Generates a new unique portlet id.
406     * 
407     * @return the portlet id
408     */
409    protected String generatePortletId() {
410        return UUID.randomUUID().toString();
411    }
412
413    /**
414     * Returns the tracked models and channels as unmodifiable map.
415     * If sessions are not tracked, the method returns an empty map.
416     * It is therefore always safe to invoke the method and use its
417     * result.
418     * 
419     * If you need a particular session's portlet ids, you should
420     * prefer {@link #portletIds(PortalSession)} over calling
421     * this method with `get(portalSession)` appended.
422     * 
423     * @return the result
424     */
425    protected Map<PortalSession, Set<String>> portletIdsByPortalSession() {
426        // Create copy to get a non-weak map.
427        if (portletIdsByPortalSession != null) {
428            return Collections.unmodifiableMap(
429                new HashMap<>(portletIdsByPortalSession));
430        }
431        return Collections.emptyMap();
432    }
433
434    /**
435     * Returns the tracked sessions. This is effectively
436     * `portletIdsByPortalSession().keySet()` converted to
437     * an array. This representation is especially useful 
438     * when the portal sessions are used as argument for 
439     * {@link #fire(Event, Channel...)}.
440     *
441     * @return the portal sessions
442     */
443    protected PortalSession[] trackedSessions() {
444        if (portletIdsByPortalSession == null) {
445            return new PortalSession[0];
446        }
447        Set<PortalSession> sessions = new HashSet<>(
448            portletIdsByPortalSession.keySet());
449        return sessions.toArray(new PortalSession[0]);
450    }
451
452    /**
453     * Returns the set of portlet ids associated with the portal session
454     * as an unmodifiable {@link Set}. If sessions aren't tracked, or no
455     * portlets have registered yet, an empty set is returned. The method 
456     * can therefore always be called and always returns a usable result.
457     * 
458     * @param portalSession the portal session
459     * @return the set
460     */
461    protected Set<String> portletIds(PortalSession portalSession) {
462        if (portletIdsByPortalSession != null) {
463            return Collections.unmodifiableSet(
464                portletIdsByPortalSession.getOrDefault(
465                    portalSession, Collections.emptySet()));
466        }
467        return new HashSet<>();
468    }
469
470    /**
471     * Track the given portlet from the given session if tracking is
472     * enabled.
473     *
474     * @param portalSession the portal session
475     * @param portletId the portlet id
476     */
477    protected void trackPortlet(PortalSession portalSession, String portletId) {
478        if (portletIdsByPortalSession != null) {
479            portletIdsByPortalSession.computeIfAbsent(portalSession,
480                newKey -> ConcurrentHashMap.newKeySet()).add(portletId);
481            updateRefresh();
482        }
483    }
484
485    /**
486     * Puts the given portlet state in the session using the 
487     * {@link #type()} and the given portlet id as keys.
488     * 
489     * @param session the session to use
490     * @param portletId the portlet id
491     * @param portletState the portlet state
492     * @return the portlet state
493     */
494    @SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" })
495    protected <T extends Serializable> T putInSession(
496            Session session, String portletId, T portletState) {
497        ((Map<Serializable,
498                Map<Serializable, Map<String, Serializable>>>) (Map<
499                        Serializable, ?>) session)
500                            .computeIfAbsent(AbstractPortlet.class,
501                                newKey -> new ConcurrentHashMap<>())
502                            .computeIfAbsent(type(),
503                                newKey -> new ConcurrentHashMap<>())
504                            .put(portletId, portletState);
505        return portletState;
506    }
507
508    /**
509     * Puts the given portlet instance state in the browser
510     * session associated with the channel, using  
511     * {@link #type()} and the portlet id from the model.
512     *
513     * @param session the session to use
514     * @param portletModel the portlet model
515     * @return the portlet model
516     */
517    protected <T extends PortletBaseModel> T putInSession(
518            Session session, T portletModel) {
519        return putInSession(session, portletModel.getPortletId(), portletModel);
520    }
521
522    /**
523     * Returns the portlet state of this portlet's type with the given id
524     * from the session.
525     * 
526     * @param session the session to use
527     * @param portletId the portlet id
528     * @param type the state's type
529     * @return the portlet state
530     */
531    @SuppressWarnings("unchecked")
532    protected <T extends Serializable> Optional<T> stateFromSession(
533            Session session, String portletId, Class<T> type) {
534        return Optional.ofNullable(
535            ((Map<Serializable,
536                    Map<Serializable, Map<String, T>>>) (Map<Serializable,
537                            ?>) session)
538                                .computeIfAbsent(AbstractPortlet.class,
539                                    newKey -> new HashMap<>())
540                                .computeIfAbsent(type(),
541                                    newKey -> new HashMap<>())
542                                .get(portletId));
543    }
544
545    /**
546     * Returns all portlet states of this portlet's type from the
547     * session.
548     * 
549     * @param channel the channel, used to access the session
550     * @param type the states' type
551     * @return the states
552     */
553    @SuppressWarnings("unchecked")
554    protected <T extends Serializable> Collection<T> statesFromSession(
555            IOSubchannel channel, Class<T> type) {
556        return channel.associated(Session.class)
557            .map(session -> ((Map<Serializable,
558                    Map<Serializable, Map<String, T>>>) (Map<Serializable,
559                            ?>) session)
560                                .computeIfAbsent(AbstractPortlet.class,
561                                    newKey -> new HashMap<>())
562                                .computeIfAbsent(type(),
563                                    newKey -> new HashMap<>())
564                                .values())
565            .orElseThrow(
566                () -> new IllegalStateException("Session is missing."));
567    }
568
569    /**
570     * Removes the portlet state of the portlet with the given id
571     * from the session. 
572     * 
573     * @param session the session to use
574     * @param portletId the portlet id
575     * @return the removed state if state existed
576     */
577    @SuppressWarnings("unchecked")
578    protected Optional<? extends Serializable> removeState(
579            Session session, String portletId) {
580        Serializable state = ((Map<Serializable,
581                Map<Serializable, Map<String, Serializable>>>) (Map<
582                        Serializable, ?>) session)
583                            .computeIfAbsent(AbstractPortlet.class,
584                                newKey -> new HashMap<>())
585                            .computeIfAbsent(type(), newKey -> new HashMap<>())
586                            .remove(portletId);
587        return Optional.ofNullable(state);
588    }
589
590    /**
591     * Checks if the request applies to this component. If so, stops the event,
592     * and calls {@link #doAddPortlet}. 
593     * 
594     * @param event the event
595     * @param portalSession the channel
596     */
597    @Handler
598    @SuppressWarnings({ "PMD.SignatureDeclareThrowsException",
599        "PMD.AvoidDuplicateLiterals" })
600    public final void onAddPortletRequest(AddPortletRequest event,
601            PortalSession portalSession) throws Exception {
602        if (!event.portletType().equals(type())) {
603            return;
604        }
605        event.stop();
606        String portletId = doAddPortlet(event, portalSession);
607        event.setResult(portletId);
608        trackPortlet(portalSession, portletId);
609    }
610
611    /**
612     * Called by {@link #onAddPortletRequest} to complete adding the portlet.
613     * If the portlet has associated state, the implementation should
614     * call {@link #putInSession(Session, String, Serializable)} to create
615     * the state and put it in the session.
616     * 
617     * @param event the event
618     * @param portalSession the channel
619     * @return the id of the created portlet
620     */
621    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
622    protected abstract String doAddPortlet(AddPortletRequest event,
623            PortalSession portalSession) throws Exception;
624
625    /**
626     * Checks if the request applies to this component. If so, stops 
627     * the event, removes the portlet state from the browser session
628     * and calls {@link #doDeletePortlet} with the state.
629     * 
630     * If the association of {@link PortalSession}s and portlet ids
631     * is tracked for this portlet, any existing association is
632     * also removed.
633     * 
634     * @param event the event
635     * @param portalSession the portal session
636     */
637    @Handler
638    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
639    public final void onDeletePortletRequest(DeletePortletRequest event,
640            PortalSession portalSession) throws Exception {
641        Optional<? extends Serializable> optPortletState
642            = stateFromSession(portalSession.browserSession(),
643                event.portletId(), Serializable.class);
644        if (!optPortletState.isPresent()) {
645            return;
646        }
647        String portletId = event.portletId();
648        removeState(portalSession.browserSession(), portletId);
649        if (portletIdsByPortalSession != null) {
650            for (Iterator<PortalSession> psi = portletIdsByPortalSession
651                .keySet().iterator(); psi.hasNext();) {
652                Set<String> idSet = portletIdsByPortalSession.get(psi.next());
653                idSet.remove(portletId);
654                if (idSet.isEmpty()) {
655                    psi.remove();
656                }
657            }
658            updateRefresh();
659        }
660        event.stop();
661        doDeletePortlet(event, portalSession, event.portletId(),
662            optPortletState.get());
663    }
664
665    /**
666     * Called by {@link #onDeletePortletRequest} to complete deleting
667     * the portlet. If the portlet component wants to veto the
668     * deletion of the portlet, it puts the state information back in
669     * the session with 
670     * {@link #putInSession(Session, String, Serializable)} and does
671     * not fire the {@link DeletePortlet} event.
672     * 
673     * @param event the event
674     * @param channel the channel
675     * @param portletId the portlet id
676     * @param portletState the portlet state
677     */
678    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
679    protected abstract void doDeletePortlet(DeletePortletRequest event,
680            PortalSession channel, String portletId,
681            Serializable portletState) throws Exception;
682
683    /**
684     * Checks if the request applies to this component by calling
685     * {@link #stateFromSession(Session, String, Class)}. If a model
686     * is found, sets the event's result to `true`, stops the event, and 
687     * calls {@link #doRenderPortlet} with the state information. 
688     * 
689     * Some portlets that do not persist their models between sessions
690     * (e.g. because the model only references data maintained elsewhere)
691     * should override {@link #stateFromSession(Session, String, Class)}
692     * in such a way that it creates the requested model if it doesn't 
693     * exist yet.
694     * 
695     * @param event the event
696     * @param portalSession the portal session
697     */
698    @Handler
699    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
700    public final void onRenderPortlet(RenderPortletRequest event,
701            PortalSession portalSession) throws Exception {
702        Optional<? extends Serializable> optPortletState
703            = stateFromSession(portalSession.browserSession(),
704                event.portletId(), Serializable.class);
705        if (!optPortletState.isPresent()) {
706            return;
707        }
708        event.setResult(true);
709        event.stop();
710        doRenderPortlet(
711            event, portalSession, event.portletId(), optPortletState.get());
712        trackPortlet(portalSession, event.portletId());
713    }
714
715    /**
716     * Called by {@link #onRenderPortlet} to complete rendering
717     * the portlet.
718     * 
719     * @param event the event
720     * @param channel the channel
721     * @param portletId the portlet id
722     * @param portletState the portletState
723     */
724    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
725    protected abstract void doRenderPortlet(RenderPortletRequest event,
726            PortalSession channel, String portletId,
727            Serializable portletState) throws Exception;
728
729    /**
730     * Checks if the request applies to this component by calling
731     * {@link #stateFromSession(Session, String, Class)}. If a model
732     * is found, calls {@link #doNotifyPortletModel} with the state 
733     * information. 
734     * 
735     * @param event the event
736     * @param channel the channel
737     */
738    @Handler
739    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
740    public final void onNotifyPortletModel(NotifyPortletModel event,
741            PortalSession channel) throws Exception {
742        Optional<? extends Serializable> optPortletState
743            = stateFromSession(channel.browserSession(), event.portletId(),
744                Serializable.class);
745        if (!optPortletState.isPresent()) {
746            return;
747        }
748        doNotifyPortletModel(event, channel, optPortletState.get());
749    }
750
751    /**
752     * Called by {@link #onNotifyPortletModel} to complete handling
753     * the notification. The default implementation does nothing.
754     * 
755     * @param event the event
756     * @param channel the channel
757     * @param portletState the portletState
758     */
759    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
760    protected void doNotifyPortletModel(NotifyPortletModel event,
761            PortalSession channel, Serializable portletState)
762            throws Exception {
763        // Default is to do nothing.
764    }
765
766    /**
767     * Removes the {@link PortalSession} from the set of tracked sessions.
768     * If derived portlets need to perform extra actions when a
769     * portalSession is closed, they have to override 
770     * {@link #afterOnClosed(Closed, PortalSession)}.
771     * 
772     * @param event the closed event
773     * @param portalSession the portal session
774     */
775    @Handler
776    public final void onClosed(Closed event, PortalSession portalSession) {
777        if (portletIdsByPortalSession != null) {
778            portletIdsByPortalSession.remove(portalSession);
779            updateRefresh();
780        }
781        afterOnClosed(event, portalSession);
782    }
783
784    /**
785     * Invoked by {@link #onClosed(Closed, PortalSession)} after
786     * the portal session has been removed from the set of
787     * tracked sessions. The default implementation does
788     * nothing.
789     * 
790     * @param event the closed event
791     * @param portalSession the portal session
792     */
793    protected void afterOnClosed(Closed event, PortalSession portalSession) {
794        // Default is to do nothing.
795    }
796
797    /**
798     * Defines the portlet model following the JavaBean conventions.
799     * 
800     * Portlet models should follow these conventions because
801     * many template engines rely on them and to support serialization
802     * to portable formats. 
803     */
804    @SuppressWarnings("serial")
805    public static class PortletBaseModel implements Serializable {
806
807        protected String portletId;
808
809        /**
810         * Creates a new model with the given type and id.
811         * 
812         * @param portletId the portlet id
813         */
814        @ConstructorProperties({ "portletId" })
815        public PortletBaseModel(String portletId) {
816            this.portletId = portletId;
817        }
818
819        /**
820         * Returns the portlet id.
821         * 
822         * @return the portlet id
823         */
824        public String getPortletId() {
825            return portletId;
826        }
827
828        /*
829         * (non-Javadoc)
830         * 
831         * @see java.lang.Object#hashCode()
832         */
833        @Override
834        @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
835        public int hashCode() {
836            @SuppressWarnings("PMD.AvoidFinalLocalVariable")
837            final int prime = 31;
838            int result = 1;
839            result = prime * result
840                + ((portletId == null) ? 0 : portletId.hashCode());
841            return result;
842        }
843
844        /**
845         * Two objects are equal if they have equal portlet ids.
846         * 
847         * @param obj the other object
848         * @return the result
849         */
850        @Override
851        public boolean equals(Object obj) {
852            if (this == obj) {
853                return true;
854            }
855            if (obj == null) {
856                return false;
857            }
858            if (getClass() != obj.getClass()) {
859                return false;
860            }
861            PortletBaseModel other = (PortletBaseModel) obj;
862            if (portletId == null) {
863                if (other.portletId != null) {
864                    return false;
865                }
866            } else if (!portletId.equals(other.portletId)) {
867                return false;
868            }
869            return true;
870        }
871    }
872
873    /**
874     * Send to the portal page for adding or updating a complete portlet
875     * representation.
876     */
877    public class RenderPortletFromReader extends RenderPortlet {
878
879        private final Future<String> content;
880
881        /**
882         * Creates a new event.
883         * 
884         * @param portletClass the portlet class
885         * @param portletId the id of the portlet
886         */
887        public RenderPortletFromReader(RenderPortletRequestBase<?> request,
888                Class<?> portletClass, String portletId, Reader contentReader) {
889            super(portletClass, portletId);
890            // Start to prepare the content immediately and concurrently.
891            content = request.processedBy().map(pby -> pby.executorService())
892                .orElse(Components.defaultExecutorService()).submit(() -> {
893                    StringWriter content = new StringWriter();
894                    CharBuffer buffer = CharBuffer.allocate(8192);
895                    try (Reader rdr = new BufferedReader(contentReader)) {
896                        while (true) {
897                            if (rdr.read(buffer) < 0) {
898                                break;
899                            }
900                            buffer.flip();
901                            content.append(buffer);
902                            buffer.clear();
903                        }
904                    } catch (IOException e) {
905                        throw new IllegalStateException(e);
906                    }
907                    return content.toString();
908                });
909
910        }
911
912        @Override
913        public Future<String> content() {
914            return content;
915        }
916    }
917}