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}