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.ArrayList; 023import java.util.HashMap; 024import java.util.List; 025import java.util.Map; 026import org.jdrupes.json.JsonArray; 027import org.jgrapes.core.Channel; 028import org.jgrapes.core.Component; 029import org.jgrapes.core.Event; 030import org.jgrapes.core.Manager; 031import org.jgrapes.core.TypedIdKey; 032import org.jgrapes.core.annotation.Handler; 033import org.jgrapes.util.events.KeyValueStoreQuery; 034import org.jgrapes.util.events.KeyValueStoreUpdate; 035import org.jgrapes.util.events.KeyValueStoreUpdate.Action; 036import org.jgrapes.util.events.KeyValueStoreUpdate.Deletion; 037import org.jgrapes.util.events.KeyValueStoreUpdate.Update; 038import org.jgrapes.webconsole.base.events.ConsoleReady; 039import org.jgrapes.webconsole.base.events.JsonInput; 040import org.jgrapes.webconsole.base.events.SimpleConsoleCommand; 041 042/** 043 * The Class BrowserLocalBackedKVStore. 044 */ 045public class BrowserLocalBackedKVStore extends Component { 046 047 private final String consolePrefix; 048 049 /** 050 * Create a new key/value store that uses the browser's local storage 051 * for persisting the values. 052 * 053 * @param componentChannel the channel that the component's 054 * handlers listen on by default and that 055 * {@link Manager#fire(Event, Channel...)} sends the event to 056 * @param consolePrefix the web console's prefix as returned by 057 * {@link ConsoleWeblet#prefix()}, i.e. staring and ending with a slash 058 */ 059 public BrowserLocalBackedKVStore( 060 Channel componentChannel, String consolePrefix) { 061 super(componentChannel); 062 this.consolePrefix = consolePrefix; 063 } 064 065 /** 066 * Intercept {@link ConsoleReady} event to first get data. 067 * 068 * @param event the event 069 * @param channel the channel 070 * @throws InterruptedException the interrupted exception 071 */ 072 @Handler(priority = 1000) 073 public void onConsoleReady(ConsoleReady event, ConsoleConnection channel) 074 throws InterruptedException { 075 // Put there by onJsonInput if retrieval has been done. 076 if (TypedIdKey.get(channel.session(), Store.class, 077 consolePrefix).isPresent()) { 078 // Already have store, nothing to do 079 return; 080 } 081 // Suspend and trigger data retrieval 082 event.suspendHandling(); 083 channel.setAssociated(this, event); 084 String keyStart = consolePrefix 085 + BrowserLocalBackedKVStore.class.getName() + "/"; 086 channel 087 .respond(new SimpleConsoleCommand("retrieveLocalData", keyStart)); 088 } 089 090 @SuppressWarnings("PMD.LooseCoupling") 091 private Store getStore(ConsoleConnection channel) { 092 return TypedIdKey 093 .get(channel.session(), Store.class, consolePrefix) 094 .orElseGet( 095 () -> TypedIdKey.put(channel.session(), consolePrefix, 096 new Store())); 097 } 098 099 /** 100 * Evaluate "retrievedLocalData" response. 101 * 102 * @param event the event 103 * @param channel the channel 104 * @throws InterruptedException the interrupted exception 105 * @throws IOException Signals that an I/O exception has occurred. 106 */ 107 @Handler 108 @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") 109 public void onJsonInput(JsonInput event, ConsoleConnection channel) 110 throws InterruptedException, IOException { 111 if (!"retrievedLocalData".equals(event.request().method())) { 112 return; 113 } 114 channel.associated(this, ConsoleReady.class) 115 .ifPresent(origEvent -> { 116 // We have intercepted the web console ready event, fill store. 117 // Having a store now also shows that retrieval has been done. 118 final String keyStart = consolePrefix 119 + BrowserLocalBackedKVStore.class.getName() + "/"; 120 @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", 121 "PMD.LooseCoupling" }) 122 Store data = getStore(channel); 123 JsonArray params = (JsonArray) event.request().params(); 124 params.asArray(0).arrayStream().forEach(item -> { 125 String key = item.asString(0); 126 if (key.startsWith(keyStart)) { 127 data.put(key.substring( 128 keyStart.length() - 1), item.asString(1)); 129 } 130 }); 131 // Don't re-use 132 channel.setAssociated(this, null); 133 // Let others process the web console ready event 134 origEvent.resumeHandling(); 135 }); 136 } 137 138 /** 139 * Handle data update events. 140 * 141 * @param event the event 142 * @param channel the channel 143 * @throws InterruptedException the interrupted exception 144 * @throws IOException Signals that an I/O exception has occurred. 145 */ 146 @Handler 147 @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", 148 "PMD.AvoidInstantiatingObjectsInLoops" }) 149 public void onKeyValueStoreUpdate( 150 KeyValueStoreUpdate event, ConsoleConnection channel) 151 throws InterruptedException, IOException { 152 @SuppressWarnings("PMD.LooseCoupling") 153 Store data = getStore(channel); 154 List<String[]> actions = new ArrayList<>(); 155 String keyStart = consolePrefix 156 + BrowserLocalBackedKVStore.class.getName(); 157 for (Action action : event.actions()) { 158 @SuppressWarnings("PMD.UselessParentheses") 159 String key = keyStart + (action.key().startsWith("/") 160 ? action.key() 161 : ("/" + action.key())); 162 if (action instanceof Update) { 163 actions.add(new String[] { "u", key, 164 ((Update) action).value() }); 165 data.put(action.key(), ((Update) action).value()); 166 } else if (action instanceof Deletion) { 167 actions.add(new String[] { "d", key }); 168 data.remove(action.key()); 169 } 170 } 171 channel.respond(new SimpleConsoleCommand("storeLocalData", 172 new Object[] { actions.toArray() })); 173 } 174 175 /** 176 * Handle data query.. 177 * 178 * @param event the event 179 * @param channel the channel 180 */ 181 @Handler 182 @SuppressWarnings("PMD.ConfusingTernary") 183 public void onKeyValueStoreQuery( 184 KeyValueStoreQuery event, ConsoleConnection channel) { 185 @SuppressWarnings("PMD.UseConcurrentHashMap") 186 Map<String, String> result = new HashMap<>(); 187 TypedIdKey.get(channel.session(), Store.class, consolePrefix) 188 .ifPresent(data -> { 189 if (!event.query().endsWith("/")) { 190 // Single value 191 if (data.containsKey(event.query())) { 192 result.put(event.query(), data.get(event.query())); 193 } 194 } else { 195 for (Map.Entry<String, String> e : data.entrySet()) { 196 if (e.getKey().startsWith(event.query())) { 197 result.put(e.getKey(), e.getValue()); 198 } 199 } 200 } 201 event.setResult(result); 202 }); 203 } 204 205 /** 206 * The store. 207 */ 208 @SuppressWarnings("serial") 209 private final class Store extends HashMap<String, String> { 210 } 211}