package org.ovirt.engine.core.extensions.mgr; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; public abstract class Configuration { /** * Load the configuration from a properties file. * * @param file the properties file * @throws IOException if anything fails while loading the properties file */ public static Configuration loadFile(File file) throws IOException { Properties properties = new Properties(); try (InputStream in = new FileInputStream(file)) { properties.load(in); } return new Root(file, properties); } /** * This is a cache of prefix views used to avoid creating the same view object multiple times. */ private Map<String, Configuration> prefixViews; /** * This is a cache of typed views used to avoid creating the same view object multiple times. */ private Map<Class<?>, Object> typedViews; /** * Create a new empty configuration object. Subclasses should provide their own constructor, and the first thing it * should do is call this one. */ protected Configuration() { prefixViews = new HashMap<>(); typedViews = new HashMap<>(); } /** * Gets the file that this configuration has been loaded from. * * @return the reference to the file object */ public abstract File getFile(); /** * Gets the properties object that is associated with the configuration hierarchy * * @return the reference to the properties object */ public abstract Properties getProperties(); /** * Gets the value associated to a configuration parameter, searching it only the this configuration. * * @param key the name of the configuration parameter * @return the value of the configuration parameter or {@code null} if no such parameter exists */ public abstract String getString(String key); /** * Get the list of the parameter names contained in this configuration object. * * @return a list containing the key names, with no particular order */ public abstract List<String> getKeys(); /** * This method calculates complete key names. It is intended basically for log messages. For example, if the * configuration used to build an object is a view of another configuration with the prefix {@code directory} and * we are using a key named {@code name} we want to display in the logs {@code directory.name} and not just * {@code name}. * * @param key the relative key name * @return the absolute key name, including all the prefixes if the configuration is a subset of another * configuration */ public abstract String getAbsoluteKey(String key); /** * Returns a configuration object that represents a view of the parameters that start with a given prefix. These * objects are cached, so if the same prefix is requested twice the same object will be returned. * * @param prefix the prefix * @return the configuration object, will never be {@code null} */ public synchronized Configuration getView(String prefix) { Configuration view = prefixViews.get(prefix); if (view == null) { view = new View(this, prefix); prefixViews.put(prefix, view); } return view; } /** * Returns an object that implements the given interface and whose getters obtain the values from the given * configuration. For example, if you have the need to manage two parameters containing a host name and * a port number you can you can create a {@code Address} interface like this: * * <pre> * public interface Address { * public String getHost(); * public int getPort(); * } * </pre> * * When using this interface the names of the parameters will be derived from the names of the Java Beans properties * of the interface, in this case they will be {@code host} and {@code port}. To actually use this interface write * something like this: * * <pre> * Configuration config = ...; * Address address = config.getView("server", Address.class); * System.out.println(address.getHost()); * System.out.println(address.getPort()); * </pre> * * It is equivalent to this: * * <pre> * Configuration config = ...; * Configuration address = config.getView("server"); * System.out.println(address.getString("host")); * System.out.println(address.getInt("port")); * </pre> * * @param type the class object corresponding to the interface type that defines view * @param <V> the interface type that defines the view * @return the configuration object, will never be {@code null} */ public synchronized <V> V getView(Class<V> type) { Object view = typedViews.get(type); if (view == null) { view = Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, new Handler(this)); typedViews.put(type, view); } return (V) view; } /** * Gets the parent configuration. * * @return the parent configuration if this is a view or {@code null} if this is a root configuration */ public Configuration getParent() { return null; } /** * Checks if this configuration object contains any parameter. * * @return {@code true} if the configuration object doesn't contain any parameter, {@code false} otherwise */ public boolean isEmpty() { return getKeys().isEmpty(); } public boolean getBoolean(String key, boolean defaultValueIfNull) { String image = getString(key); if (image == null) { return defaultValueIfNull; } return Boolean.parseBoolean(image); } public boolean getBoolean(String key) { return getBoolean(key, false); } public Integer getInteger(String key) { String image = getString(key); if (image == null) { return null; } return Integer.parseInt(image); } public String[] getArray(String key) { String image = getString(key); if (image == null) { return null; } return image.split(","); } public List<String> getList(String key) { String[] array = getArray(key); if (array == null) { return null; } return Arrays.asList(array); } public <E extends Enum<E>> E getEnum(Class<E> type, String key) { String value = getString(key); if (value == null) { return null; } return Enum.valueOf(type, value); } public File getFile(String key) { String value = getString(key); if (value == null) { return null; } return new File(value); } /** * This class is the root of the hierarchy, backed by a simple properties object. */ private static class Root extends Configuration { /** * The file that the configuration has been loaded from. */ private File file; /** * The actual values of the configuration are stored in this properties object. */ private Properties properties; /** * Creates a configuration object that serves as the root of a hierarchy. */ private Root(File sourceFile, Properties properties) { super(); this.properties = properties; this.file = sourceFile; } /** * {@inheritDoc} */ @Override public File getFile() { return file; } /** * {@inheritDoc} */ @Override public Configuration getParent() { return null; } /** * {@inheritDoc} */ @Override public String getString(String key) { return properties.getProperty(key); } /** * {@inheritDoc} */ @Override public String getAbsoluteKey(String key) { return key; } /** * {@inheritDoc} */ @Override public List<String> getKeys() { List<String> keys = new LinkedList<>(); Enumeration<?> elements = properties.propertyNames(); while (elements.hasMoreElements()) { String next = (String) elements.nextElement(); keys.add(next); } return Collections.unmodifiableList(keys); } @Override public Properties getProperties() { return properties; } } /** * This class is a view on top of another configuration. It view consist on adding a prefix to all the parameters. */ private static class View extends Configuration { // This configuration doesn't actually store anything, it all comes from a parent configuration adding a prefix // to all the parameters: private Configuration parent; private String prefix; /** * Creates a configuration object that represents the subset of parameters that start with the given prefix. * When a parameter is requested from this configuration object the prefix will be added to the given name and * the value will then be obtained from the parent. * * @param parent the parent configuration object * @param prefix the prefix that will be added to parameter names before retrieving values from the parent * configuration object */ private View(Configuration parent, String prefix) { this.parent = parent; this.prefix = prefix; } /** * {@inheritDoc} */ @Override public File getFile() { return parent.getFile(); } /** * {@inheritDoc} */ @Override public Configuration getParent() { return parent; } /** * {@inheritDoc} */ @Override public String getString(String key) { return parent.getString(prefix + "." + key); } /** * {@inheritDoc} */ @Override public String getAbsoluteKey(String key) { return parent.getAbsoluteKey(prefix + "." + key); } /** * {@inheritDoc} */ @Override public List<String> getKeys() { List<String> keys = new LinkedList<>(); for (String key : parent.getKeys()) { if (key.startsWith(prefix + ".")) { key = key.substring(prefix.length() + 1); keys.add(key); } } return Collections.unmodifiableList(keys); } @Override public Properties getProperties() { return parent.getProperties(); } } /** * This class handles invocations of typed configurations. */ private static class Handler implements InvocationHandler { private static final String GET = "get"; private static final String IS = "is"; /** * This is the raw configuration handled by this instance. */ private Configuration config; public Handler(Configuration config) { this.config = config; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Get key from the name of the method: String key = getKey(method); // Convert the value to the return type of the method: Class<?> returnType = method.getReturnType(); if (returnType == String.class) { return config.getString(key); } if (returnType == Boolean.class || returnType == Boolean.TYPE) { return config.getBoolean(key); } if (returnType == Integer.class || returnType == Integer.TYPE) { return config.getInteger(key); } if (returnType.isEnum()) { return config.getEnum((Class<Enum>) returnType, key); } if (returnType.isArray() && returnType.getComponentType() == String.class) { return config.getArray(key); } if (returnType == List.class) { return config.getList(key); } if (returnType == File.class) { return config.getFile(key); } if (returnType.isInterface()) { return config.getView(key).getView(returnType); } // If we haven't converted the value before it means that it isn't supported: throw new IllegalArgumentException( "The return type of method \"" + method.getName() + "\" of view " + "class \"" + method.getDeclaringClass().getName() + "\" isn't supported." ); } /** * Calculates the name of the configuration key from the name of the getter. * * @param method the reference to the getter * @return the name of the key */ private String getKey(Method method) { String name = method.getName(); if (name.startsWith(GET)) { return name.substring(GET.length()).toLowerCase(); } if (name.startsWith(IS)) { return name.substring(IS.length()).toLowerCase(); } throw new IllegalArgumentException( "The method \"" + name + "\" of view class \"" + method.getDeclaringClass().getName() + "\" isn't a " + "a valid getter, it doesn't start with \"" + GET + "\" or \"" + IS + "\"." ); } } }