View Javadoc
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&lt;String&gt; 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.&lt;MCRMyType&gt; getInstanceOf(name)
128      *     .ifPresent(myTypeObj -&gt; 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.&lt;MCRMyType&gt; getSingleInstanceOf(name)
151      *     .ifPresent(myTypeObj -&gt; 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.&lt;MCRMyType&gt; getSingleInstanceOf(name, alternative)
173      *     .ifPresent(myTypeObj -&gt; 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>&lt;T&gt;</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&lt;String&gt;}
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 }