package co.codewizards.cloudstore.core.util; import static co.codewizards.cloudstore.core.io.StreamUtil.*; import static co.codewizards.cloudstore.core.oio.OioFileFactory.*; import static co.codewizards.cloudstore.core.util.AssertUtil.*; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import co.codewizards.cloudstore.core.oio.File; /** * {@link java.util.Properties} utilities. * @author Marc Klinger - marc[at]nightlabs[dot]de * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de */ public final class PropertiesUtil { private static final Logger logger = LoggerFactory.getLogger(PropertiesUtil.class); private PropertiesUtil() { } /** * Suffix appended to the real property-key to store the boolean flag whether * the real property represents the <code>null</code> value. * <p> * It is not possible to store a <code>null</code> value in a {@link Properties} instance (and neither it is * in a properties file). But sometimes it is necessary to explicitly formulate a <code>null</code> value, * for example when overriding a property in a way as if it had not been specified in the overridden properties. * <p> * For example, let there be these properties declared in a persistence unit: * <pre> * javax.jdo.option.ConnectionFactoryName = jdbc/someDataSource * javax.jdo.option.TransactionType = JTA * javax.persistence.jtaDataSource = jdbc/someDataSource * javax.persistence.transactionType = JTA * </pre> * <p> * If the transaction type is to be overridden by "RESOURCE_LOCAL", this is straight-forward: * <p> * <pre> * javax.jdo.option.TransactionType = RESOURCE_LOCAL * javax.persistence.transactionType = RESOURCE_LOCAL * </pre> * <p> * But to override the datasource properties to be null, is not possible by simply writing * "javax.jdo.option.ConnectionFactoryName = = " as this would * be interpreted as empty string. Therefore it is possible to declare the <code>null</code> value using an additional * key: * <pre> * javax.jdo.option.ConnectionFactoryName = jdbc/someDataSource * javax.jdo.option.ConnectionFactoryName.null = true * javax.persistence.jtaDataSource.null = true * </pre> * It is not necessary to quote the referenced key as shown in the 2nd example ("javax.persistence.jtaDataSource" is * not present). However, if it is present ("javax.jdo.option.ConnectionFactoryName" above), the * null-indicating meta-property "javax.jdo.option.ConnectionFactoryName.null" overrides the value "jdbc/someDataSource". * * @see #getMetaPropertyKeyNullValue(String) * @see #filterProperties(Map, Map) */ public static final String SUFFIX_NULL_VALUE = ".null"; /** * Get all matches where property keys match the given pattern. * @param properties The properties to match * @param pattern The pattern to match against * @return The {@link Matcher}s that matched. */ public static Collection<Matcher> getPropertyKeyMatches(final java.util.Properties properties, final Pattern pattern) { final Collection<Matcher> matches = new ArrayList<Matcher>(); for (final Object element : properties.keySet()) { final String key = (String) element; final Matcher m = pattern.matcher(key); if(m.matches()) matches.add(m); } return matches; } /** * Get all properties whose keys start with the given prefix. * <p> * The returned property elements have the form <code>(key,value)</code> where <code>key</code> is * the part of the original key after the given <code>keyPrefix</code> and <code>value</code> is * the original value. * </p> * @param properties The properties to filter. * @param keyPrefix The kex prefix to use * @return the properties that start with the given prefix */ public static java.util.Properties getProperties(final java.util.Properties properties, final String keyPrefix) { final java.util.Properties newProperties = new java.util.Properties(); final Collection<Matcher> matches = getPropertyKeyMatches(properties, Pattern.compile("^"+Pattern.quote(keyPrefix)+"(.*)$")); for (final Matcher m : matches) newProperties.put(m.group(1), properties.get(m.group(0))); return newProperties; } public static void putAll(final java.util.Properties source, final java.util.Properties target) { for (final Object element : source.keySet()) { final String key = (String) element; target.setProperty(key, source.getProperty(key)); } } public static java.util.Properties load(final String filename) throws IOException { return load(filename != null ? createFile(filename) : null); } public static java.util.Properties load(final File file) throws IOException { final InputStream in = castStream(file.createInputStream()); try { final java.util.Properties properties = new java.util.Properties(); properties.load(in); return properties; } finally { in.close(); } } public static void store(final String filename, final java.util.Properties properties, final String comment) throws IOException { store(filename != null ? createFile(filename) : null, properties, comment); } public static void store(final File file, final java.util.Properties properties, final String comment) throws IOException { final OutputStream out = castStream(file.createOutputStream()); try { properties.store(out, comment); } finally { out.close(); } } /** * Filter the given raw properties. * <p> * This is a convenience method delegating to * {@link #filterProperties(Map, Map)} with <code>variables == null</code>. * @param rawProperties the properties to be filtered; must not be <code>null</code>. * @return the filtered properties. * @see #filterProperties(Map, Map) */ public static Map<String, String> filterProperties(final Map<?, ?> rawProperties) { return filterProperties(rawProperties, null); } /** * Filter the given raw properties. * <p> * <u>Replace null-meta-data to <code>null</code> values:</u> Every property for which the * method {@link #isNullValue(Map, String)} returns <code>true</code> is written with a <code>null</code> * value into the result-map. Every property for which the method {@link #isMetaPropertyKeyNullValue(String)} * returns <code>true</code>, is ignored (i.e. does not occur in the result-map). * <p> * It is not possible to store a <code>null</code> value in a {@link Properties} instance (and neither it is * in a properties file). But sometimes it is necessary to explicitly formulate a <code>null</code> value, * for example when overriding a property in a way as if it had not been specified in the overridden properties. * <p> * For example, let there be these properties declared in a persistence unit: * <pre> * javax.jdo.option.ConnectionFactoryName = jdbc/someDataSource * javax.jdo.option.TransactionType = JTA * javax.persistence.jtaDataSource = jdbc/someDataSource * javax.persistence.transactionType = JTA * </pre> * <p> * If the transaction type is to be overridden by "RESOURCE_LOCAL", this is straight-forward: * <p> * <pre> * javax.jdo.option.TransactionType = RESOURCE_LOCAL * javax.persistence.transactionType = RESOURCE_LOCAL * </pre> * <p> * But to override the datasource properties to be null, is not possible by simply writing * "javax.jdo.option.ConnectionFactoryName = = " as this would * be interpreted as empty string. Therefore it is possible to declare the <code>null</code> value using an additional * key: * <pre> * javax.jdo.option.ConnectionFactoryName = jdbc/someDataSource * javax.jdo.option.ConnectionFactoryName.null = true * javax.persistence.jtaDataSource.null = true * </pre> * It is not necessary to quote the referenced key as shown in the 2nd example ("javax.persistence.jtaDataSource" is * not present). However, if it is present ("javax.jdo.option.ConnectionFactoryName" above), the * null-indicating meta-property "javax.jdo.option.ConnectionFactoryName.null" overrides the value "jdbc/someDataSource". * <p> * <u>Replace template variables:</u> If the optional <code>variables</code> argument is present, every * value is filtered using {@link IOUtil#replaceTemplateVariables(String, Map)}. * <p> * For example, let there be these properties: * <pre> * some.url1 = file:${java.io.tmpdir}/myTempDir * some.url2 = http://host.domain.tld/${user.name} * </pre> * <p> * If this method is called with {@link System#getProperties()} as <code>variables</code> and the user "marco" is currently * working on a linux machine, the resulting <code>Map</code> will contain the following resolved properties: * <pre> * some.url1 = file:/tmp/myTempDir * some.url2 = http://host.domain.tld/marco * </pre> * @param rawProperties the properties to be filtered; must not be <code>null</code>. * @param variables optional template variables; if present, every value is filtered using * {@link IOUtil#replaceTemplateVariables(String, Map)}. * @return the filtered properties. */ public static Map<String, String> filterProperties(final Map<?, ?> rawProperties, final Map<?, ?> variables) { if (rawProperties == null) throw new IllegalArgumentException("rawProperties == null"); final Map<String, String> filteredProperties = new HashMap<String, String>(); for (final Map.Entry<?, ?> me : rawProperties.entrySet()) { final String key = me.getKey() == null ? null : me.getKey().toString(); String value = me.getValue() == null ? null : me.getValue().toString(); if (isMetaPropertyKey(key)) { if (isMetaPropertyKeyNullValue(key) && Boolean.parseBoolean(value)) { final String refKey = getReferencedPropertyKeyForMetaPropertyKey(key); filteredProperties.put(refKey, null); } continue; } if (value != null && isNullValue(rawProperties, key)) value = null; if (value != null && variables != null) value = IOUtil.replaceTemplateVariables(value, variables); filteredProperties.put(key, value); } return filteredProperties; } /** * Determine, if the given property-key is a <code>null</code>-indicating meta-property for another property. * <p> * It is not possible to store a <code>null</code> value in a {@link Properties} instance (and neither it is * in a properties file). But sometimes it is necessary to explicitly formulate a <code>null</code> value, * for example when overriding a property in a way as if it had not been specified in the overridden properties. * <p> * For example, let there be these properties declared in a persistence unit: * <pre> * javax.jdo.option.ConnectionFactoryName = jdbc/someDataSource * javax.jdo.option.TransactionType = JTA * javax.persistence.jtaDataSource = jdbc/someDataSource * javax.persistence.transactionType = JTA * </pre> * <p> * If the transaction type is to be overridden by "RESOURCE_LOCAL", this is straight-forward: * <p> * <pre> * javax.jdo.option.TransactionType = RESOURCE_LOCAL * javax.persistence.transactionType = RESOURCE_LOCAL * </pre> * <p> * But to override the datasource properties to be null, is not possible by simply writing * "javax.jdo.option.ConnectionFactoryName = = " as this would * be interpreted as empty string. Therefore it is possible to declare the <code>null</code> value using an additional * key: * <pre> * javax.jdo.option.ConnectionFactoryName = jdbc/someDataSource * javax.jdo.option.ConnectionFactoryName.null = true * javax.persistence.jtaDataSource.null = true * </pre> * It is not necessary to quote the referenced key as shown in the 2nd example ("javax.persistence.jtaDataSource" is * not present). However, if it is present ("javax.jdo.option.ConnectionFactoryName" above), the * null-indicating meta-property "javax.jdo.option.ConnectionFactoryName.null" overrides the value "jdbc/someDataSource". * * @param key the property-key to check. * @return <code>true</code>, if the given key references a property that is a <code>null</code>-indicating * meta-property for another property. * @see #isMetaPropertyKey(String) */ public static boolean isMetaPropertyKeyNullValue(final String key) { if (key == null) return false; return key.endsWith(SUFFIX_NULL_VALUE); } /** * Determine, if the given property-key is a meta-property for another property. * <p> * Currently, this is equivalent to {@link #isMetaPropertyKeyNullValue(String)}, but other * meta-properties might be introduced later. * @param key the property-key to check. * @return <code>true</code>, if the given key references a property that is a meta-property * for another property. */ public static boolean isMetaPropertyKey(final String key) { return isMetaPropertyKeyNullValue(key); } /** * Get the referenced property-key for the given meta-property's key. * @param key a meta-property's key - for example the <code>null</code>-indicating property-key * "some.prop.null". * @return the referenced property-key - for example "some.prop". * @see #SUFFIX_NULL_VALUE * @see #getMetaPropertyKeyNullValue(String) */ public static String getReferencedPropertyKeyForMetaPropertyKey(final String key) { if (!isMetaPropertyKeyNullValue(key)) throw new IllegalArgumentException("key='" + key + "' is not a meta-property!"); return key.substring(0, key.length() - SUFFIX_NULL_VALUE.length()); } /** * Get the <code>null</code>-indicating meta-property's key for the given real property's key. * @param key a property-key - for example "some.prop". * @return the <code>null</code>-indicating meta-property's key - for example "some.prop.null". * @see #SUFFIX_NULL_VALUE * @see #getReferencedPropertyKeyForMetaPropertyKey(String) */ public static String getMetaPropertyKeyNullValue(String key) { if (key == null) key = String.valueOf(key); if (isMetaPropertyKeyNullValue(key)) throw new IllegalArgumentException("key='" + key + "' is already a meta-property indicating a null-value!"); return key + SUFFIX_NULL_VALUE; } /** * Determine, if the property identified by the given <code>key</code> has a <code>null</code>-value. * <p> * It is not possible to store a <code>null</code> value in a {@link Properties} instance (and neither it is * in a properties file). But sometimes it is necessary to explicitly formulate a <code>null</code> value, * for example when overriding a property in a way as if it had not been specified in the overridden properties. * <p> * For example, let there be these properties declared in a persistence unit: * <pre> * javax.jdo.option.ConnectionFactoryName = jdbc/someDataSource * javax.jdo.option.TransactionType = JTA * javax.persistence.jtaDataSource = jdbc/someDataSource * javax.persistence.transactionType = JTA * </pre> * <p> * If the transaction type is to be overridden by "RESOURCE_LOCAL", this is straight-forward: * <p> * <pre> * javax.jdo.option.TransactionType = RESOURCE_LOCAL * javax.persistence.transactionType = RESOURCE_LOCAL * </pre> * <p> * But to override the datasource properties to be null, is not possible by simply writing * "javax.jdo.option.ConnectionFactoryName = " as this would * be interpreted as empty string. Therefore it is possible to declare the <code>null</code> value using an additional * key: * <pre> * javax.jdo.option.ConnectionFactoryName = jdbc/someDataSource * javax.jdo.option.ConnectionFactoryName.null = true * javax.persistence.jtaDataSource.null = true * </pre> * It is not necessary to quote the referenced key as shown in the 2nd example ("javax.persistence.jtaDataSource" is * not present). However, if it is present ("javax.jdo.option.ConnectionFactoryName" above), the * null-indicating meta-property "javax.jdo.option.ConnectionFactoryName.null" overrides the value "jdbc/someDataSource". * * @param properties the properties. Must not be <code>null</code>. * @param key the property-key for which to determine, whether its value is <code>null</code>. * @return <code>true</code>, if the property referenced by the given <code>key</code> is <code>null</code>; * <code>false</code> otherwise. */ public static boolean isNullValue(final Map<?, ?> properties, final String key) { if (properties == null) throw new IllegalArgumentException("properties == null"); if (properties.get(key) == null) return true; if (isMetaPropertyKeyNullValue(key)) return false; final String metaNullValue = String.valueOf(properties.get(getMetaPropertyKeyNullValue(key))); return Boolean.parseBoolean(metaNullValue); } public static int getSystemPropertyValueAsInt(final String key, final int defaultValue) { final long value = getSystemPropertyValueAsLong(key, defaultValue); if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) { logger.warn("System property '{}' is set to the value '{}' which is out of range for a 32-bit integer. Falling back to default value {}.", key, value, defaultValue); return defaultValue; } return (int) value; } public static long getSystemPropertyValueAsLong(final String key, final long defaultValue) { return getPropertyValueAsLong(System.getProperties(), key, defaultValue); } public static long getPropertyValueAsLong(final Properties properties, final String key, long defaultValue) { assertNotNull(properties, "properties"); assertNotNull(key, "key"); final String value = properties.getProperty(key); if (value == null) return defaultValue; try { return Long.parseLong(value.trim()); } catch (NumberFormatException x) { return defaultValue; } } /** * Converts a system property key to an OS environment variable name. * <p> * OS env vars cannot contain a "." and they have a few more restrictions. Most of the restrictions * (e.g. depending on the shell, the name must not start with a digit) are no problem, because our * system properties don't infringe on them. But the "." is commonly used in most of our system properties. * <p> * Therefore, this method throws an {@link IllegalArgumentException}, if any unexpected property key * infringes on a known env var restriction (e.g. starting with a digit). Dots (".") and index-brackets * ("[" and "]") are all converted to underscores ("_"). * <p> * Thus, e.g. the system property "cloudstore.configDir" is equivalent to the env var "cloudstore_configDir" * and the system property "cloudstore.ldap.bindDnTemplate[index]" is equivalent to the env var * "cloudstore_ldap_bindDnTemplate_index_". Please note, that they are case sensitive and the case is not modified! * * @param key the system property key to be converted to an env var name. Must not be <code>null</code>. * @return the env var name. Never <code>null</code>. */ public static String systemPropertyToEnvironmentVariable(final String key) { assertNotNull(key, "key"); if (key.isEmpty()) throw new IllegalArgumentException("key is an empty string! At least one character is required!"); if (!validSystemPropertyKeyPattern.matcher(key).matches()) { if (Character.isDigit(key.charAt(0))) // be kind: give more precise exception ;-) throw new IllegalArgumentException("key must not start with a digit: " + key); throw new IllegalArgumentException("key is not valid according to pattern: " + key); } return key.replace('.', '_').replace('[', '_').replace(']', '_'); } private static final Pattern validSystemPropertyKeyPattern = Pattern.compile("[a-zA-Z_]+[a-zA-Z0-9_\\.\\[\\]]*"); }