org.mycore.common.config.MCRConfiguration2 Maven / Gradle / Ivy
/*
* This file is part of *** M y C o R e ***
* See http://www.mycore.de/ for details.
*
* MyCoRe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* MyCoRe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MyCoRe. If not, see .
*/
package org.mycore.common.config;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.logging.log4j.LogManager;
import org.mycore.common.MCRClassTools;
import org.mycore.common.function.MCRTriConsumer;
import org.mycore.common.inject.MCRInjectorConfig;
import com.google.inject.ConfigurationException;
/**
* Provides methods to manage and read all configuration properties from the MyCoRe configuration files.
* The Properties used by this class are used from {@link MCRConfigurationBase}.
* NOTE
* All {@link Optional} values returned by this class are {@link Optional#empty() empty} if the property
* is not set OR the trimmed value {@link String#isEmpty() is empty}. If you want to distinguish between
* empty properties and unset properties use {@link MCRConfigurationBase#getString(String)} instead.
*
*
* Using this class is very easy, here is an example:
*
*
* // Get a configuration property as a String:
* String sValue = MCRConfiguration2.getString("MCR.String.Value").orElse(defaultValue);
*
* // Get a configuration property as a List of String (values are seperated by ","):
* List<String> lValue = MCRConfiguration2.getString("MCR.StringList.Value").stream()
* .flatMap(MCRConfiguration2::splitValue)
* .collect(Collectors.toList());
*
* // Get a configuration property as a long array (values are seperated by ","):
* long[] la = MCRConfiguration2.getString("MCR.LongList.Value").stream()
* .flatMap(MCRConfiguration2::splitValue)
* .mapToLong(Long::parseLong)
* .toArray();
*
* // Get a configuration property as an int, use 500 as default if not set:
* int max = MCRConfiguration2.getInt("MCR.Cache.Size").orElse(500);
*
*
* There are some helper methods to help you with converting values
*
* - {@link #getOrThrow(String, Function)}
* - {@link #splitValue(String)}
* - {@link #instantiateClass(String)}
*
*
* As you see, the class provides methods to get configuration properties as different data types and allows you to
* specify defaults. All MyCoRe configuration properties should start with "MCR.
"
*
* Using the set
methods allows client code to set new configuration properties or
* overwrite existing ones with new values.
*
* @author Thomas Scheffler (yagee)
* @since 2018.05
*/
public class MCRConfiguration2 {
private static ConcurrentHashMap LISTENERS = new ConcurrentHashMap<>();
static ConcurrentHashMap instanceHolder = new ConcurrentHashMap<>();
public static Map getPropertiesMap() {
return Collections.unmodifiableMap(MCRConfigurationBase.getResolvedProperties().getAsMap());
}
/**
* Returns a sub map of properties where key is transformed.
*
*
* - if property starts with
propertyPrefix
, the property is in the result map
* - the key of the target map is the name of the property without
propertPrefix
*
* Example for propertyPrefix="MCR.Foo."
:
*
* MCR.Foo.Bar=Baz
* MCR.Foo.Hello=World
* MCR.Other.Prop=Value
*
* will result in
*
* Bar=Baz
* Hello=World
*
* @param propertyPrefix prefix of the property name
* @return a map of the properties as stated above
*/
public static Map getSubPropertiesMap(String propertyPrefix) {
return MCRConfigurationBase.getResolvedProperties()
.getAsMap()
.entrySet()
.stream()
.filter(e -> e.getKey().startsWith(propertyPrefix))
.collect(Collectors.toMap(e -> e.getKey().substring(propertyPrefix.length()), Map.Entry::getValue));
}
/**
* Returns a new instance of the class specified in the configuration property with the given name.
* If you call a method on the returned Optional directly you need to set the type like this:
*
* MCRConfiguration.<MCRMyType> getInstanceOf(name)
* .ifPresent(myTypeObj -> myTypeObj.method());
*
*
* @param name
* the non-null and non-empty name of the configuration property
* @return the value of the configuration property as a String, or null
* @throws MCRConfigurationException
* if the class can not be loaded or instantiated
*/
public static Optional getInstanceOf(String name) throws MCRConfigurationException {
return getString(name).map(MCRConfiguration2::instantiateClass);
}
/**
* Returns a instance of the class specified in the configuration property with the given name. If the class was
* previously instantiated by this method this instance is returned.
* If you call a method on the returned Optional directly you need to set the type like this:
*
* MCRConfiguration.<MCRMyType> getSingleInstanceOf(name)
* .ifPresent(myTypeObj -> myTypeObj.method());
*
*
* @param name
* non-null and non-empty name of the configuration property
* @return the instance of the class named by the value of the configuration property
* @throws MCRConfigurationException
* if the class can not be loaded or instantiated
*/
public static Optional getSingleInstanceOf(String name) {
return getString(name)
.map(className -> new SingletonKey(name, className))
.map(key -> (T) instanceHolder.computeIfAbsent(key, k -> getInstanceOf(name).orElse(null)));
}
/**
* Returns a instance of the class specified in the configuration property with the given name. If the class was
* previously instantiated by this method this instance is returned.
* If you call a method on the returned Optional directly you need to set the type like this:
*
* MCRConfiguration.<MCRMyType> getSingleInstanceOf(name, alternative)
* .ifPresent(myTypeObj -> myTypeObj.method());
*
*
* @param name
* non-null and non-empty name of the configuration property
* @param alternative
* alternative class if property is undefined
* @return the instance of the class named by the value of the configuration property
* @throws MCRConfigurationException
* if the class can not be loaded or instantiated
*/
public static Optional getSingleInstanceOf(String name, Class alternative) {
return MCRConfiguration2.getSingleInstanceOf(name)
.or(() -> Optional.ofNullable(alternative)
.map(className -> new MCRConfiguration2.SingletonKey(name, className.getName()))
.map(key -> (T) MCRConfiguration2.instanceHolder.computeIfAbsent(key,
k -> instantiateClass(alternative))));
}
/**
* Loads a Java Class defined in property name
.
* @param name Name of the property
* @param Supertype of class defined in name
* @return Optional of Class asignable to <T>
* @throws MCRConfigurationException
* if the the class can not be loaded or instantiated
*/
public static Optional> getClass(String name) throws MCRConfigurationException {
return getString(name).map(MCRConfiguration2::getClassObject);
}
/**
* Returns the configuration property with the specified name.
* If the value of the property is empty after trimming the returned Optional is empty.
* @param name
* the non-null and non-empty name of the configuration property
* @return the value of the configuration property as an {@link Optional Optional<String>}
*/
public static Optional getString(String name) {
return MCRConfigurationBase.getString(name)
.map(String::trim)
.filter(s -> !s.isEmpty());
}
/**
* Returns the configuration property with the specified name as String.
*
* @param name
* the non-null and non-empty name of the configuration property
* @throws MCRConfigurationException
* if property is not set
*/
public static String getStringOrThrow(String name) {
return getString(name).orElseThrow(() -> createConfigurationException(name));
}
/**
* Returns the configuration property with the specified name.
*
* @param name
* the non-null and non-empty name of the configuration property
* @param mapper
* maps the String value to the return value
* @throws MCRConfigurationException
* if property is not set
*/
public static T getOrThrow(String name, Function mapper) {
return getString(name).map(mapper).orElseThrow(() -> createConfigurationException(name));
}
public static MCRConfigurationException createConfigurationException(String propertyName) {
return new MCRConfigurationException("Configuration property " + propertyName + " is not set.");
}
/**
* Splits a String value in a Stream of trimmed non-empty Strings.
*
* This method can be used to split a property value delimited by ',' into values.
*
*
* Example:
*
*
*
* MCRConfiguration2.getOrThrow("MCR.ListProp", MCRConfiguration2::splitValue)
* .map(Integer::parseInt)
* .collect(Collectors.toList())
*
*
* @param value a property value
* @return a Stream of trimmed, non-empty Strings
*/
public static Stream splitValue(String value) {
return MCRConfigurationBase.PROPERTY_SPLITTER.splitAsStream(value)
.map(String::trim)
.filter(s -> !s.isEmpty());
}
/**
* Returns the configuration property with the specified name as an
* int
value.
*
* @param name
* the non-null and non-empty name of the configuration property
* @return the value of the configuration property as an int
value
* @throws NumberFormatException
* if the configuration property is not an int
value
*/
public static Optional getInt(String name) throws NumberFormatException {
return getString(name).map(Integer::parseInt);
}
/**
* Returns the configuration property with the specified name as a
* long
value.
*
* @param name
* the non-null and non-empty name of the configuration property
* @return the value of the configuration property as a long
value
* @throws NumberFormatException
* if the configuration property is not a long
value
*/
public static Optional getLong(String name) throws NumberFormatException {
return getString(name).map(Long::parseLong);
}
/**
* Returns the configuration property with the specified name as a
* float
value.
*
* @param name
* the non-null and non-empty name of the configuration property
* @return the value of the configuration property as a float
value
* @throws NumberFormatException
* if the configuration property is not a float
value
*/
public static Optional getFloat(String name) throws NumberFormatException {
return getString(name).map(Float::parseFloat);
}
/**
* Returns the configuration property with the specified name as a
* double
value.
*
* @param name
* the non-null and non-empty name of the configuration property
* @return the value of the configuration property as a double
*
value
* @throws NumberFormatException
* if the configuration property is not a double
value
*/
public static Optional getDouble(String name) throws NumberFormatException {
return getString(name).map(Double::parseDouble);
}
/**
* Returns the configuration property with the specified name as a
* boolean
value.
*
* @param name
* the non-null and non-empty name of the configuration property
* @return true
, if and only if the specified property has the value true
*/
public static Optional getBoolean(String name) {
return getString(name).map(Boolean::parseBoolean);
}
/**
* Sets the configuration property with the specified name to a new
* String
value. If the parameter value
is
* null
, the property will be deleted.
*
* @param name
* the non-null and non-empty name of the configuration property
* @param value
* the new value of the configuration property, possibly
* null
*/
public static void set(final String name, String value) {
Optional oldValue = MCRConfigurationBase.getStringUnchecked(name);
MCRConfigurationBase.set(name, value);
LISTENERS
.values()
.stream()
.filter(el -> el.keyPredicate.test(name))
.forEach(el -> el.listener.accept(name, oldValue, Optional.ofNullable(value)));
}
public static void set(String name, Supplier value) {
set(name, value.get());
}
public static void set(String name, T value, Function mapper) {
set(name, mapper.apply(value));
}
/**
* Adds a listener that is called after a new value is set.
*
* @param keyPredicate
* a filter upon the property name that if matches executes the listener
* @param listener
* a {@link MCRTriConsumer} with property name as first argument and than old and new value as Optional.
* @return a UUID to {@link #removePropertyChangeEventListener(UUID) remove the listener} later
*/
public static UUID addPropertyChangeEventLister(Predicate keyPredicate,
MCRTriConsumer, Optional> listener) {
EventListener eventListener = new EventListener(keyPredicate, listener);
LISTENERS.put(eventListener.uuid, eventListener);
return eventListener.uuid;
}
public static boolean removePropertyChangeEventListener(UUID uuid) {
return LISTENERS.remove(uuid) != null;
}
public static T instantiateClass(String classname) {
LogManager.getLogger().debug("Loading Class: {}", classname);
Class cl = getClassObject(classname);
return instantiateClass(cl);
}
private static T instantiateClass(Class cl) {
try {
return MCRInjectorConfig.injector().getInstance(cl);
} catch (ConfigurationException e) {
// no default or injectable constructor, check for singleton factory method
try {
return (T) Stream.of(cl.getMethods())
.filter(m -> m.getReturnType().isAssignableFrom(cl))
.filter(m -> Modifier.isStatic(m.getModifiers()))
.filter(m -> Modifier.isPublic(m.getModifiers()))
.filter(m -> m.getName().toLowerCase(Locale.ROOT).contains("instance"))
.findAny()
.orElseThrow(() -> new MCRConfigurationException("Could not instantiate class " + cl.getName(), e))
.invoke(cl, (Object[]) null);
} catch (ReflectiveOperationException r) {
throw new MCRConfigurationException("Could not instantiate class " + cl.getName(), r);
}
}
}
private static Class getClassObject(String classname) {
try {
return MCRClassTools.forName(classname.trim());
} catch (ClassNotFoundException ex) {
throw new MCRConfigurationException("Could not load class.", ex);
}
}
private static class EventListener {
private Predicate keyPredicate;
private MCRTriConsumer, Optional> listener;
private UUID uuid;
EventListener(Predicate keyPredicate,
MCRTriConsumer, Optional> listener) {
this.keyPredicate = keyPredicate;
this.listener = listener;
this.uuid = UUID.randomUUID();
}
}
static class SingletonKey {
private String property, className;
SingletonKey(String property, String className) {
super();
this.property = property;
this.className = className;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((className == null) ? 0 : className.hashCode());
result = prime * result + ((property == null) ? 0 : property.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
SingletonKey other = (SingletonKey) obj;
if (className == null) {
if (other.className != null) {
return false;
}
} else if (!className.equals(other.className)) {
return false;
}
if (property == null) {
return other.property == null;
} else {
return property.equals(other.property);
}
}
}
}