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.Arrays; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 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 String[][] values = event.request().param(0); 124 Arrays.stream(values).forEach(item -> { 125 String key = item[0]; 126 if (key.startsWith(keyStart)) { 127 data.put(key.substring(keyStart.length() - 1), item[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 @SuppressWarnings("PMD.LooseCoupling") 152 Store data = getStore(channel); 153 List<String[]> actions = new ArrayList<>(); 154 String keyStart = consolePrefix 155 + BrowserLocalBackedKVStore.class.getName(); 156 for (Action action : event.actions()) { 157 @SuppressWarnings("PMD.UselessParentheses") 158 String key = keyStart + (action.key().startsWith("/") 159 ? action.key() 160 : ("/" + action.key())); 161 if (action instanceof Update) { 162 actions.add(new String[] { "u", key, 163 ((Update) action).value() }); 164 data.put(action.key(), ((Update) action).value()); 165 } else if (action instanceof Deletion) { 166 actions.add(new String[] { "d", key }); 167 data.remove(action.key()); 168 } 169 } 170 channel.respond(new SimpleConsoleCommand("storeLocalData", 171 new Object[] { actions.toArray() })); 172 } 173 174 /** 175 * Handle data query.. 176 * 177 * @param event the event 178 * @param channel the channel 179 */ 180 @Handler 181 @SuppressWarnings("PMD.ConfusingTernary") 182 public void onKeyValueStoreQuery( 183 KeyValueStoreQuery event, ConsoleConnection channel) { 184 @SuppressWarnings("PMD.UseConcurrentHashMap") 185 Map<String, String> result = new HashMap<>(); 186 TypedIdKey.get(channel.session(), Store.class, consolePrefix) 187 .ifPresent(data -> { 188 if (!event.query().endsWith("/")) { 189 // Single value 190 if (data.containsKey(event.query())) { 191 result.put(event.query(), data.get(event.query())); 192 } 193 } else { 194 for (Map.Entry<String, String> e : data.entrySet()) { 195 if (e.getKey().startsWith(event.query())) { 196 result.put(e.getKey(), e.getValue()); 197 } 198 } 199 } 200 event.setResult(result); 201 }); 202 } 203 204 /** 205 * The store. 206 */ 207 @SuppressWarnings("serial") 208 private final class Store extends HashMap<String, String> { 209 } 210}