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}