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