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.nio.charset.StandardCharsets;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.List;
031import java.util.Map;
032import java.util.Optional;
033import org.jgrapes.core.Channel;
034import org.jgrapes.core.Component;
035import org.jgrapes.core.Event;
036import org.jgrapes.core.annotation.Handler;
037import org.jgrapes.http.Session;
038import org.jgrapes.io.IOSubchannel;
039import org.jgrapes.util.events.KeyValueStoreQuery;
040import org.jgrapes.util.events.KeyValueStoreUpdate;
041import org.jgrapes.webconsole.base.Conlet.RenderMode;
042import org.jgrapes.webconsole.base.events.ConsoleLayoutChanged;
043import org.jgrapes.webconsole.base.events.ConsolePrepared;
044import org.jgrapes.webconsole.base.events.LastConsoleLayout;
045import org.jgrapes.webconsole.base.events.RenderConletRequest;
046
047/**
048 * A component that restores the console layout
049 * using key/value events for persisting the data between sessions.
050 * 
051 * <img src="KVPPBootSeq.svg" alt="Boot Event Sequence">
052 * 
053 * This component requires another component that handles the key/value
054 * store events ({@link KeyValueStoreUpdate}, {@link KeyValueStoreQuery})
055 * used by this component for implementing persistence. When the web console 
056 * becomesready, this policy sends a query for the persisted data.
057 * 
058 * When the web console has been prepared, the policy sends the last layout
059 * as retrieved from persistent storage to the web console and then generates
060 * render events for all web console components contained in this layout.
061 * 
062 * Each time the layout is changed in the web console, the web console sends 
063 * the new layout data and this component updates the persistent storage
064 * accordingly.
065 * 
066 * @startuml KVPPBootSeq.svg
067 * hide footbox
068 * 
069 * actor System
070 * System -> KVStoreBasedConsolePolicy: ConsolePrepared
071 * activate KVStoreBasedConsolePolicy
072 * KVStoreBasedConsolePolicy -> "KV Store": KeyValueStoreQuery
073 * deactivate KVStoreBasedConsolePolicy
074 * activate "KV Store"
075 * "KV Store" -> KVStoreBasedConsolePolicy: KeyValueStoreData
076 * deactivate "KV Store"
077 * activate KVStoreBasedConsolePolicy
078 * KVStoreBasedConsolePolicy -> WebConsole: LastConsoleLayout
079 * activate WebConsole
080 * WebConsole -> Browser: "lastConsoleLayout"
081 * deactivate WebConsole
082 * loop for all conlets to be displayed
083 *     KVStoreBasedConsolePolicy -> ConletX: RenderConletRequest
084 *     activate ConletX
085 *     ConletX -> WebConsole: RenderConlet
086 *     deactivate ConletX
087 *     activate WebConsole
088 *     WebConsole -> Browser: "renderConlet"
089 *     deactivate WebConsole
090 * end
091 * deactivate KVStoreBasedConsolePolicy
092 * 
093 * Browser -> WebConsole: "consoleLayout"
094 * activate WebConsole
095 * WebConsole -> KVStoreBasedConsolePolicy: ConsoleLayoutChanged
096 * deactivate WebConsole
097 * activate KVStoreBasedConsolePolicy
098 * KVStoreBasedConsolePolicy -> "KV Store": KeyValueStoreUpdate
099 * deactivate KVStoreBasedConsolePolicy
100 * activate "KV Store"
101 * deactivate "KV Store"
102 * 
103 * @enduml
104 */
105public class KVStoreBasedConsolePolicy extends Component {
106
107    /** The mapper. */
108    @SuppressWarnings("PMD.FieldNamingConventions")
109    protected static final ObjectMapper mapper = JsonMapper.builder()
110        .addModule(new ParameterNamesModule()).addModule(new Jdk8Module())
111        .addModule(new JavaTimeModule()).build();
112
113    /**
114     * Creates a new component with its channel set to
115     * itself.
116     */
117    public KVStoreBasedConsolePolicy() {
118        // Everything done by super.
119    }
120
121    /**
122     * Creates a new component with its channel set to the given channel.
123     * 
124     * @param componentChannel
125     */
126    public KVStoreBasedConsolePolicy(Channel componentChannel) {
127        super(componentChannel);
128    }
129
130    /**
131     * Create browser session scoped storage and forward event to it.
132     *
133     * @param event the event
134     * @param channel the channel
135     */
136    @Handler
137    public void onConsolePrepared(
138            ConsolePrepared event, ConsoleConnection channel) {
139        ((KVStoredLayoutData) channel.session().transientData()
140            .computeIfAbsent(KVStoredLayoutData.class,
141                key -> new KVStoredLayoutData(channel.session())))
142                    .onConsolePrepared(event, channel);
143    }
144
145    /**
146     * Forward layout changed event to browser session scoped storage.
147     *
148     * @param event the event
149     * @param channel the channel
150     * @throws IOException Signals that an I/O exception has occurred.
151     */
152    @Handler
153    public void onConsoleLayoutChanged(ConsoleLayoutChanged event,
154            ConsoleConnection channel) throws IOException {
155        Optional<KVStoredLayoutData> optDs = Optional.ofNullable(
156            (KVStoredLayoutData) channel.session().transientData()
157                .get(KVStoredLayoutData.class));
158        if (optDs.isPresent()) {
159            optDs.get().onConsoleLayoutChanged(event, channel);
160        }
161    }
162
163    /**
164     * Caches the data in the session.
165     */
166    @SuppressWarnings({ "PMD.CommentRequired",
167        "PMD.PublicMemberInNonPublicType" })
168    private class KVStoredLayoutData {
169
170        private final String storagePath;
171        private Map<String, Object> persisted;
172
173        public KVStoredLayoutData(Session session) {
174            storagePath = "/"
175                + WebConsoleUtils.userFromSession(session)
176                    .map(ConsoleUser::getName).orElse("")
177                + "/" + KVStoreBasedConsolePolicy.class.getName();
178        }
179
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
200                        .readValue(data.getBytes(StandardCharsets.UTF_8),
201                            mapper.getTypeFactory().constructMapType(Map.class,
202                                String.class, Object.class));
203                }
204            } catch (InterruptedException | IOException e) {
205                persisted = new HashMap<>();
206            }
207
208            // Make sure data is consistent
209            @SuppressWarnings("unchecked")
210            List<String> previewLayout = (List<String>) persisted
211                .computeIfAbsent("previewLayout",
212                    newKey -> Collections.emptyList());
213            @SuppressWarnings("unchecked")
214            List<String> tabsLayout = (List<String>) persisted.computeIfAbsent(
215                "tabsLayout", newKey -> Collections.emptyList());
216            Object xtraInfo = persisted.computeIfAbsent(
217                "xtraInfo", newKey -> new HashMap<>());
218
219            // Update (now consistent) layout
220            channel.respond(new LastConsoleLayout(
221                previewLayout, tabsLayout, xtraInfo));
222
223            // Restore conlets
224            for (String conletId : tabsLayout) {
225                fire(new RenderConletRequest(renderSupport, conletId,
226                    RenderMode.asSet(RenderMode.View)), channel);
227            }
228            for (String conletId : previewLayout) {
229                fire(new RenderConletRequest(renderSupport, conletId,
230                    RenderMode.asSet(RenderMode.Preview,
231                        RenderMode.Foreground)),
232                    channel);
233            }
234        }
235
236        public void onConsoleLayoutChanged(ConsoleLayoutChanged event,
237                IOSubchannel channel) throws IOException {
238            persisted.put("previewLayout", event.previewLayout());
239            persisted.put("tabsLayout", event.tabsLayout());
240            persisted.put("xtraInfo", event.xtraInfo());
241
242            // Now store.
243            fire(new KeyValueStoreUpdate().update(storagePath,
244                mapper.writer().writeValueAsString(persisted)), channel);
245        }
246
247    }
248}