1 /* 2 * This file is part of *** M y C o R e *** 3 * See http://www.mycore.de/ for details. 4 * 5 * MyCoRe is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * MyCoRe is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with MyCoRe. If not, see <http://www.gnu.org/licenses/>. 17 */ 18 19 package org.mycore.common.config; 20 21 import org.apache.logging.log4j.LogManager; 22 import org.mycore.common.MCRClassTools; 23 import org.mycore.common.function.MCRTriConsumer; 24 25 import java.util.Collections; 26 import java.util.Map; 27 import java.util.Optional; 28 import java.util.UUID; 29 import java.util.concurrent.Callable; 30 import java.util.concurrent.ConcurrentHashMap; 31 import java.util.function.Function; 32 import java.util.function.Predicate; 33 import java.util.function.Supplier; 34 import java.util.stream.Collectors; 35 import java.util.stream.Stream; 36 37 /** 38 * Provides methods to manage and read all configuration properties from the MyCoRe configuration files. 39 * The Properties used by this class are used from {@link MCRConfigurationBase}. 40 * <h2>NOTE</h2> 41 * <p><strong>All {@link Optional} values returned by this class are {@link Optional#empty() empty} if the property 42 * is not set OR the trimmed value {@link String#isEmpty() is empty}. If you want to distinguish between 43 * empty properties and unset properties use {@link MCRConfigurationBase#getString(String)} instead.</strong> 44 * </p> 45 * <p> 46 * Using this class is very easy, here is an example: 47 * </p> 48 * <PRE> 49 * // Get a configuration property as a String: 50 * String sValue = MCRConfiguration2.getString("MCR.String.Value").orElse(defaultValue); 51 * 52 * // Get a configuration property as a List of String (values are seperated by ","): 53 * List<String> lValue = MCRConfiguration2.getString("MCR.StringList.Value").stream() 54 * .flatMap(MCRConfiguration2::splitValue) 55 * .collect(Collectors.toList()); 56 * 57 * // Get a configuration property as a long array (values are seperated by ","): 58 * long[] la = MCRConfiguration2.getString("MCR.LongList.Value").stream() 59 * .flatMap(MCRConfiguration2::splitValue) 60 * .mapToLong(Long::parseLong) 61 * .toArray(); 62 * 63 * // Get a configuration property as an int, use 500 as default if not set: 64 * int max = MCRConfiguration2.getInt("MCR.Cache.Size").orElse(500); 65 * </PRE> 66 * 67 * There are some helper methods to help you with converting values 68 * <ul> 69 * <li>{@link #getOrThrow(String, Function)}</li> 70 * <li>{@link #splitValue(String)}</li> 71 * <li>{@link #instantiateClass(String)}</li> 72 * </ul> 73 * 74 * As you see, the class provides methods to get configuration properties as different data types and allows you to 75 * specify defaults. All MyCoRe configuration properties should start with "<CODE>MCR.</CODE>" 76 * 77 * Using the <CODE>set</CODE> methods allows client code to set new configuration properties or 78 * overwrite existing ones with new values. 79 * 80 * @author Thomas Scheffler (yagee) 81 * @since 2018.05 82 */ 83 public class MCRConfiguration2 { 84 85 private static ConcurrentHashMap<UUID, EventListener> LISTENERS = new ConcurrentHashMap<>(); 86 87 static ConcurrentHashMap<SingletonKey, Object> instanceHolder = new ConcurrentHashMap<>(); 88 89 public static Map<String, String> getPropertiesMap() { 90 return Collections.unmodifiableMap(MCRConfigurationBase.getResolvedProperties().getAsMap()); 91 } 92 93 /** 94 * Returns a sub map of properties where key is transformed. 95 * 96 * <ol> 97 * <li>if property starts with <code>propertyPrefix</code>, the property is in the result map</li> 98 * <li>the key of the target map is the name of the property without <code>propertPrefix</code></li> 99 * </ol> 100 * Example for <code>propertyPrefix="MCR.Foo."</code>: 101 * <pre> 102 * MCR.Foo.Bar=Baz 103 * MCR.Foo.Hello=World 104 * MCR.Other.Prop=Value 105 * </pre> 106 * will result in 107 * <pre> 108 * Bar=Baz 109 * Hello=World 110 * </pre> 111 * @param propertyPrefix prefix of the property name 112 * @return a map of the properties as stated above 113 */ 114 public static Map<String, String> getSubPropertiesMap(String propertyPrefix) { 115 return MCRConfigurationBase.getResolvedProperties() 116 .getAsMap() 117 .entrySet() 118 .stream() 119 .filter(e -> e.getKey().startsWith(propertyPrefix)) 120 .collect(Collectors.toMap(e -> e.getKey().substring(propertyPrefix.length()), Map.Entry::getValue)); 121 } 122 123 /** 124 * Returns a new instance of the class specified in the configuration property with the given name. 125 * If you call a method on the returned Optional directly you need to set the type like this: 126 * <pre> 127 * MCRConfiguration.<MCRMyType> getInstanceOf(name) 128 * .ifPresent(myTypeObj -> myTypeObj.method()); 129 * </pre> 130 * 131 * @param name 132 * the non-null and non-empty name of the configuration property 133 * @return the value of the configuration property as a String, or null 134 * @throws MCRConfigurationException 135 * if the class can not be loaded or instantiated 136 */ 137 public static <T> Optional<T> getInstanceOf(String name) throws MCRConfigurationException { 138 if (MCRConfigurableInstanceHelper.isSingleton(name)) { 139 return getSingleInstanceOf(name); 140 } else { 141 return MCRConfigurableInstanceHelper.getInstance(name); 142 } 143 } 144 145 /** 146 * Returns a instance of the class specified in the configuration property with the given name. If the class was 147 * previously instantiated by this method this instance is returned. 148 * If you call a method on the returned Optional directly you need to set the type like this: 149 * <pre> 150 * MCRConfiguration.<MCRMyType> getSingleInstanceOf(name) 151 * .ifPresent(myTypeObj -> myTypeObj.method()); 152 * </pre> 153 * 154 * @param name 155 * non-null and non-empty name of the configuration property 156 * @return the instance of the class named by the value of the configuration property 157 * @throws MCRConfigurationException 158 * if the class can not be loaded or instantiated 159 */ 160 public static <T> Optional<T> getSingleInstanceOf(String name) { 161 return getString(name) 162 .map(className -> new SingletonKey(name, className)) 163 .map(key -> (T) instanceHolder.computeIfAbsent(key, 164 k -> MCRConfigurableInstanceHelper.getInstance(name).orElse(null))); 165 } 166 167 /** 168 * Returns a instance of the class specified in the configuration property with the given name. If the class was 169 * previously instantiated by this method this instance is returned. 170 * If you call a method on the returned Optional directly you need to set the type like this: 171 * <pre> 172 * MCRConfiguration.<MCRMyType> getSingleInstanceOf(name, alternative) 173 * .ifPresent(myTypeObj -> myTypeObj.method()); 174 * </pre> 175 * 176 * @param name 177 * non-null and non-empty name of the configuration property 178 * @param alternative 179 * alternative class if property is undefined 180 * @return the instance of the class named by the value of the configuration property 181 * @throws MCRConfigurationException 182 * if the class can not be loaded or instantiated 183 */ 184 public static <T> Optional<T> getSingleInstanceOf(String name, Class<? extends T> alternative) { 185 return MCRConfiguration2.<T>getSingleInstanceOf(name) 186 .or(() -> Optional.ofNullable(alternative) 187 .map(className -> new MCRConfiguration2.SingletonKey(name, className.getName())) 188 .map(key -> (T) MCRConfiguration2.instanceHolder.computeIfAbsent(key, 189 (k) -> MCRConfigurableInstanceHelper.getInstance(alternative, Collections.emptyMap(), null)))); 190 } 191 192 /** 193 * Loads a Java Class defined in property <code>name</code>. 194 * @param name Name of the property 195 * @param <T> Supertype of class defined in <code>name</code> 196 * @return Optional of Class asignable to <code><T></code> 197 * @throws MCRConfigurationException 198 * if the the class can not be loaded or instantiated 199 */ 200 public static <T> Optional<Class<? extends T>> getClass(String name) throws MCRConfigurationException { 201 return getString(name).map(MCRConfiguration2::<T>getClassObject); 202 } 203 204 /** 205 * Returns the configuration property with the specified name. 206 * If the value of the property is empty after trimming the returned Optional is empty. 207 * @param name 208 * the non-null and non-empty name of the configuration property 209 * @return the value of the configuration property as an {@link Optional Optional<String>} 210 */ 211 public static Optional<String> getString(String name) { 212 return MCRConfigurationBase.getString(name) 213 .map(String::trim) 214 .filter(s -> !s.isEmpty()); 215 } 216 217 /** 218 * Returns the configuration property with the specified name as String. 219 * 220 * @param name 221 * the non-null and non-empty name of the configuration property 222 * @throws MCRConfigurationException 223 * if property is not set 224 */ 225 public static String getStringOrThrow(String name) { 226 return getString(name).orElseThrow(() -> createConfigurationException(name)); 227 } 228 229 /** 230 * Returns the configuration property with the specified name. 231 * 232 * @param name 233 * the non-null and non-empty name of the configuration property 234 * @param mapper 235 * maps the String value to the return value 236 * @throws MCRConfigurationException 237 * if property is not set 238 */ 239 public static <T> T getOrThrow(String name, Function<String, ? extends T> mapper) { 240 return getString(name).map(mapper).orElseThrow(() -> createConfigurationException(name)); 241 } 242 243 public static MCRConfigurationException createConfigurationException(String propertyName) { 244 return new MCRConfigurationException("Configuration property " + propertyName + " is not set."); 245 } 246 247 /** 248 * Splits a String value in a Stream of trimmed non-empty Strings. 249 * 250 * This method can be used to split a property value delimited by ',' into values. 251 * 252 * <p> 253 * Example: 254 * </p> 255 * <p> 256 * <code> 257 * MCRConfiguration2.getOrThrow("MCR.ListProp", MCRConfiguration2::splitValue)<br> 258 * .map(Integer::parseInt)<br> 259 * .collect(Collectors.toList())<br> 260 * </code> 261 * </p> 262 * @param value a property value 263 * @return a Stream of trimmed, non-empty Strings 264 */ 265 public static Stream<String> splitValue(String value) { 266 return MCRConfigurationBase.PROPERTY_SPLITTER.splitAsStream(value) 267 .map(String::trim) 268 .filter(s -> !s.isEmpty()); 269 } 270 271 /** 272 * @param prefix 273 * @return a list of properties which represent a configurable class 274 */ 275 public static Stream<String> getInstantiatablePropertyKeys(String prefix) { 276 return getSubPropertiesMap(prefix).entrySet() 277 .stream() 278 .filter(es -> { 279 String s = es.getKey(); 280 if (!s.contains(".")) { 281 return true; 282 } 283 284 return (s.endsWith(".class") || s.endsWith(".Class")) && 285 !s.substring(0, s.length() - ".class".length()).contains("."); 286 }) 287 .filter(es -> es.getValue() != null) 288 .filter(es -> !es.getValue().isBlank()) 289 .map(Map.Entry::getKey) 290 .map(prefix::concat); 291 } 292 293 /** 294 * Gets a list of properties which represent a configurable class and turns them in to a map. 295 * @param prefix 296 * @param <T> 297 * @return a map where the key is a String describing the configurable instance value 298 */ 299 public static <T> Map<String, Callable<T>> getInstances(String prefix) { 300 return getInstantiatablePropertyKeys(prefix) 301 .collect(Collectors.toMap(MCRConfigurableInstanceHelper::getIDFromClassProperty, v -> { 302 final String classProp = v; 303 return () -> (T) getInstanceOf(classProp).orElse(null); 304 })); 305 } 306 307 /** 308 * Returns the configuration property with the specified name as an <CODE> 309 * int</CODE> value. 310 * 311 * @param name 312 * the non-null and non-empty name of the configuration property 313 * @return the value of the configuration property as an <CODE>int</CODE> value 314 * @throws NumberFormatException 315 * if the configuration property is not an <CODE>int</CODE> value 316 */ 317 public static Optional<Integer> getInt(String name) throws NumberFormatException { 318 return getString(name).map(Integer::parseInt); 319 } 320 321 /** 322 * Returns the configuration property with the specified name as a <CODE> 323 * long</CODE> value. 324 * 325 * @param name 326 * the non-null and non-empty name of the configuration property 327 * @return the value of the configuration property as a <CODE>long</CODE> value 328 * @throws NumberFormatException 329 * if the configuration property is not a <CODE>long</CODE> value 330 */ 331 public static Optional<Long> getLong(String name) throws NumberFormatException { 332 return getString(name).map(Long::parseLong); 333 } 334 335 /** 336 * Returns the configuration property with the specified name as a <CODE> 337 * float</CODE> value. 338 * 339 * @param name 340 * the non-null and non-empty name of the configuration property 341 * @return the value of the configuration property as a <CODE>float</CODE> value 342 * @throws NumberFormatException 343 * if the configuration property is not a <CODE>float</CODE> value 344 */ 345 public static Optional<Float> getFloat(String name) throws NumberFormatException { 346 return getString(name).map(Float::parseFloat); 347 } 348 349 /** 350 * Returns the configuration property with the specified name as a <CODE> 351 * double</CODE> value. 352 * 353 * @param name 354 * the non-null and non-empty name of the configuration property 355 * @return the value of the configuration property as a <CODE>double 356 * </CODE> value 357 * @throws NumberFormatException 358 * if the configuration property is not a <CODE>double</CODE> value 359 */ 360 public static Optional<Double> getDouble(String name) throws NumberFormatException { 361 return getString(name).map(Double::parseDouble); 362 } 363 364 /** 365 * Returns the configuration property with the specified name as a <CODE> 366 * boolean</CODE> value. 367 * 368 * @param name 369 * the non-null and non-empty name of the configuration property 370 * @return <CODE>true</CODE>, if and only if the specified property has the value <CODE>true</CODE> 371 */ 372 public static Optional<Boolean> getBoolean(String name) { 373 return getString(name).map(Boolean::parseBoolean); 374 } 375 376 /** 377 * Sets the configuration property with the specified name to a new <CODE> 378 * String</CODE> value. If the parameter <CODE>value</CODE> is <CODE> 379 * null</CODE>, the property will be deleted. 380 * 381 * @param name 382 * the non-null and non-empty name of the configuration property 383 * @param value 384 * the new value of the configuration property, possibly <CODE> 385 * null</CODE> 386 */ 387 public static void set(final String name, String value) { 388 Optional<String> oldValue = MCRConfigurationBase.getStringUnchecked(name); 389 MCRConfigurationBase.set(name, value); 390 LISTENERS 391 .values() 392 .stream() 393 .filter(el -> el.keyPredicate.test(name)) 394 .forEach(el -> el.listener.accept(name, oldValue, Optional.ofNullable(value))); 395 } 396 397 public static void set(String name, Supplier<String> value) { 398 set(name, value.get()); 399 } 400 401 public static <T> void set(String name, T value, Function<T, String> mapper) { 402 set(name, mapper.apply(value)); 403 } 404 405 /** 406 * Adds a listener that is called after a new value is set. 407 * 408 * @param keyPredicate 409 * a filter upon the property name that if matches executes the listener 410 * @param listener 411 * a {@link MCRTriConsumer} with property name as first argument and than old and new value as Optional. 412 * @return a UUID to {@link #removePropertyChangeEventListener(UUID) remove the listener} later 413 */ 414 public static UUID addPropertyChangeEventLister(Predicate<String> keyPredicate, 415 MCRTriConsumer<String, Optional<String>, Optional<String>> listener) { 416 EventListener eventListener = new EventListener(keyPredicate, listener); 417 LISTENERS.put(eventListener.uuid, eventListener); 418 return eventListener.uuid; 419 } 420 421 public static boolean removePropertyChangeEventListener(UUID uuid) { 422 return LISTENERS.remove(uuid) != null; 423 } 424 425 public static <T> T instantiateClass(String classname) { 426 LogManager.getLogger().debug("Loading Class: {}", classname); 427 428 Class<? extends T> cl = getClassObject(classname); 429 return MCRConfigurableInstanceHelper.getInstance(cl, Collections.emptyMap(), null); 430 } 431 432 private static <T> Class<? extends T> getClassObject(String classname) { 433 try { 434 return MCRClassTools.forName(classname.trim()); 435 } catch (ClassNotFoundException ex) { 436 throw new MCRConfigurationException("Could not load class.", ex); 437 } 438 } 439 440 private static class EventListener { 441 442 private Predicate<String> keyPredicate; 443 444 private MCRTriConsumer<String, Optional<String>, Optional<String>> listener; 445 446 private UUID uuid; 447 448 EventListener(Predicate<String> keyPredicate, 449 MCRTriConsumer<String, Optional<String>, Optional<String>> listener) { 450 this.keyPredicate = keyPredicate; 451 this.listener = listener; 452 this.uuid = UUID.randomUUID(); 453 } 454 455 } 456 457 static class SingletonKey { 458 private String property, className; 459 460 SingletonKey(String property, String className) { 461 super(); 462 this.property = property; 463 this.className = className; 464 } 465 466 @Override 467 public int hashCode() { 468 final int prime = 31; 469 int result = 1; 470 result = prime * result + ((className == null) ? 0 : className.hashCode()); 471 result = prime * result + ((property == null) ? 0 : property.hashCode()); 472 return result; 473 } 474 475 @Override 476 public boolean equals(Object obj) { 477 if (this == obj) { 478 return true; 479 } 480 if (obj == null) { 481 return false; 482 } 483 if (getClass() != obj.getClass()) { 484 return false; 485 } 486 SingletonKey other = (SingletonKey) obj; 487 if (className == null) { 488 if (other.className != null) { 489 return false; 490 } 491 } else if (!className.equals(other.className)) { 492 return false; 493 } 494 if (property == null) { 495 return other.property == null; 496 } else { 497 return property.equals(other.property); 498 } 499 } 500 } 501 502 }