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 com.fasterxml.jackson.databind.ObjectMapper;
022import com.fasterxml.jackson.databind.json.JsonMapper;
023import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
024import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
025import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
026import java.io.IOException;
027import java.util.Collections;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Optional;
032import org.jgrapes.core.Channel;
033import org.jgrapes.core.Component;
034import org.jgrapes.core.Event;
035import org.jgrapes.core.annotation.Handler;
036import org.jgrapes.http.Session;
037import org.jgrapes.io.IOSubchannel;
038import org.jgrapes.util.events.KeyValueStoreQuery;
039import org.jgrapes.util.events.KeyValueStoreUpdate;
040import org.jgrapes.webconsole.base.Conlet.RenderMode;
041import org.jgrapes.webconsole.base.events.ConsoleLayoutChanged;
042import org.jgrapes.webconsole.base.events.ConsolePrepared;
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 * <img src="KVPPBootSeq.svg" alt="Boot Event Sequence">
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 * actor System
069 * System -> KVStoreBasedConsolePolicy: ConsolePrepared
070 * activate KVStoreBasedConsolePolicy
071 * KVStoreBasedConsolePolicy -> "KV Store": KeyValueStoreQuery
072 * deactivate KVStoreBasedConsolePolicy
073 * activate "KV Store"
074 * "KV Store" -> KVStoreBasedConsolePolicy: KeyValueStoreData
075 * deactivate "KV Store"
076 * activate KVStoreBasedConsolePolicy
077 * KVStoreBasedConsolePolicy -> WebConsole: LastConsoleLayout
078 * activate WebConsole
079 * WebConsole -> Browser: "lastConsoleLayout"
080 * deactivate WebConsole
081 * loop for all conlets to be displayed
082 *     KVStoreBasedConsolePolicy -> ConletX: RenderConletRequest
083 *     activate ConletX
084 *     ConletX -> WebConsole: RenderConlet
085 *     deactivate ConletX
086 *     activate WebConsole
087 *     WebConsole -> Browser: "renderConlet"
088 *     deactivate WebConsole
089 * end
090 * deactivate KVStoreBasedConsolePolicy
091 * 
092 * Browser -> WebConsole: "consoleLayout"
093 * activate WebConsole
094 * WebConsole -> KVStoreBasedConsolePolicy: ConsoleLayoutChanged
095 * deactivate WebConsole
096 * activate KVStoreBasedConsolePolicy
097 * KVStoreBasedConsolePolicy -> "KV Store": KeyValueStoreUpdate
098 * deactivate KVStoreBasedConsolePolicy
099 * activate "KV Store"
100 * deactivate "KV Store"
101 * 
102 * @enduml
103 */
104public class KVStoreBasedConsolePolicy extends Component {
105
106    /** The mapper. */
107    @SuppressWarnings("PMD.FieldNamingConventions")
108    protected static final ObjectMapper mapper = JsonMapper.builder()
109        .addModule(new ParameterNamesModule()).addModule(new Jdk8Module())
110        .addModule(new JavaTimeModule()).build();
111
112    /**
113     * Creates a new component with its channel set to
114     * itself.
115     */
116    public KVStoreBasedConsolePolicy() {
117        // Everything done by super.
118    }
119
120    /**
121     * Creates a new component with its channel set to the given channel.
122     * 
123     * @param componentChannel
124     */
125    public KVStoreBasedConsolePolicy(Channel componentChannel) {
126        super(componentChannel);
127    }
128
129    /**
130     * Create browser session scoped storage and forward event to it.
131     *
132     * @param event the event
133     * @param channel the channel
134     */
135    @Handler
136    public void onConsolePrepared(
137            ConsolePrepared event, ConsoleConnection channel) {
138        ((KVStoredLayoutData) channel.session().transientData()
139            .computeIfAbsent(KVStoredLayoutData.class,
140                key -> new KVStoredLayoutData(channel.session())))
141                    .onConsolePrepared(event, channel);
142    }
143
144    /**
145     * Forward layout changed event to browser session scoped storage.
146     *
147     * @param event the event
148     * @param channel the channel
149     * @throws IOException Signals that an I/O exception has occurred.
150     */
151    @Handler
152    public void onConsoleLayoutChanged(ConsoleLayoutChanged event,
153            ConsoleConnection channel) throws IOException {
154        Optional<KVStoredLayoutData> optDs = Optional.ofNullable(
155            (KVStoredLayoutData) channel.session().transientData()
156                .get(KVStoredLayoutData.class));
157        if (optDs.isPresent()) {
158            optDs.get().onConsoleLayoutChanged(event, channel);
159        }
160    }
161
162    /**
163     * Caches the data in the session.
164     */
165    @SuppressWarnings("PMD.CommentRequired")
166    private class KVStoredLayoutData {
167
168        private final String storagePath;
169        private Map<String, Object> persisted;
170
171        public KVStoredLayoutData(Session session) {
172            storagePath = "/"
173                + WebConsoleUtils.userFromSession(session)
174                    .map(ConsoleUser::getName).orElse("")
175                + "/" + KVStoreBasedConsolePolicy.class.getName();
176        }
177
178        @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
179            "PMD.AvoidInstantiatingObjectsInLoops" })
180        public void onConsolePrepared(
181                ConsolePrepared event, IOSubchannel channel) {
182            KeyValueStoreQuery query = new KeyValueStoreQuery(
183                storagePath, channel);
184            Event.onCompletion(query, e -> onQueryCompleted(e, channel,
185                event.event().renderSupport()));
186            fire(query, channel);
187        }
188
189        @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
190        public void onQueryCompleted(KeyValueStoreQuery query,
191                IOSubchannel channel, RenderSupport renderSupport) {
192            try {
193                String data = Optional.ofNullable(query.get())
194                    .flatMap(m -> Optional.ofNullable(m.get(storagePath)))
195                    .orElse(null);
196                if (data == null) {
197                    persisted = new HashMap<>();
198                } else {
199                    persisted = mapper.readValue(data.getBytes(),
200                        mapper.getTypeFactory().constructMapType(Map.class,
201                            String.class, Object.class));
202                }
203            } catch (InterruptedException | IOException e) {
204                persisted = new HashMap<>();
205            }
206
207            // Make sure data is consistent
208            @SuppressWarnings("unchecked")
209            List<String> previewLayout = (List<String>) persisted
210                .computeIfAbsent("previewLayout",
211                    newKey -> Collections.emptyList());
212            @SuppressWarnings("unchecked")
213            List<String> tabsLayout = (List<String>) persisted.computeIfAbsent(
214                "tabsLayout", newKey -> Collections.emptyList());
215            Object xtraInfo = persisted.computeIfAbsent(
216                "xtraInfo", newKey -> new Object());
217
218            // Update (now consistent) layout
219            channel.respond(new LastConsoleLayout(
220                previewLayout, tabsLayout, xtraInfo));
221
222            // Restore conlets
223            for (String conletId : tabsLayout) {
224                fire(new RenderConletRequest(renderSupport, conletId,
225                    RenderMode.asSet(RenderMode.View)), channel);
226            }
227            for (String conletId : previewLayout) {
228                fire(new RenderConletRequest(renderSupport, conletId,
229                    RenderMode.asSet(RenderMode.Preview,
230                        RenderMode.Foreground)),
231                    channel);
232            }
233        }
234
235        public void onConsoleLayoutChanged(ConsoleLayoutChanged event,
236                IOSubchannel channel) throws IOException {
237            persisted.put("previewLayout", event.previewLayout());
238            persisted.put("tabsLayout", event.tabsLayout());
239            persisted.put("xtraInfo", event.xtraInfo());
240
241            // Now store.
242            fire(new KeyValueStoreUpdate().update(storagePath,
243                mapper.writer().writeValueAsString(persisted)), channel);
244        }
245
246    }
247}