001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2017-2018 Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 * 
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 * 
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jgrapes.webconsole.base;
020
021import java.io.IOException;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Optional;
027import org.jdrupes.json.JsonBeanDecoder;
028import org.jdrupes.json.JsonBeanEncoder;
029import org.jdrupes.json.JsonDecodeException;
030import org.jdrupes.json.JsonObject;
031import org.jgrapes.core.Channel;
032import org.jgrapes.core.Component;
033import org.jgrapes.core.annotation.Handler;
034import org.jgrapes.http.Session;
035import org.jgrapes.io.IOSubchannel;
036import org.jgrapes.util.events.KeyValueStoreData;
037import org.jgrapes.util.events.KeyValueStoreQuery;
038import org.jgrapes.util.events.KeyValueStoreUpdate;
039import org.jgrapes.webconsole.base.Conlet.RenderMode;
040import org.jgrapes.webconsole.base.events.ConsoleLayoutChanged;
041import org.jgrapes.webconsole.base.events.ConsolePrepared;
042import org.jgrapes.webconsole.base.events.ConsoleReady;
043import org.jgrapes.webconsole.base.events.LastConsoleLayout;
044import org.jgrapes.webconsole.base.events.RenderConletRequest;
045
046/**
047 * A component that restores the console layout
048 * using key/value events for persisting the data between sessions.
049 * 
050 * ![Boot Event Sequence](KVPPBootSeq.svg)
051 * 
052 * This component requires another component that handles the key/value
053 * store events ({@link KeyValueStoreUpdate}, {@link KeyValueStoreQuery})
054 * used by this component for implementing persistence. When the web console 
055 * becomesready, this policy sends a query for the persisted data.
056 * 
057 * When the web console has been prepared, the policy sends the last layout
058 * as retrieved from persistent storage to the web console and then generates
059 * render events for all web console components contained in this layout.
060 * 
061 * Each time the layout is changed in the web console, the web console sends 
062 * the new layout data and this component updates the persistent storage
063 * accordingly.
064 * 
065 * @startuml KVPPBootSeq.svg
066 * hide footbox
067 * 
068 * Browser -> WebConsole: "consoleReady"
069 * activate WebConsole
070 * WebConsole -> KVStoreBasedConsolePolicy: ConsoleReady
071 * deactivate WebConsole
072 * activate KVStoreBasedConsolePolicy
073 * KVStoreBasedConsolePolicy -> "KV Store": KeyValueStoreQuery
074 * activate "KV Store"
075 * "KV Store" -> KVStoreBasedConsolePolicy: KeyValueStoreData
076 * deactivate "KV Store"
077 * deactivate KVStoreBasedConsolePolicy
078 * 
079 * actor System
080 * System -> KVStoreBasedConsolePolicy: ConsolePrepared
081 * activate KVStoreBasedConsolePolicy
082 * KVStoreBasedConsolePolicy -> WebConsole: LastConsoleLayout
083 * activate WebConsole
084 * WebConsole -> Browser: "lastConsoleLayout"
085 * deactivate WebConsole
086 * loop for all conlets to be displayed
087 *     KVStoreBasedConsolePolicy -> ConletX: RenderConletRequest
088 *     activate ConletX
089 *     ConletX -> WebConsole: RenderConlet
090 *     deactivate ConletX
091 *     activate WebConsole
092 *     WebConsole -> Browser: "renderConlet"
093 *     deactivate WebConsole
094 * end
095 * deactivate KVStoreBasedConsolePolicy
096 * 
097 * Browser -> WebConsole: "consoleLayout"
098 * activate WebConsole
099 * WebConsole -> KVStoreBasedConsolePolicy: ConsoleLayoutChanged
100 * deactivate WebConsole
101 * activate KVStoreBasedConsolePolicy
102 * KVStoreBasedConsolePolicy -> "KV Store": KeyValueStoreUpdate
103 * deactivate KVStoreBasedConsolePolicy
104 * 
105 * @enduml
106 */
107public class KVStoreBasedConsolePolicy extends Component {
108
109    /**
110     * Creates a new component with its channel set to
111     * itself.
112     */
113    public KVStoreBasedConsolePolicy() {
114        // Everything done by super.
115    }
116
117    /**
118     * Creates a new component with its channel set to the given channel.
119     * 
120     * @param componentChannel
121     */
122    public KVStoreBasedConsolePolicy(Channel componentChannel) {
123        super(componentChannel);
124    }
125
126    /**
127     * Intercept the {@link ConsoleReady} event. Request the 
128     * session data from the key/value store and resume.
129     * 
130     * @param event
131     * @param channel
132     * @throws InterruptedException
133     */
134    @Handler
135    public void onConsoleReady(ConsoleReady event, ConsoleSession channel)
136            throws InterruptedException {
137        ConsoleSessionDataStore sessionDs = channel.associated(
138            ConsoleSessionDataStore.class,
139            () -> new ConsoleSessionDataStore(channel.browserSession()));
140        sessionDs.onConsoleReady(event, channel);
141    }
142
143    /**
144     * Handle returned data.
145     *
146     * @param event the event
147     * @param channel the channel
148     * @throws JsonDecodeException the json decode exception
149     */
150    @Handler
151    public void onKeyValueStoreData(
152            KeyValueStoreData event, ConsoleSession channel)
153            throws JsonDecodeException {
154        Optional<ConsoleSessionDataStore> optSessionDs
155            = channel.associated(ConsoleSessionDataStore.class);
156        if (optSessionDs.isPresent()) {
157            optSessionDs.get().onKeyValueStoreData(event, channel);
158        }
159    }
160
161    /**
162     * Handle web console page loaded.
163     *
164     * @param event the event
165     * @param channel the channel
166     */
167    @Handler
168    public void onConsolePrepared(
169            ConsolePrepared event, ConsoleSession channel) {
170        channel.associated(ConsoleSessionDataStore.class).ifPresent(
171            psess -> psess.onConsolePrepared(event, channel));
172    }
173
174    /**
175     * Handle changed layout.
176     *
177     * @param event the event
178     * @param channel the channel
179     * @throws IOException Signals that an I/O exception has occurred.
180     */
181    @Handler
182    public void onConsoleLayoutChanged(ConsoleLayoutChanged event,
183            ConsoleSession channel) throws IOException {
184        Optional<ConsoleSessionDataStore> optDs = channel.associated(
185            ConsoleSessionDataStore.class);
186        if (optDs.isPresent()) {
187            optDs.get().onConsoleLayoutChanged(event, channel);
188        }
189    }
190
191    /**
192     * Stores the data for the web console session.
193     */
194    @SuppressWarnings("PMD.CommentRequired")
195    private class ConsoleSessionDataStore {
196
197        private final String storagePath;
198        private Map<String, Object> persisted;
199
200        public ConsoleSessionDataStore(Session session) {
201            storagePath = "/"
202                + WebConsoleUtils.userFromSession(session)
203                    .map(UserPrincipal::toString).orElse("")
204                + "/" + KVStoreBasedConsolePolicy.class.getName();
205        }
206
207        public void onConsoleReady(ConsoleReady event, IOSubchannel channel)
208                throws InterruptedException {
209            if (persisted != null) {
210                return;
211            }
212            KeyValueStoreQuery query = new KeyValueStoreQuery(
213                storagePath, channel);
214            fire(query, channel);
215        }
216
217        public void onKeyValueStoreData(
218                KeyValueStoreData event, IOSubchannel channel)
219                throws JsonDecodeException {
220            if (!event.event().query().equals(storagePath)) {
221                return;
222            }
223            String data = event.data().get(storagePath);
224            if (data != null) {
225                JsonBeanDecoder decoder = JsonBeanDecoder.create(data);
226                @SuppressWarnings({ "unchecked", "PMD.LooseCoupling" })
227                Class<Map<String, Object>> cls
228                    = (Class<Map<String, Object>>) (Class<?>) HashMap.class;
229                persisted = decoder.readObject(cls);
230            }
231        }
232
233        @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
234            "PMD.AvoidInstantiatingObjectsInLoops" })
235        public void onConsolePrepared(
236                ConsolePrepared event, IOSubchannel channel) {
237            if (persisted == null) {
238                // Retrieval was not successful
239                persisted = new HashMap<>();
240            }
241            // Make sure data is consistent
242            @SuppressWarnings("unchecked")
243            List<String> previewLayout = (List<String>) persisted
244                .computeIfAbsent("previewLayout",
245                    newKey -> {
246                        return Collections.emptyList();
247                    });
248            @SuppressWarnings("unchecked")
249            List<String> tabsLayout = (List<String>) persisted.computeIfAbsent(
250                "tabsLayout", newKey -> {
251                    return Collections.emptyList();
252                });
253            JsonObject xtraInfo = (JsonObject) persisted.computeIfAbsent(
254                "xtraInfo", newKey -> {
255                    return JsonObject.create();
256                });
257
258            // Update layout
259            channel.respond(new LastConsoleLayout(
260                previewLayout, tabsLayout, xtraInfo));
261
262            // Restore conlets
263            for (String conletId : tabsLayout) {
264                fire(new RenderConletRequest(
265                    event.event().renderSupport(), conletId,
266                    RenderMode.asSet(RenderMode.View)), channel);
267            }
268            for (String conletId : previewLayout) {
269                fire(new RenderConletRequest(
270                    event.event().renderSupport(), conletId,
271                    RenderMode.asSet(RenderMode.Preview,
272                        RenderMode.Foreground)),
273                    channel);
274            }
275        }
276
277        public void onConsoleLayoutChanged(ConsoleLayoutChanged event,
278                IOSubchannel channel) throws IOException {
279            persisted.put("previewLayout", event.previewLayout());
280            persisted.put("tabsLayout", event.tabsLayout());
281            persisted.put("xtraInfo", event.xtraInfo());
282
283            // Now store.
284            JsonBeanEncoder encoder = JsonBeanEncoder.create();
285            encoder.writeObject(persisted);
286            fire(new KeyValueStoreUpdate()
287                .update(storagePath, encoder.toJson()), channel);
288        }
289
290    }
291}