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