001/* 002 * JGrapes Event Driven Framework 003 * Copyright (C) 2017-2022 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.util.events; 020 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.Map; 025import java.util.Optional; 026import java.util.Set; 027import java.util.concurrent.ConcurrentHashMap; 028import java.util.stream.Collectors; 029import org.jgrapes.core.Event; 030import org.jgrapes.core.Manager; 031import org.jgrapes.util.ConfigurationStore; 032 033/** 034 * An event to indicate that configuration information has been 035 * updated. 036 * 037 * Configuration information provided by this event is organized 038 * by paths and associated key/value pairs. The path information 039 * should be used by components to select the information important 040 * to them. Often, a component simply matches the path from the event 041 * with its own path in the component hierarchy 042 * (see {@link Manager#componentPath()}). But paths can also be used 043 * to structure information in a way that is completely independent of 044 * the implementation's structure as the filtering is completely up 045 * to the component. 046 */ 047@SuppressWarnings("PMD.DataflowAnomalyAnalysis") 048public class ConfigurationUpdate extends Event<Void> { 049 050 @SuppressWarnings("PMD.UseConcurrentHashMap") 051 private final Map<String, Map<String, Object>> structuredValues 052 = new HashMap<>(); 053 private final Map<String, Map<String, Object>> flattenedCache 054 = new ConcurrentHashMap<>(); 055 056 /** 057 * Return all paths affected by this event. 058 * 059 * @return the paths 060 */ 061 @SuppressWarnings("PMD.ConfusingTernary") 062 public Set<String> paths() { 063 synchronized (structuredValues) { 064 return new HashSet<>(structuredValues.keySet()); 065 } 066 } 067 068 private Optional<Map<String, Object>> flattened(String path) { 069 return Optional.ofNullable(flattenedCache.computeIfAbsent(path, 070 p -> ConfigurationStore.flatten(structuredValues.get(path)))); 071 } 072 073 /** 074 * Return the properties for a given path if any exist. 075 * If a property has a structured value (list or collection), 076 * the values are returned as several entries as described in 077 * {@link ConfigurationStore#flatten(Map)}. All values are 078 * converted to their string representation. 079 * 080 * @param path the path 081 * @return the updated values or `null` if the path has been 082 * removed (implies the removal of all values for that path). 083 */ 084 public Optional<Map<String, String>> values(String path) { 085 if (structuredValues.get(path) == null) { 086 return Optional.empty(); 087 } 088 Map<String, Object> result = flattened(path).get(); 089 return Optional.of(result).map(o -> o.entrySet().stream() 090 .collect( 091 Collectors.toMap(Map.Entry::getKey, e -> ConfigurationStore 092 .as(e.getValue(), String.class).orElse(null)))); 093 } 094 095 /** 096 * Return the value with the given path and key if it exists 097 * and is of or can be converted to the requested type. 098 * 099 * @param <T> the generic type 100 * @param path the path 101 * @param key the key 102 * @param as the as 103 * @return the optional 104 */ 105 @SuppressWarnings("PMD.ShortVariable") 106 public <T> Optional<T> value(String path, String key, Class<T> as) { 107 return flattened(path) 108 .flatMap(map -> ConfigurationStore.as(map.get(key), as)); 109 } 110 111 /** 112 * Return the value with the given path and key if it exists as string. 113 * 114 * @param path the path 115 * @param key the key 116 * @return the value 117 */ 118 public Optional<String> value(String path, String key) { 119 return value(path, key, String.class); 120 } 121 122 /** 123 * Return the properties for a given path if they exists as 124 * a map with (possibly) structured values (see 125 * {@link ConfigurationStore#structured(String)}). The type 126 * of the value depends on the configuration store used. 127 * Some configuration stores support types other than string, 128 * some don't. Too avoid any problems, it is strongly recommended 129 * to call {@link ConfigurationStore#as(Object, Class)} for any 130 * value obtained from the result of this method. 131 * 132 * @param path the path 133 * @return the updated values or `null` if the path has been 134 * removed (implies the removal of all values for that path). 135 */ 136 public Optional<Map<String, Object>> structured(String path) { 137 if (structuredValues.get(path) == null) { 138 return Optional.empty(); 139 } 140 return Optional 141 .of(Collections.unmodifiableMap(structuredValues.get(path))); 142 } 143 144 /** 145 * Set new (updated), possibly structured configuration values (see 146 * {@link ConfigurationStore#structure(Map)} for the given path. 147 * Any information associated with the path before the invocation 148 * of this method is replaced. 149 * 150 * @param path the value's path 151 * @return the event for easy chaining 152 */ 153 @SuppressWarnings("unchecked") 154 public ConfigurationUpdate set(String path, Map<String, ?> values) { 155 if (path == null || !path.startsWith("/")) { 156 throw new IllegalArgumentException("Path must start with \"/\"."); 157 } 158 structuredValues.put(path, (Map<String, Object>) values); 159 return this; 160 } 161 162 /** 163 * Add a new (or updated) configuration value for the given path 164 * and key. 165 * 166 * @param path the value's path 167 * @param selector the key or the path within the structured value 168 * @param value the value 169 * @return the event for easy chaining 170 */ 171 public ConfigurationUpdate add(String path, String selector, Object value) { 172 if (path == null || !path.startsWith("/")) { 173 throw new IllegalArgumentException("Path must start with \"/\"."); 174 } 175 synchronized (structuredValues) { 176 @SuppressWarnings("PMD.UseConcurrentHashMap") 177 Map<String, Object> scoped = structuredValues 178 .computeIfAbsent(path, newKey -> new HashMap<String, Object>()); 179 ConfigurationStore.mergeValue(scoped, selector, value); 180 flattenedCache.remove(path); 181 } 182 return this; 183 } 184 185 /** 186 * Associate the given path with `null`. This signals to handlers 187 * that the path has been removed from the configuration. 188 * 189 * @param path the path that has been removed 190 * @return the event for easy chaining 191 */ 192 public ConfigurationUpdate removePath(String path) { 193 if (path == null || !path.startsWith("/")) { 194 throw new IllegalArgumentException("Path must start with \"/\"."); 195 } 196 structuredValues.put(path, null); 197 return this; 198 } 199 200}