001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2017-2020 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.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.Map.Entry;
039import java.util.Optional;
040import java.util.ResourceBundle;
041import java.util.Set;
042import java.util.UUID;
043import java.util.WeakHashMap;
044import java.util.concurrent.ConcurrentHashMap;
045import java.util.concurrent.Future;
046import java.util.function.Supplier;
047import java.util.stream.Collectors;
048import org.jgrapes.core.Channel;
049import org.jgrapes.core.Component;
050import org.jgrapes.core.Components;
051import org.jgrapes.core.Components.Timer;
052import org.jgrapes.core.Event;
053import org.jgrapes.core.annotation.Handler;
054import org.jgrapes.core.annotation.HandlerDefinition.ChannelReplacements;
055import org.jgrapes.http.Session;
056import org.jgrapes.io.IOSubchannel;
057import org.jgrapes.io.events.Closed;
058import org.jgrapes.webconsole.base.Conlet.RenderMode;
059import org.jgrapes.webconsole.base.events.AddConletRequest;
060import org.jgrapes.webconsole.base.events.AddConletType;
061import org.jgrapes.webconsole.base.events.ConletDeleted;
062import org.jgrapes.webconsole.base.events.ConletResourceRequest;
063import org.jgrapes.webconsole.base.events.ConsoleReady;
064import org.jgrapes.webconsole.base.events.NotifyConletModel;
065import org.jgrapes.webconsole.base.events.NotifyConletView;
066import org.jgrapes.webconsole.base.events.RenderConlet;
067import org.jgrapes.webconsole.base.events.RenderConletRequest;
068import org.jgrapes.webconsole.base.events.RenderConletRequestBase;
069import org.jgrapes.webconsole.base.events.SetLocale;
070
071/**
072 * Provides a base class for implementing web console components.
073 * In addition to translating events to invocations of abstract
074 * methods, this class manages the state information of a
075 * web console component instance.
076 * 
077 * # Event handling
078 * 
079 * The following diagrams show the events exchanged between
080 * the {@link WebConsole} and a web console component from the 
081 * web console component's perspective. If applicable, they also show 
082 * how the events are translated by the {@link AbstractConlet} to invocations 
083 * of the abstract methods that have to be implemented by the
084 * derived class (the web console component component that provides
085 * a specific web console component type).
086 * 
087 * ## ConsoleReady
088 * 
089 * ![Add web console component type handling](AddConletTypeHandling.svg)
090 * 
091 * From the web console's page point of view, a web console component 
092 * consists of CSS and JavaScript that is added to the console page by
093 * {@link AddConletType} events and HTML that is provided by 
094 * {@link RenderConlet} events (see below). These events must 
095 * therefore be generated by a web console component. With respect to
096 * the firing of the initial {@link AddConletType}, 
097 * the {@link AbstractConlet} does not provide any support. The
098 * handler for the {@link ConsoleReady} must be implemented by
099 * the derived class itself.
100 * 
101 * ## AddConletRequest
102 * 
103 * ![Add web console component handling](AddConletHandling.svg)
104 * 
105 * The {@link AddConletRequest} indicates that a new web console component
106 * instance of a given type should be added to the page. The
107 * {@link AbstractConlet} checks the type requested, and if
108 * it matches, invokes {@link #doAddConlet doAddConlet}. The
109 * derived class generates a new unique web console component id (optionally 
110 * using {@link #generateConletId generateConletId}) 
111 * and a model (state) for the instance. It 
112 * calls {@link #putInSession putInSession} to make the
113 * model known to the {@link AbstractConlet}. Eventually,
114 * it fires the {@link RenderConlet} event and returns a new
115 * {@link ConletTrackingInfo} with the conlet id and the rendered
116 * modes. 
117 * 
118 * The {@link RenderConlet} event provides to the console session
119 * the HTML that represents the web console component on the page.
120 * The HTML may be generated using and thus depending on the web console
121 * component model.
122 * Alternatively, state independent HTML may be provided followed 
123 * by a {@link NotifyConletView} event that updates
124 * the HTML (using JavaScript) on the console page. The latter approach
125 * is preferred if the model changes frequently and updating the
126 * rendered representation is more efficient than providing a new one.
127 * 
128 * ## RenderConletRequest
129 * 
130 * ![Render web console component handling](RenderConletHandling.svg)
131 * 
132 * A {@link RenderConletRequest} event indicates that the web console page
133 * needs the HTML for displaying a web console component. This may be caused
134 * by e.g. a refresh or by requesting a full page view from
135 * the preview.
136 * 
137 * Upon receiving such an event, the {@link AbstractConlet}
138 * checks if it has state information for the web console component id
139 * requested. If so, it invokes {@link #doRenderConlet doRenderConlet}
140 * with the state information. This method has to fire
141 * the {@link RenderConlet} event that provides the HTML to the console.
142 * 
143 * Method {@link #doRenderConlet doRenderConlet} returns the rendered
144 * view mode(s) which are used to updated the conlet tracking information.
145 * 
146 * ## ConletDeleted
147 * 
148 * ![Web console component deleted handling](ConletDeletedHandling.svg)
149 * 
150 * When the {@link AbstractConlet} receives a {@link ConletDeleted},
151 * it updates the information about the shown conlet views. If the
152 * conlet is no longer used in the browser (no views remain),
153 * it deletes the state information from the session. In any case, it
154 * invokes 
155 * {@link #doConletDeleted(ConletDeleted, ConsoleSession, String, Serializable)}
156 * with the state information.
157 * 
158 * ## NotifyConletModel
159 * 
160 * ![Notify web console component model handling](NotifyConletModelHandling.svg)
161 * 
162 * If the web console component display includes input elements, actions 
163 * on these elements may result in {@link NotifyConletModel} events from
164 * the web console page to the web console. When the {@link AbstractConlet}
165 * receives such events, it checks if state information for the 
166 * web console component id exists. If so, it invokes 
167 * {@link #doNotifyConletModel doNotifyConletModel} with the
168 * retrieved information. The web console component usually responds with
169 * a {@link NotifyConletView} event. However, it can also
170 * re-render the complete portelt display.
171 * 
172 * Support for unsolicited updates
173 * -------------------------------
174 * 
175 * In addition, the class provides support for tracking the 
176 * relationship between {@link ConsoleSession}s and the ids 
177 * of web console components displayed in the console session and support for
178 * unsolicited updates.
179 *
180 * @param <S> the type of the web console component state information
181 * 
182 * @startuml AddConletTypeHandling.svg
183 * hide footbox
184 * 
185 * activate WebConsole
186 * WebConsole -> Conlet: ConsoleReady
187 * deactivate WebConsole
188 * activate Conlet
189 * Conlet -> WebConsole: AddConletType 
190 * deactivate Conlet
191 * activate WebConsole
192 * deactivate WebConsole
193 * @enduml 
194 * @startuml AddConletHandling.svg
195 * hide footbox
196 * 
197 * activate WebConsole
198 * WebConsole -> Conlet: AddConletRequest
199 * deactivate WebConsole
200 * activate Conlet
201 * Conlet -> Conlet: doAddConlet
202 * activate Conlet
203 * opt
204 *         Conlet -> Conlet: generateConletId
205 * activate Conlet
206 * deactivate Conlet
207 * end opt
208 * Conlet -> Conlet: putInSession
209 * activate Conlet
210 * deactivate Conlet
211 * Conlet -> WebConsole: RenderConlet
212 * activate WebConsole
213 * deactivate WebConsole
214 * opt 
215 *     Conlet -> WebConsole: NotifyConletView
216 * activate WebConsole
217 * deactivate WebConsole
218 * end opt 
219 * deactivate Conlet
220 * Conlet -> Conlet: trackConlet
221 * activate Conlet
222 * deactivate Conlet
223 * deactivate Conlet
224 * @enduml 
225 * 
226 * @startuml RenderConletHandling.svg
227 * hide footbox
228 * 
229 * activate WebConsole
230 * WebConsole -> Conlet: RenderConletRequest
231 * deactivate WebConsole
232 * activate Conlet
233 * Conlet -> Conlet: doRenderConlet
234 * activate Conlet
235 * Conlet -> WebConsole: RenderConlet 
236 * activate WebConsole
237 * deactivate WebConsole
238 * opt 
239 *     Conlet -> WebConsole: NotifyConletView
240 * activate WebConsole
241 * deactivate WebConsole
242 * end opt 
243 * deactivate Conlet
244 * Conlet -> Conlet: trackConlet.addModes
245 * activate Conlet
246 * deactivate Conlet
247 * deactivate Conlet
248 * @enduml 
249 * 
250 * @startuml NotifyConletModelHandling.svg
251 * hide footbox
252 * 
253 * activate WebConsole
254 * WebConsole -> Conlet: NotifyConletModel
255 * deactivate WebConsole
256 * activate Conlet
257 * Conlet -> Conlet: doNotifyConletModel
258 * activate Conlet
259 * opt
260 *     Conlet -> WebConsole: RenderConlet
261 * end opt 
262 * opt 
263 *     Conlet -> WebConsole: NotifyConletView
264 * end opt 
265 * deactivate Conlet
266 * deactivate Conlet
267 * activate WebConsole
268 * deactivate WebConsole
269 * @enduml 
270 * 
271 * @startuml ConletDeletedHandling.svg
272 * hide footbox
273 * 
274 * activate WebConsole
275 * WebConsole -> Conlet: ConletDeleted
276 * deactivate WebConsole
277 * activate Conlet
278 * Conlet -> Conlet: trackConlet.removeModes
279 * activate Conlet
280 * deactivate Conlet
281 * Conlet -> Conlet: doConletDeleted
282 * activate Conlet
283 * deactivate Conlet
284 * deactivate Conlet
285 * @enduml 
286 */
287@SuppressWarnings({ "PMD.TooManyMethods",
288    "PMD.EmptyMethodInAbstractClassShouldBeAbstract" })
289public abstract class AbstractConlet<S extends Serializable>
290        extends Component {
291
292    private static final Map<Class<?>,
293            Map<Locale, ResourceBundle>> supportedLocales
294                = Collections.synchronizedMap(new WeakHashMap<>());
295    private static final Map<Class<?>,
296            Map<Locale, ResourceBundle>> l10nBundles
297                = Collections.synchronizedMap(new WeakHashMap<>());
298    private Map<ConsoleSession,
299            Map<String, ConletTrackingInfo>> conletInfosByConsoleSession;
300    private Duration refreshInterval;
301    private Supplier<Event<?>> refreshEventSupplier;
302    private Timer refreshTimer;
303
304    /**
305     * Creates a new component that listens for new events
306     * on the given channel.
307     * 
308     * @param channel the channel to listen on
309     */
310    public AbstractConlet(Channel channel) {
311        this(channel, null);
312    }
313
314    /**
315     * Like {@link #AbstractConlet(Channel)}, but supports
316     * the specification of channel replacements.
317     *
318     * @param channel the channel to listen on
319     * @param channelReplacements the channel replacements (see
320     * {@link Component})
321     */
322    public AbstractConlet(Channel channel,
323            ChannelReplacements channelReplacements) {
324        super(channel, channelReplacements);
325        conletInfosByConsoleSession
326            = Collections.synchronizedMap(new WeakHashMap<>());
327    }
328
329    /**
330     * If set to a value different from `null` causes an event
331     * from the given supplier to be fired on all tracked web console
332     * sessions periodically.
333     *
334     * @param interval the refresh interval
335     * @param supplier the supplier
336     * @return the web console component for easy chaining
337     */
338    public AbstractConlet<S> setPeriodicRefresh(
339            Duration interval, Supplier<Event<?>> supplier) {
340        refreshInterval = interval;
341        refreshEventSupplier = supplier;
342        if (refreshTimer != null) {
343            refreshTimer.cancel();
344            refreshTimer = null;
345        }
346        updateRefresh();
347        return this;
348    }
349
350    private void updateRefresh() {
351        if (refreshInterval == null || conletIdsByConsoleSession().isEmpty()) {
352            // At least one of the prerequisites is missing, terminate
353            if (refreshTimer != null) {
354                refreshTimer.cancel();
355                refreshTimer = null;
356            }
357            return;
358        }
359        if (refreshTimer != null) {
360            // Already running.
361            return;
362        }
363        refreshTimer = Components.schedule(tmr -> {
364            tmr.reschedule(tmr.scheduledFor().plus(refreshInterval));
365            fire(refreshEventSupplier.get(), trackedSessions());
366        }, Instant.now().plus(refreshInterval));
367    }
368
369    /**
370     * Returns the web console component type. The default implementation
371     * returns the class' name.
372     * 
373     * @return the type
374     */
375    protected String type() {
376        return getClass().getName();
377    }
378
379    /**
380     * A default handler for resource requests. Checks that the request
381     * is directed at this web console component, and calls 
382     * {@link #doGetResource}.
383     * 
384     * @param event the resource request event
385     * @param channel the channel that the request was recived on
386     */
387    @Handler
388    public final void onConletResourceRequest(
389            ConletResourceRequest event, IOSubchannel channel) {
390        // For me?
391        if (!event.conletClass().equals(type())) {
392            return;
393        }
394        doGetResource(event, channel);
395    }
396
397    /**
398     * The default implementation searches for a file with the 
399     * requested resource URI in the web console component's class 
400     * path and sets its {@link URL} as result if found.
401     * 
402     * @param event the event. The result will be set to
403     * `true` on success
404     * @param channel the channel
405     */
406    protected void doGetResource(ConletResourceRequest event,
407            IOSubchannel channel) {
408        URL resourceUrl = this.getClass().getResource(
409            event.resourceUri().getPath());
410        if (resourceUrl == null) {
411            return;
412        }
413        event.setResult(new ResourceByUrl(event, resourceUrl));
414        event.stop();
415    }
416
417    /**
418     * Provides a resource bundle for localization.
419     * The default implementation looks up a bundle using the
420     * package name plus "l10n" as base name. Note that the bundle 
421     * returned for a given locale may be the fallback bundle.
422     * 
423     * @return the resource bundle
424     */
425    protected ResourceBundle resourceBundle(Locale locale) {
426        return ResourceBundle.getBundle(
427            getClass().getPackage().getName() + ".l10n", locale,
428            getClass().getClassLoader(),
429            ResourceBundle.Control.getNoFallbackControl(
430                ResourceBundle.Control.FORMAT_DEFAULT));
431    }
432
433    /**
434     * Returns bundles for the given locales. 
435     * 
436     * The default implementation uses {@link #resourceBundle(Locale)} 
437     * to lookup the bundles. The method is guaranteed to return a 
438     * bundle for each requested locale even if it is only the fallback 
439     * bundle. The evaluated results are cached for the conlet class.
440     *
441     * @param toGet the locales to get bundles for
442     * @return the map with locales and bundles
443     */
444    protected Map<Locale, ResourceBundle> l10nBundles(Set<Locale> toGet) {
445        Map<Locale, ResourceBundle> result = new HashMap<>();
446        for (Locale locale : toGet) {
447            ResourceBundle bundle = l10nBundles
448                .computeIfAbsent(getClass(), cls -> new HashMap<>())
449                .computeIfAbsent(locale, l -> resourceBundle(locale));
450            result.put(locale, bundle);
451        }
452        return Collections.unmodifiableMap(result);
453    }
454
455    /**
456     * Provides localizations for the given key for all requested locales.
457     * 
458     * The default implementation uses {@link #l10nBundles(Set)} to obtain
459     * the localizations.
460     *
461     * @param locales the requested locales
462     * @param key the key
463     * @return the result
464     */
465    protected Map<Locale, String> localizations(Set<Locale> locales,
466            String key) {
467        Map<Locale, String> result = new HashMap<>();
468        Map<Locale, ResourceBundle> bundles = l10nBundles(locales);
469        for (Map.Entry<Locale, ResourceBundle> entry : bundles.entrySet()) {
470            result.put(entry.getKey(), entry.getValue().getString(key));
471        }
472        return result;
473    }
474
475    /**
476     * Returns the supported locales and the associated bundles.
477     * 
478     * The default implementation invokes {@link #resourceBundle(Locale)}
479     * with all available locales and drops results with fallback bundles.
480     * The evaluated results are cached for the conlet class.
481     *
482     * @return the result
483     */
484    protected Map<Locale, ResourceBundle> supportedLocales() {
485        return supportedLocales.computeIfAbsent(getClass(), cls -> {
486            ResourceBundle.clearCache(cls.getClassLoader());
487            Map<Locale, ResourceBundle> bundles = new HashMap<>();
488            for (Locale locale : Locale.getAvailableLocales()) {
489                if (locale.getLanguage().equals("")) {
490                    continue;
491                }
492                ResourceBundle bundle = resourceBundle(locale);
493                if (bundle.getLocale().equals(locale)) {
494                    bundles.put(locale, bundle);
495                }
496            }
497            return bundles;
498        });
499    }
500
501    /**
502     * Generates a new unique web console component id.
503     * 
504     * @return the web console component id
505     */
506    protected String generateConletId() {
507        return UUID.randomUUID().toString();
508    }
509
510    /**
511     * Returns the tracked sessions and conlet ids as map.
512     * 
513     * If you need a particular session's web console component ids, you 
514     * should prefer {@link #conletIds(ConsoleSession)} over calling
515     * this method with `get(consoleSession)` appended.
516     * 
517     * @return the result
518     */
519    protected Map<ConsoleSession, Set<String>> conletIdsByConsoleSession() {
520        return conletInfosByConsoleSession.entrySet().stream()
521            .collect(Collectors.toMap(Entry::getKey,
522                e -> new HashSet<>(e.getValue().keySet())));
523    }
524
525    /**
526     * Returns the tracked sessions. This is effectively
527     * `conletInfosByConsoleSession().keySet()` converted to
528     * an array. This representation is especially useful 
529     * when the web console sessions are used as argument for 
530     * {@link #fire(Event, Channel...)}.
531     *
532     * @return the web console sessions
533     */
534    protected ConsoleSession[] trackedSessions() {
535        Set<ConsoleSession> sessions = new HashSet<>(
536            conletInfosByConsoleSession.keySet());
537        return sessions.toArray(new ConsoleSession[0]);
538    }
539
540    /**
541     * Returns the set of web console component ids associated with the 
542     * console session as a {@link Set}. If no web console components 
543     * have registered yet, an empty set is returned.
544     * 
545     * @param consoleSession the console session
546     * @return the set
547     */
548    protected Set<String> conletIds(ConsoleSession consoleSession) {
549        return new HashSet<>(conletInfosByConsoleSession.getOrDefault(
550            consoleSession, Collections.emptyMap()).keySet());
551    }
552
553    /**
554     * Track the given web console component from the given session. 
555     * This is invoked by 
556     * {@link #onAddConletRequest(AddConletRequest, ConsoleSession)} and
557     * needs only be used if 
558     * {@link #onAddConletRequest(AddConletRequest, ConsoleSession)} 
559     * or {@link #onRenderConletRequest(RenderConletRequest, ConsoleSession)}
560     * is overridden.
561     *
562     * @param consoleSession the web console session
563     * @param conletId the conlet id
564     * @param info the info to be added if untracked. If `null`
565     * a new {@link ConletTrackingInfo} is added.
566     * @return the conlet tracking info
567     */
568    protected ConletTrackingInfo trackConlet(ConsoleSession consoleSession,
569            String conletId, ConletTrackingInfo info) {
570        Map<String, ConletTrackingInfo> infos
571            = conletInfosByConsoleSession.computeIfAbsent(consoleSession,
572                newKey -> new ConcurrentHashMap<>());
573        ConletTrackingInfo result = infos.computeIfAbsent(conletId,
574            key -> info != null ? info : new ConletTrackingInfo(conletId));
575        updateRefresh();
576        return result;
577    }
578
579    /**
580     * Puts the given web console component state in the session using the 
581     * {@link #type()} and the given web console component id as keys.
582     * 
583     * @param session the session to use
584     * @param conletId the web console component id
585     * @param conletState the web console component state
586     * @return the portlweb console componentet state
587     */
588    @SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" })
589    protected Serializable putInSession(Session session, String conletId,
590            Serializable conletState) {
591        ((Map<Serializable,
592                Map<Serializable, Map<String, Serializable>>>) (Map<
593                        Serializable, ?>) session)
594                            .computeIfAbsent(AbstractConlet.class,
595                                newKey -> new ConcurrentHashMap<>())
596                            .computeIfAbsent(type(),
597                                newKey -> new ConcurrentHashMap<>())
598                            .put(conletId, conletState);
599        return conletState;
600    }
601
602    /**
603     * Puts the given web console component instance state in the browser
604     * session associated with the channel, using  
605     * {@link #type()} and the web console component id from the model.
606     *
607     * @param session the session to use
608     * @param conletModel the web console component model
609     * @return the web console component model
610     */
611    protected <T extends ConletBaseModel> T putInSession(
612            Session session, T conletModel) {
613        putInSession(session, conletModel.getConletId(), conletModel);
614        return conletModel;
615    }
616
617    /**
618     * Returns the state of this web console component's type 
619     * with the given id from the session.
620     *
621     * @param session the session to use
622     * @param conletId the web console component id
623     * @return the web console component state
624     */
625    @SuppressWarnings("unchecked")
626    protected Optional<S> stateFromSession(Session session, String conletId) {
627        return Optional.ofNullable(
628            ((Map<Serializable,
629                    Map<Serializable, Map<String, S>>>) (Map<Serializable,
630                            ?>) session)
631                                .computeIfAbsent(AbstractConlet.class,
632                                    newKey -> new HashMap<>())
633                                .computeIfAbsent(type(),
634                                    newKey -> new HashMap<>())
635                                .get(conletId));
636    }
637
638    /**
639     * Returns all web console component states of this web console 
640     * component's type from the session.
641     *
642     * @param channel the channel, used to access the session
643     * @return the states
644     */
645    @SuppressWarnings("unchecked")
646    protected Collection<S> statesFromSession(IOSubchannel channel) {
647        return channel.associated(Session.class)
648            .map(session -> ((Map<Serializable,
649                    Map<Serializable, Map<String, S>>>) (Map<Serializable,
650                            ?>) session)
651                                .computeIfAbsent(AbstractConlet.class,
652                                    newKey -> new HashMap<>())
653                                .computeIfAbsent(type(),
654                                    newKey -> new HashMap<>())
655                                .values())
656            .orElseThrow(
657                () -> new IllegalStateException("Session is missing."));
658    }
659
660    /**
661     * Removes the web console component state of the 
662     * web console component with the given id from the session. 
663     * 
664     * @param session the session to use
665     * @param conletId the web console component id
666     * @return the removed state if state existed
667     */
668    @SuppressWarnings("unchecked")
669    protected Optional<S> removeState(Session session, String conletId) {
670        S state = ((Map<Serializable,
671                Map<Serializable, Map<String, S>>>) (Map<Serializable,
672                        ?>) session)
673                            .computeIfAbsent(AbstractConlet.class,
674                                newKey -> new HashMap<>())
675                            .computeIfAbsent(type(), newKey -> new HashMap<>())
676                            .remove(conletId);
677        return Optional.ofNullable(state);
678    }
679
680    /**
681     * Checks if the request applies to this component. If so, stops the event,
682     * and calls {@link #doAddConlet}. 
683     *
684     * @param event the event
685     * @param consoleSession the channel
686     * @throws Exception the exception
687     */
688    @Handler
689    @SuppressWarnings({ "PMD.SignatureDeclareThrowsException",
690        "PMD.AvoidDuplicateLiterals" })
691    public final void onAddConletRequest(AddConletRequest event,
692            ConsoleSession consoleSession) throws Exception {
693        if (!event.conletType().equals(type())) {
694            return;
695        }
696        event.stop();
697        ConletTrackingInfo info = doAddConlet(event, consoleSession);
698        event.setResult(info.conletId());
699        trackConlet(consoleSession, info.conletId(), info);
700    }
701
702    /**
703     * Called by {@link #onAddConletRequest} to complete adding the 
704     * web console component. If the web console component has associated 
705     * state, the implementation should
706     * call {@link #putInSession(Session, String, Serializable)} to create
707     * the state and put it in the session.
708     * 
709     * @param event the event
710     * @param consoleSession the channel
711     * @return the tracking info of the new web console component
712     */
713    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
714    protected abstract ConletTrackingInfo doAddConlet(AddConletRequest event,
715            ConsoleSession consoleSession) throws Exception;
716
717    /**
718     * Checks if the request applies to this component. If so, stops 
719     * the event. If the conlet is completely removed from the browser,
720     * removes the web console component state from the 
721     * browser session. In all cases, it calls {@link #doConletDeleted} 
722     * with the state.
723     * 
724     * @param event the event
725     * @param consoleSession the web console session
726     * @throws Exception the exception
727     */
728    @Handler
729    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
730    public final void onConletDeleted(ConletDeleted event,
731            ConsoleSession consoleSession) throws Exception {
732        Optional<S> optConletState = stateFromSession(
733            consoleSession.browserSession(), event.conletId());
734        if (!optConletState.isPresent()) {
735            return;
736        }
737        String conletId = event.conletId();
738        if (event.renderModes().isEmpty()) {
739            removeState(consoleSession.browserSession(), conletId);
740            for (Iterator<Entry<ConsoleSession, Map<String,
741                    ConletTrackingInfo>>> csi = conletInfosByConsoleSession
742                        .entrySet().iterator();
743                    csi.hasNext();) {
744                Map<String, ConletTrackingInfo> infos = csi.next().getValue();
745                infos.remove(conletId);
746                if (infos.isEmpty()) {
747                    csi.remove();
748                }
749            }
750            updateRefresh();
751        } else {
752            trackConlet(consoleSession, conletId, null)
753                .removeModes(event.renderModes());
754        }
755        event.stop();
756        doConletDeleted(event, consoleSession, event.conletId(),
757            optConletState.get());
758    }
759
760    /**
761     * Called by {@link #onConletDeleted} to propagate the event to derived
762     * classes.
763     * 
764     * @param event the event
765     * @param channel the channel
766     * @param conletId the web console component id
767     * @param conletState the web console component state
768     */
769    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
770    protected void doConletDeleted(ConletDeleted event,
771            ConsoleSession channel, String conletId, S conletState)
772            throws Exception {
773    }
774
775    /**
776     * Checks if the request applies to this component by calling
777     * {@link #stateFromSession(Session, String)}. If a model
778     * is found, sets the event's result to `true`, stops the event, and 
779     * calls {@link #doRenderConlet} with the state information. 
780     * 
781     * Some web console components that do not persist their models 
782     * between sessions (e.g. because the model only references data 
783     * maintained elsewhere) should override 
784     * {@link #stateFromSession(Session, String)}
785     * in such a way that it creates the requested model if it doesn't 
786     * exist yet.
787     *
788     * @param event the event
789     * @param consoleSession the web console session
790     * @throws Exception the exception
791     */
792    @Handler
793    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
794    public final void onRenderConletRequest(RenderConletRequest event,
795            ConsoleSession consoleSession) throws Exception {
796        Optional<S> optConletState = stateFromSession(
797            consoleSession.browserSession(), event.conletId());
798        if (!optConletState.isPresent()) {
799            return;
800        }
801        event.setResult(true);
802        event.stop();
803        Set<RenderMode> rendered = doRenderConlet(
804            event, consoleSession, event.conletId(), optConletState.get());
805        trackConlet(consoleSession, event.conletId(), null).addModes(rendered);
806    }
807
808    /**
809     * Called by 
810     * {@link #onRenderConletRequest(RenderConletRequest, ConsoleSession)} 
811     * to complete rendering the web console component.
812     *
813     * @param event the event
814     * @param channel the channel
815     * @param conletId the web console component id
816     * @param conletState the web console component state
817     * @return the rendered modes
818     * @throws Exception the exception
819     */
820    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
821    protected abstract Set<RenderMode> doRenderConlet(RenderConletRequest event,
822            ConsoleSession channel, String conletId, S conletState)
823            throws Exception;
824
825    /**
826     * Invokes {@link #doSetLocale(SetLocale, ConsoleSession, String)}
827     * for each web console component in the console session.
828     * 
829     * If the vent has the reload flag set, does nothing.
830     * 
831     * The default implementation fires a 
832     *
833     * @param event the event
834     * @param consoleSession the web console session
835     * @throws Exception the exception
836     */
837    @Handler
838    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
839    public void onSetLocale(SetLocale event, ConsoleSession consoleSession)
840            throws Exception {
841        if (event.reload()) {
842            return;
843        }
844        for (String conletId : conletIds(consoleSession)) {
845            if (!doSetLocale(event, consoleSession, conletId)) {
846                event.forceReload();
847                break;
848            }
849        }
850    }
851
852    /**
853     * Called by {@link #onSetLocale(SetLocale, ConsoleSession)} for
854     * each web console component in the console session. Derived 
855     * classes must send events for updating the representation to 
856     * match the new locale.
857     * 
858     * If the method returns `false` this indicates that the representation 
859     * cannot be updated without reloading the web console page.
860     * 
861     * The default implementation fires a {@link RenderConletRequest}
862     * with tracked render modes (one of or both {@link RenderMode#Preview}
863     * and {@link RenderMode#View}), thus updating the known representations.
864     * (Assuming that "Edit" and "Help" modes are represented with modal 
865     * dialogs and therefore locale changes aren't possible while these are 
866     * open.) 
867     *
868     * @param event the event
869     * @param channel the channel
870     * @param conletId the web console component id
871     * @return true, if the locale could be changed
872     * @throws Exception the exception
873     */
874    protected boolean doSetLocale(SetLocale event, ConsoleSession channel,
875            String conletId) throws Exception {
876        fire(new RenderConletRequest(event.renderSupport(), conletId,
877            trackConlet(channel, conletId, null).renderedAs()),
878            channel);
879        return true;
880    }
881
882    /**
883     * Checks if the request applies to this component by calling
884     * {@link #stateFromSession(Session, String)}. If a model
885     * is found, calls {@link #doNotifyConletModel} with the state 
886     * information. 
887     *
888     * @param event the event
889     * @param channel the channel
890     * @throws Exception the exception
891     */
892    @Handler
893    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
894    public final void onNotifyConletModel(NotifyConletModel event,
895            ConsoleSession channel) throws Exception {
896        Optional<S> optConletState
897            = stateFromSession(channel.browserSession(), event.conletId());
898        if (!optConletState.isPresent()) {
899            return;
900        }
901        doNotifyConletModel(event, channel, optConletState.get());
902    }
903
904    /**
905     * Called by {@link #onNotifyConletModel} to complete handling
906     * the notification. The default implementation does nothing.
907     * 
908     * @param event the event
909     * @param channel the channel
910     * @param conletState the web console component state
911     */
912    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
913    protected void doNotifyConletModel(NotifyConletModel event,
914            ConsoleSession channel, S conletState) throws Exception {
915        // Default is to do nothing.
916    }
917
918    /**
919     * Removes the {@link ConsoleSession} from the set of tracked sessions.
920     * If derived web console components need to perform extra actions when a
921     * console session is closed, they have to override 
922     * {@link #afterOnClosed(Closed, ConsoleSession)}.
923     * 
924     * @param event the closed event
925     * @param consoleSession the web console session
926     */
927    @Handler
928    public final void onClosed(Closed event, ConsoleSession consoleSession) {
929        conletInfosByConsoleSession.remove(consoleSession);
930        updateRefresh();
931        afterOnClosed(event, consoleSession);
932    }
933
934    /**
935     * Invoked by {@link #onClosed(Closed, ConsoleSession)} after
936     * the web console session has been removed from the set of
937     * tracked sessions. The default implementation does
938     * nothing.
939     * 
940     * @param event the closed event
941     * @param consoleSession the web console session
942     */
943    protected void afterOnClosed(Closed event, ConsoleSession consoleSession) {
944        // Default is to do nothing.
945    }
946
947    /**
948     * The information tracked about web console components that are
949     * used by the console. It includes the component's id and the
950     * currently rendered views (only preview and view are tracked,
951     * with "deletable preview" mapped to "preview").
952     */
953    protected static class ConletTrackingInfo {
954        private String conletId;
955        private Set<RenderMode> renderedAs;
956
957        /**
958         * Instantiates a new conlet tracking info.
959         *
960         * @param conletId the conlet id
961         */
962        public ConletTrackingInfo(String conletId) {
963            this.conletId = conletId;
964            renderedAs = new HashSet<>();
965        }
966
967        /**
968         * Returns the conlet id.
969         *
970         * @return the id
971         */
972        public String conletId() {
973            return conletId;
974        }
975
976        /**
977         * The render modes current used.
978         *
979         * @return the render modes
980         */
981        public Set<RenderMode> renderedAs() {
982            return renderedAs;
983        }
984
985        /**
986         * Adds the given modes.
987         *
988         * @param modes the modes
989         * @return the conlet tracking info
990         */
991        public ConletTrackingInfo addModes(Set<RenderMode> modes) {
992            if (modes.contains(RenderMode.Preview)) {
993                renderedAs.add(RenderMode.Preview);
994            }
995            if (modes.contains(RenderMode.View)) {
996                renderedAs.add(RenderMode.View);
997            }
998            return this;
999        }
1000
1001        /**
1002         * Removes the given modes.
1003         *
1004         * @param modes the modes
1005         * @return the conlet tracking info
1006         */
1007        public ConletTrackingInfo removeModes(Set<RenderMode> modes) {
1008            renderedAs.removeAll(modes);
1009            return this;
1010        }
1011
1012        @Override
1013        public int hashCode() {
1014            return conletId.hashCode();
1015        }
1016
1017        @Override
1018        public boolean equals(Object obj) {
1019            if (this == obj) {
1020                return true;
1021            }
1022            if (obj == null) {
1023                return false;
1024            }
1025            if (getClass() != obj.getClass()) {
1026                return false;
1027            }
1028            ConletTrackingInfo other = (ConletTrackingInfo) obj;
1029            if (conletId == null) {
1030                if (other.conletId != null) {
1031                    return false;
1032                }
1033            } else if (!conletId.equals(other.conletId)) {
1034                return false;
1035            }
1036            return true;
1037        }
1038    }
1039
1040    /**
1041     * Defines the web console component model following the 
1042     * JavaBean conventions.
1043     * 
1044     * Conlet models should follow these conventions because
1045     * many template engines rely on them and to support serialization
1046     * to portable formats. 
1047     */
1048    @SuppressWarnings("serial")
1049    public static class ConletBaseModel implements Serializable {
1050
1051        protected String conletId;
1052
1053        /**
1054         * Creates a new model with the given type and id.
1055         * 
1056         * @param conletId the web console component id
1057         */
1058        @ConstructorProperties({ "conletId" })
1059        public ConletBaseModel(String conletId) {
1060            this.conletId = conletId;
1061        }
1062
1063        /**
1064         * Returns the web console component id.
1065         * 
1066         * @return the web console component id
1067         */
1068        public String getConletId() {
1069            return conletId;
1070        }
1071
1072        /**
1073         * Hash code.
1074         *
1075         * @return the int
1076         */
1077        /*
1078         * (non-Javadoc)
1079         * 
1080         * @see java.lang.Object#hashCode()
1081         */
1082        @Override
1083        @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
1084        public int hashCode() {
1085            @SuppressWarnings("PMD.AvoidFinalLocalVariable")
1086            final int prime = 31;
1087            int result = 1;
1088            result = prime * result
1089                + ((conletId == null) ? 0 : conletId.hashCode());
1090            return result;
1091        }
1092
1093        /**
1094         * Two objects are equal if they have equal web console component ids.
1095         * 
1096         * @param obj the other object
1097         * @return the result
1098         */
1099        @Override
1100        public boolean equals(Object obj) {
1101            if (this == obj) {
1102                return true;
1103            }
1104            if (obj == null) {
1105                return false;
1106            }
1107            if (getClass() != obj.getClass()) {
1108                return false;
1109            }
1110            ConletBaseModel other = (ConletBaseModel) obj;
1111            if (conletId == null) {
1112                if (other.conletId != null) {
1113                    return false;
1114                }
1115            } else if (!conletId.equals(other.conletId)) {
1116                return false;
1117            }
1118            return true;
1119        }
1120    }
1121
1122    /**
1123     * Send to the web console page for adding or updating a complete web 
1124     * console component representation.
1125     */
1126    public static class RenderConletFromReader extends RenderConlet {
1127
1128        private final Future<String> content;
1129
1130        /**
1131         * Creates a new event.
1132         *
1133         * @param request the request
1134         * @param conletType the conlet type
1135         * @param conletId the id of the web console component
1136         * @param contentReader the content reader
1137         */
1138        public RenderConletFromReader(RenderConletRequestBase<?> request,
1139                String conletType, String conletId, Reader contentReader) {
1140            super(conletType, conletId);
1141            // Start to prepare the content immediately and concurrently.
1142            content = request.processedBy().map(pby -> pby.executorService())
1143                .orElse(Components.defaultExecutorService()).submit(() -> {
1144                    StringWriter content = new StringWriter();
1145                    CharBuffer buffer = CharBuffer.allocate(8192);
1146                    try (Reader rdr = new BufferedReader(contentReader)) {
1147                        while (true) {
1148                            if (rdr.read(buffer) < 0) {
1149                                break;
1150                            }
1151                            buffer.flip();
1152                            content.append(buffer);
1153                            buffer.clear();
1154                        }
1155                    } catch (IOException e) {
1156                        throw new IllegalStateException(e);
1157                    }
1158                    return content.toString();
1159                });
1160
1161        }
1162
1163        /**
1164         * Content.
1165         *
1166         * @return the future
1167         */
1168        @Override
1169        public Future<String> content() {
1170            return content;
1171        }
1172    }
1173}