001/* 002 * JGrapes Event Driven Framework 003 * Copyright (C) 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; 020 021import java.math.BigDecimal; 022import java.time.Instant; 023import java.time.format.DateTimeParseException; 024import java.time.temporal.TemporalAccessor; 025import java.util.ArrayList; 026import java.util.Collection; 027import java.util.HashMap; 028import java.util.LinkedList; 029import java.util.List; 030import java.util.Map; 031import java.util.Optional; 032import java.util.Queue; 033import java.util.TreeMap; 034import java.util.regex.Pattern; 035import java.util.stream.Collectors; 036import org.jgrapes.core.Channel; 037import org.jgrapes.core.Component; 038import org.jgrapes.core.Event; 039import org.jgrapes.core.Manager; 040import org.jgrapes.core.annotation.Handler; 041import org.jgrapes.core.annotation.HandlerDefinition.ChannelReplacements; 042import org.jgrapes.util.events.InitialConfiguration; 043 044/** 045 * A base class for configuration stores. Implementing classes must 046 * override one of the methods {@link #structured(String)} or 047 * {@link #values(String)} as the default implementations of either 048 * calls the other. 049 */ 050@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.GodClass" }) 051public abstract class ConfigurationStore extends Component { 052 053 public static final Pattern NUMBER = Pattern.compile("^\\d+$"); 054 055 /** 056 * Creates a new component with its channel set to itself. 057 */ 058 public ConfigurationStore() { 059 // Nothing to do. 060 } 061 062 /** 063 * Creates a new component base with its channel set to the given 064 * channel. As a special case {@link Channel#SELF} can be 065 * passed to the constructor to make the component use itself 066 * as channel. The special value is necessary as you 067 * obviously cannot pass an object to be constructed to its 068 * constructor. 069 * 070 * @param componentChannel the channel that the component's 071 * handlers listen on by default and that 072 * {@link Manager#fire(Event, Channel...)} sends the event to 073 */ 074 public ConfigurationStore(Channel componentChannel) { 075 super(componentChannel); 076 } 077 078 /** 079 * Creates a new component base like {@link #ConfigurationStore(Channel)} 080 * but with channel mappings for {@link Handler} annotations. 081 * 082 * @param componentChannel the channel that the component's 083 * handlers listen on by default and that 084 * {@link Manager#fire(Event, Channel...)} sends the event to 085 * @param channelReplacements the channel replacements to apply 086 * to the `channels` elements of the {@link Handler} annotations 087 */ 088 public ConfigurationStore(Channel componentChannel, 089 ChannelReplacements channelReplacements) { 090 super(componentChannel, channelReplacements); 091 } 092 093 /** 094 * Configuration information should be kept simple. Sometimes, 095 * however, it is unavoidable to structure the information 096 * associated with a (logical) key. This can be done by 097 * reflecting the structure in the names of actual keys, derived 098 * from the logical key. Names such as "key.0", "key.1", "key.2" 099 * can be used to express that the value associated with "key" 100 * is a list of values. "key.a", "key.b", "key.c" can be used 101 * to associate "key" with a map from "a", "b", "c" to some values. 102 * 103 * This methods looks at all values in the map passed as 104 * argument. If the value is a collection or map, the entry is 105 * converted to several entries following the pattern outlined 106 * above. 107 * 108 * @param structured the map with possibly structured properties 109 * @return the map with flattened properties 110 */ 111 public static Map<String, Object> flatten(Map<String, ?> structured) { 112 @SuppressWarnings("PMD.UseConcurrentHashMap") 113 Map<String, Object> result = new HashMap<>(); 114 flattenObject(result, null, structured); 115 return result; 116 } 117 118 @SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" }) 119 private static void flattenObject(Map<String, Object> result, 120 String prefix, Object value) { 121 if (value instanceof Map) { 122 for (var entry : ((Map<Object, Object>) value).entrySet()) { 123 if (entry.getKey().toString().startsWith("/")) { 124 continue; 125 } 126 flattenObject(result, 127 Optional.ofNullable(prefix).map(p -> p + ".").orElse("") 128 + entry.getKey(), 129 entry.getValue()); 130 } 131 return; 132 } 133 if (value instanceof Collection) { 134 int count = 0; 135 for (var item : (Collection<?>) value) { 136 flattenObject(result, prefix + "." + count++, item); 137 } 138 return; 139 } 140 result.put(prefix, value); 141 } 142 143 /** 144 * Same as {@link #structure(Map, boolean)} with `false` as 145 * second argument. 146 * 147 * @param flatProperties the flat properties 148 * @return a map with structured values 149 */ 150 @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", 151 "PMD.ReturnEmptyCollectionRatherThanNull" }) 152 public static Map<String, Object> structure(Map<String, ?> flatProperties) { 153 return structure(flatProperties, false); 154 } 155 156 /** 157 * The reverse operation to {@link #flatten(Map)}. Entries with 158 * key names matching the pattern outlined in {@link #flatten(Map)} 159 * are combined to a single entry with a structured value (map or 160 * list). 161 * 162 * Usually, only key patterns with consecutive numbers starting 163 * with zero are converted to lists (e.g. `key.0`, `key.1`, `key.2`). 164 * If entries are missing, the values at that level are converted to 165 * a `Map<Integer,Object>` with the given entries instead. If 166 * `convertSparse` is `true`, incomplete index sets such as `key.2`, 167 * `key.3`, `key.5` are converted to lists with the available number 168 * of elements despite the missing entries. 169 * 170 * If the derived class overrides {@link #structured(String)}, 171 * the leaf values in the returned structure are the values 172 * provided by the overriding implementation (while 173 * {@link #values(String))} always provides {@link String}s). 174 * Some configuration formats define types other then string and 175 * therefore value can be e.g. {@link Integer}s or {@link Instant}s. 176 * In order to support the usage of arbitrary configuration store 177 * implementations, values obtained from the data structure returned 178 * by {@link #structure(Map, boolean)} should always be passed 179 * through {@link #as(Object, Class)}. This method preserves 180 * non-string objects if they match the requested type or 181 * converts the value from its string representation to the 182 * requested type, if possible. 183 * 184 * @param flatProperties the flat properties 185 * @param convertSparse controls conversion to lists 186 * @return a map with structured values 187 */ 188 @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", 189 "PMD.ReturnEmptyCollectionRatherThanNull" }) 190 public static Map<String, Object> 191 structure(Map<String, ?> flatProperties, boolean convertSparse) { 192 if (flatProperties == null) { 193 return null; 194 } 195 @SuppressWarnings("PMD.UseConcurrentHashMap") 196 Map<String, Object> result = new HashMap<>(); 197 for (var entry : flatProperties.entrySet()) { 198 // Original key (optionally) consists of dot separated parts 199 var parts = new LinkedList<>(List.of(entry.getKey() 200 .split("\\.(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1))); 201 mergeValue(result, parts, entry.getValue()); 202 } 203 204 // Now convert all maps that have only Integer keys to lists 205 for (var entry : result.entrySet()) { 206 entry.setValue(maybeConvert(entry.getValue(), convertSparse)); 207 } 208 209 // Return result 210 return result; 211 } 212 213 /** 214 * Similar to {@link ConfigurationStore#structure(Map)} but merges 215 * only a single value into an existing map. 216 * 217 * @param target the target 218 * @param selector the path selector 219 * @param value the value 220 * @return the map 221 */ 222 @SuppressWarnings("unchecked") 223 public static Map<String, Object> mergeValue(Map<?, ?> target, 224 String selector, Object value) { 225 // Original key (optionally) consists of dot separated parts 226 var parts = new LinkedList<>(List.of(selector 227 .split("\\.(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1))); 228 mergeValue(target, parts, value); 229 230 // Now convert all maps that have only Integer keys to lists 231 for (var entry : ((Map<String, Object>) target).entrySet()) { 232 entry.setValue(maybeConvert(entry.getValue(), false)); 233 } 234 return (Map<String, Object>) target; 235 } 236 237 @SuppressWarnings("unchecked") 238 private static void mergeValue(Map<?, ?> target, Queue<String> parts, 239 Object value) { 240 var part = parts.poll(); 241 if (part.startsWith("\"") && part.endsWith("\"")) { 242 part = part.substring(1, part.length() - 1); 243 } 244 Object key = NUMBER.matcher(part).find() 245 ? Integer.parseInt(part) 246 : part; 247 if (parts.isEmpty()) { 248 // Last part (of key), store value 249 ((Map<Object, Object>) target).put(key, value); 250 return; 251 } 252 var newTarget = ((Map<Object, Object>) target) 253 .computeIfAbsent(key, k -> new TreeMap<>()); 254 255 // Convert list to map 256 if (newTarget instanceof List list) { 257 var asMap = new TreeMap<>(); 258 for (var item : list) { 259 asMap.put(asMap.size(), item); 260 } 261 newTarget = asMap; 262 ((Map<Object, Object>) target).put(key, newTarget); 263 } 264 mergeValue((Map<Object, Object>) newTarget, parts, value); 265 } 266 267 @SuppressWarnings({ "unchecked", "PMD.ConfusingTernary" }) 268 private static Object maybeConvert(Object value, boolean convertSparse) { 269 if (!(value instanceof TreeMap)) { 270 return value; 271 } 272 List<Object> converted = new ArrayList<>(); 273 for (var entry : ((Map<Object, Object>) value).entrySet()) { 274 entry.setValue(maybeConvert(entry.getValue(), convertSparse)); 275 if (converted == null) { 276 continue; 277 } 278 if (!(entry.getKey() instanceof Integer) 279 || !convertSparse 280 && ((Integer) entry.getKey()) != converted.size()) { 281 // Don't convert, leave as Map. 282 converted = null; 283 continue; 284 } 285 converted.add(entry.getValue()); 286 } 287 return converted != null ? converted : value; 288 } 289 290 /** 291 * Return the values for a given path if they exist. This 292 * method should only be used in cases where configuration values 293 * are needed before the {@link InitialConfiguration} event is 294 * fired, e.g. while creating the component tree. 295 * 296 * @param path the path 297 * @return the values, if defined for the given path 298 */ 299 public Optional<Map<String, String>> values(String path) { 300 return structured(path).map(ConfigurationStore::flatten) 301 .map(o -> o.entrySet().stream() 302 .collect(Collectors.toMap(Map.Entry::getKey, 303 e -> e.getValue().toString()))); 304 } 305 306 /** 307 * Return the properties for a given path if they exists 308 * as structured data, see {@link #structure(Map)}. 309 * 310 * @param path the path 311 * @return the values, if defined for the given path 312 */ 313 public Optional<Map<String, Object>> structured(String path) { 314 return values(path).map(ConfigurationStore::structure); 315 } 316 317 /** 318 * If the value is not `null`, return it as the requested type. 319 * The method is successful if the value already is of the 320 * requested type (or a subtype) or if the value is of type 321 * {@link String} and can be converted to the requested type. 322 * 323 * Supported types are: 324 * * {@link String} 325 * * {@link Number}, converts from {@link String} using 326 * {@link BigDecimal#BigDecimal(String)} 327 * * {@link Instant}, converts from {@link TemporalAccessor} 328 * or from {@link String} using {@link Instant#parse(CharSequence)) 329 * * `Boolean`, converts from {@link String} using 330 * {@link Boolean#valueOf(String)} 331 * 332 * @return the value 333 */ 334 @SuppressWarnings({ "unchecked", "PMD.ShortMethodName", 335 "PMD.NPathComplexity" }) 336 public static <T> Optional<T> as(Object value, Class<T> requested) { 337 // Handle null. 338 if (value == null) { 339 return Optional.empty(); 340 } 341 // Is of requested type? 342 if (requested.isAssignableFrom(value.getClass())) { 343 return Optional.of((T) value); 344 } 345 // Convert to Instant, if requested. 346 if (requested.equals(Instant.class)) { 347 if (value instanceof TemporalAccessor) { 348 return Optional.of((T) Instant.from((TemporalAccessor) value)); 349 } 350 try { 351 return Optional.of((T) Instant.parse(value.toString())); 352 } catch (DateTimeParseException e) { 353 return Optional.empty(); 354 } 355 } 356 // Convert to String, if requested. 357 if (requested.equals(String.class)) { 358 return Optional.of((T) value.toString()); 359 } 360 // Remaining conversions require a string representation. 361 if (!(value instanceof String)) { 362 return Optional.empty(); 363 } 364 if (requested.equals(Number.class)) { 365 try { 366 return Optional.of((T) new BigDecimal((String) value)); 367 } catch (NumberFormatException e) { 368 return Optional.empty(); 369 } 370 } 371 if (requested.equals(Boolean.class)) { 372 return Optional.of((T) Boolean.valueOf((String) value)); 373 } 374 return Optional.empty(); 375 } 376 377 /** 378 * Short for `as(value, String.class)`. 379 * 380 * @param value the value 381 * @return the optional 382 */ 383 public static Optional<String> asString(Object value) { 384 return as(value, String.class); 385 } 386 387 /** 388 * Short for `as(value, Number.class)`. 389 * 390 * @param value the value 391 * @return the optional 392 */ 393 public static Optional<Number> asNumber(Object value) { 394 return as(value, Number.class); 395 } 396 397 /** 398 * Short for `as(value, Instant.class)`. 399 * 400 * @param value the value 401 * @return the optional 402 */ 403 public static Optional<Instant> asInstant(Object value) { 404 return as(value, Instant.class); 405 } 406 407 /** 408 * Short for `as(value, Boolean.class)`. 409 * 410 * @param value the value 411 * @return the optional 412 */ 413 public static Optional<Boolean> asBoolean(Object value) { 414 return as(value, Boolean.class); 415 } 416}