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 public void onJsonInput(JsonInput event, ConsoleConnection channel) 109 throws InterruptedException, IOException { 110 if (!"retrievedLocalData".equals(event.request().method())) { 111 return; 112 } 113 channel.associated(this, ConsoleReady.class) 114 .ifPresent(origEvent -> { 115 // We have intercepted the web console ready event, fill store. 116 // Having a store now also shows that retrieval has been done. 117 final String keyStart = consolePrefix 118 + BrowserLocalBackedKVStore.class.getName() + "/"; 119 @SuppressWarnings({ "PMD.LooseCoupling" }) 120 Store data = getStore(channel); 121 String[][] values = event.request().param(0); 122 Arrays.stream(values).forEach(item -> { 123 String key = item[0]; 124 if (key.startsWith(keyStart)) { 125 data.put(key.substring(keyStart.length() - 1), item[1]); 126 } 127 }); 128 // Don't re-use 129 channel.setAssociated(this, null); 130 // Let others process the web console ready event 131 origEvent.resumeHandling(); 132 }); 133 } 134 135 /** 136 * Handle data update events. 137 * 138 * @param event the event 139 * @param channel the channel 140 * @throws InterruptedException the interrupted exception 141 * @throws IOException Signals that an I/O exception has occurred. 142 */ 143 @Handler 144 @SuppressWarnings("PMD.UnnecessaryVarargsArrayCreation") 145 public void onKeyValueStoreUpdate( 146 KeyValueStoreUpdate event, ConsoleConnection channel) 147 throws InterruptedException, IOException { 148 @SuppressWarnings("PMD.LooseCoupling") 149 Store data = getStore(channel); 150 List<String[]> actions = new ArrayList<>(); 151 String keyStart = consolePrefix 152 + BrowserLocalBackedKVStore.class.getName(); 153 for (Action action : event.actions()) { 154 String key = keyStart + (action.key().startsWith("/") 155 ? action.key() 156 : ("/" + action.key())); 157 if (action instanceof Update) { 158 actions.add(new String[] { "u", key, 159 ((Update) action).value() }); 160 data.put(action.key(), ((Update) action).value()); 161 } else if (action instanceof Deletion) { 162 actions.add(new String[] { "d", key }); 163 data.remove(action.key()); 164 } 165 } 166 channel.respond(new SimpleConsoleCommand("storeLocalData", 167 new Object[] { actions.toArray() })); 168 } 169 170 /** 171 * Handle data query.. 172 * 173 * @param event the event 174 * @param channel the channel 175 */ 176 @Handler 177 @SuppressWarnings("PMD.ConfusingTernary") 178 public void onKeyValueStoreQuery( 179 KeyValueStoreQuery event, ConsoleConnection channel) { 180 @SuppressWarnings("PMD.UseConcurrentHashMap") 181 Map<String, String> result = new HashMap<>(); 182 TypedIdKey.get(channel.session(), Store.class, consolePrefix) 183 .ifPresent(data -> { 184 if (!event.query().endsWith("/")) { 185 // Single value 186 if (data.containsKey(event.query())) { 187 result.put(event.query(), data.get(event.query())); 188 } 189 } else { 190 for (Map.Entry<String, String> e : data.entrySet()) { 191 if (e.getKey().startsWith(event.query())) { 192 result.put(e.getKey(), e.getValue()); 193 } 194 } 195 } 196 event.setResult(result); 197 }); 198 } 199 200 /** 201 * The store. 202 */ 203 @SuppressWarnings("serial") 204 private final class Store extends HashMap<String, String> { 205 } 206}