All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.kuali.common.util.PropertyUtils Maven / Gradle / Ivy

There is a newer version: 4.4.17
Show newest version
/**
 * Copyright 2010-2012 The Kuali Foundation
 *
 * Licensed under the Educational Community License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.opensource.org/licenses/ecl2.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.kuali.common.util;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.kuali.common.util.property.Constants;
import org.kuali.common.util.property.GlobalPropertiesMode;
import org.kuali.common.util.property.processor.AddPropertiesProcessor;
import org.kuali.common.util.property.processor.PropertyProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.PropertyPlaceholderHelper;

/**
 * Simplify handling of Properties especially as it relates to storing and loading. Properties can be loaded from
 * any url Spring resource loading can understand. When storing and loading, locations ending in .xml are automatically handled
 * using storeToXML() and loadFromXML(), respectively. Properties are always stored in sorted order
 * with the encoding indicated via a comment.
 */
public class PropertyUtils {

	private static final Logger logger = LoggerFactory.getLogger(PropertyUtils.class);

	private static final String XML_EXTENSION = ".xml";
	private static final String ENV_PREFIX = "env";
	private static final String DEFAULT_ENCODING = Charset.defaultCharset().name();
	private static final String DEFAULT_XML_ENCODING = "UTF-8";

	public static final Properties combine(List properties) {
		Properties combined = new Properties();
		for (Properties p : properties) {
			combined.putAll(PropertyUtils.toEmpty(p));
		}
		return combined;
	}

	public static final Properties combine(Properties... properties) {
		return combine(Arrays.asList(properties));
	}

	public static final void process(Properties properties, PropertyProcessor processor) {
		process(properties, Collections.singletonList(processor));
	}

	public static final void process(Properties properties, List processors) {
		for (PropertyProcessor processor : processors) {
			processor.process(properties);
		}
	}

	public static final Properties toEmpty(Properties properties) {
		return properties == null ? new Properties() : properties;
	}

	public static final boolean isSingleUnresolvedPlaceholder(String string) {
		return isSingleUnresolvedPlaceholder(string, Constants.DEFAULT_PLACEHOLDER_PREFIX, Constants.DEFAULT_PLACEHOLDER_SUFFIX);
	}

	public static final boolean isSingleUnresolvedPlaceholder(String string, String prefix, String suffix) {
		int prefixMatches = StringUtils.countMatches(string, prefix);
		int suffixMatches = StringUtils.countMatches(string, suffix);
		boolean startsWith = StringUtils.startsWith(string, prefix);
		boolean endsWith = StringUtils.endsWith(string, suffix);
		return prefixMatches == 1 && suffixMatches == 1 && startsWith && endsWith;
	}

	public static final boolean containsUnresolvedPlaceholder2(String string) {
		int beginIndex = StringUtils.indexOf(string, Constants.DEFAULT_PLACEHOLDER_PREFIX);
		if (beginIndex == -1) {
			return false;
		}
		int endIndex = StringUtils.indexOf(string, Constants.DEFAULT_PLACEHOLDER_SUFFIX, beginIndex);
		if (endIndex == -1) {
			return false;
		}
		String substring = StringUtils.substring(string, beginIndex, endIndex);
		return StringUtils.indexOf(substring, Constants.DEFAULT_VALUE_SEPARATOR) == -1;
	}

	public static final boolean containsUnresolvedPlaceholder(String string) {
		return containsUnresolvedPlaceholder(string, Constants.DEFAULT_PLACEHOLDER_PREFIX, Constants.DEFAULT_PLACEHOLDER_SUFFIX);
	}

	public static final boolean containsUnresolvedPlaceholder(String string, String prefix, String suffix) {
		int beginIndex = StringUtils.indexOf(string, prefix);
		if (beginIndex == -1) {
			return false;
		}
		return StringUtils.indexOf(string, suffix) != -1;
	}

	/**
	 * Return a new Properties object containing only those properties where the resolved value is different from the original
	 * value. Using global properties to perform property resolution as indicated by Constants.DEFAULT_GLOBAL_PROPERTIES_MODE
	 */
	public static final Properties getResolvedProperties(Properties properties) {
		return getResolvedProperties(properties, Constants.DEFAULT_PROPERTY_PLACEHOLDER_HELPER, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
	}

	/**
	 * Return a new Properties object containing only those properties where the resolved value is different from the original
	 * value. Using global properties to perform property resolution as indicated by globalPropertiesMode
	 */
	public static final Properties getResolvedProperties(Properties properties, GlobalPropertiesMode globalPropertiesMode) {
		return getResolvedProperties(properties, Constants.DEFAULT_PROPERTY_PLACEHOLDER_HELPER, globalPropertiesMode);
	}

	/**
	 * Return a new Properties object containing only those properties where the resolved value is different from the original
	 * value. Using global properties to perform property resolution as indicated by Constants.DEFAULT_GLOBAL_PROPERTIES_MODE
	 */
	public static final Properties getResolvedProperties(Properties properties, PropertyPlaceholderHelper helper) {
		return getResolvedProperties(properties, helper, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
	}

	/**
	 * Return a new Properties object containing only those properties where the resolved value is different from the original
	 * value. Using global properties to perform property resolution as indicated by globalPropertiesMode
	 */
	public static final Properties getResolvedProperties(Properties properties, PropertyPlaceholderHelper helper, GlobalPropertiesMode globalPropertiesMode) {
		Properties global = PropertyUtils.getProperties(properties, globalPropertiesMode);
		List keys = PropertyUtils.getSortedKeys(properties);
		Properties newProperties = new Properties();
		for (String key : keys) {
			String originalValue = properties.getProperty(key);
			String resolvedValue = helper.replacePlaceholders(originalValue, global);
			if (!resolvedValue.equals(originalValue)) {
				logger.debug("Resolved property '" + key + "' [{}] -> [{}]", Str.flatten(originalValue), Str.flatten(resolvedValue));
				newProperties.setProperty(key, resolvedValue);
			}
		}
		return newProperties;
	}

	/**
	 * Return the property values from keys
	 */
	public static final List getValues(Properties properties, List keys) {
		List values = new ArrayList();
		for (String key : keys) {
			values.add(properties.getProperty(key));
		}
		return values;
	}

	/**
	 * Return a sorted List of keys from properties that end with suffix.
	 */
	public static final List getEndsWithKeys(Properties properties, String suffix) {
		List keys = getSortedKeys(properties);
		List matches = new ArrayList();
		for (String key : keys) {
			if (StringUtils.endsWith(key, suffix)) {
				matches.add(key);
			}
		}
		return matches;
	}

	/**
	 * Alter the properties passed in to contain only the desired property values. includes and
	 * excludes are comma separated values.
	 */
	public static final void trim(Properties properties, String includesCSV, String excludesCSV) {
		List includes = CollectionUtils.getTrimmedListFromCSV(includesCSV);
		List excludes = CollectionUtils.getTrimmedListFromCSV(excludesCSV);
		trim(properties, includes, excludes);
	}

	/**
	 * Alter the properties passed in to contain only the desired property values.
	 */
	public static final void trim(Properties properties, List includes, List excludes) {
		List keys = getSortedKeys(properties);
		for (String key : keys) {
			if (!include(key, includes, excludes)) {
				logger.debug("Removing [{}]", key);
				properties.remove(key);
			}
		}
	}

	/**
	 * Return true if value should be included, false otherwise.
* If excludes is not empty and matches value return false.
* If value has not been explicitly excluded, check the includes list.
* If includes is empty return true.
* If includes is not empty, return true if, and only if, value matches a pattern from the * includes list.
* A single wildcard * pattern is supported for includes and excludes.
* If value contains the wildcard symbol *, IllegalArgumentException is thrown if the * include/exclude lists also contain strings with the wildcard symbol * */ public static final boolean include(String value, List includes, List excludes) { if (singleWildcardMatch(value, excludes)) { // No point incurring the overhead of matching an include pattern return false; } else { // If includes is empty always return true return CollectionUtils.isEmpty(includes) || singleWildcardMatch(value, includes); } } public static boolean singleWildcardMatch(String s, List patterns) { for (String pattern : CollectionUtils.toEmpty(patterns)) { if (singleWildcardMatch(s, pattern)) { return true; } } return false; } /** * Match {@code value} against {@code pattern} where {@code pattern} can optionally contain a single wildcard. If both are {@code null} * return {@code true}. If one is {@code null} but not the other, return {@code false}. If neither is {@code null}, any {@code pattern} * containing more than a single wildcard throws {@code IllegalArgumentException}. * *
	 * PropertyUtils.singleWildcardMatch(null, null)             = true
	 * PropertyUtils.singleWildcardMatch(null, *)                = false
	 * PropertyUtils.singleWildcardMatch(*, null)                = false
	 * PropertyUtils.singleWildcardMatch(*, "*")                 = true
	 * PropertyUtils.singleWildcardMatch("abcdef", "bcd")        = false
	 * PropertyUtils.singleWildcardMatch("abcdef", "*def")       = true
	 * PropertyUtils.singleWildcardMatch("abcdef", "abc*")       = true
	 * PropertyUtils.singleWildcardMatch("abcdef", "ab*ef")      = true
	 * PropertyUtils.singleWildcardMatch("abcdef", "abc*def")    = true
	 * PropertyUtils.singleWildcardMatch(*, "**")                = IllegalArgumentException
	 * 
*/ public static boolean singleWildcardMatch(String value, String pattern) { if (value == null && pattern == null) { // both are null return true; } else if (value != null && pattern == null) { // pattern is null but value is not return false; } else if (value == null && pattern != null) { // value is null but pattern is not return false; } else if (StringUtils.countMatches(pattern, Constants.WILDCARD) > 1) { throw new IllegalArgumentException("Pattern [" + pattern + "] is not supported. Only one wildcard is allowed in the pattern"); } else if (pattern.equals(Constants.WILDCARD)) { // neither one is null and pattern is the wildcard. Value is irrelevant return true; } else if (!StringUtils.contains(pattern, Constants.WILDCARD)) { // Neither one is null and there is no wildcard in the pattern. They must match exactly return StringUtils.equals(value, pattern); } else { int pos = StringUtils.indexOf(pattern, Constants.WILDCARD); int suffixPos = pos + Constants.WILDCARD.length(); boolean nullPrefix = pos == 0; boolean nullSuffix = suffixPos >= pattern.length(); String prefix = nullPrefix ? null : StringUtils.substring(pattern, 0, pos); String suffix = nullSuffix ? null : StringUtils.substring(pattern, suffixPos); boolean prefixMatch = nullPrefix || StringUtils.startsWith(value, prefix); boolean suffixMatch = nullSuffix || StringUtils.endsWith(value, suffix); return prefixMatch && suffixMatch; } } /** * Return property keys that should be included as a sorted list. */ public static final Properties getProperties(Properties properties, String include, String exclude) { List keys = getSortedKeys(properties, include, exclude); Properties newProperties = new Properties(); for (String key : keys) { String value = properties.getProperty(key); newProperties.setProperty(key, value); } return newProperties; } /** * Return property keys that should be included as a sorted list. */ public static final List getSortedKeys(Properties properties, String include, String exclude) { return getSortedKeys(properties, CollectionUtils.toEmptyList(include), CollectionUtils.toEmptyList(exclude)); } /** * Return property keys that should be included as a sorted list. */ public static final List getSortedKeys(Properties properties, List includes, List excludes) { List keys = getSortedKeys(properties); List includedKeys = new ArrayList(); for (String key : keys) { if (include(key, includes, excludes)) { includedKeys.add(key); } } return includedKeys; } /** * Return a sorted List of keys from properties that start with prefix */ public static final List getStartsWithKeys(Properties properties, String prefix) { List keys = getSortedKeys(properties); List matches = new ArrayList(); for (String key : keys) { if (StringUtils.startsWith(key, prefix)) { matches.add(key); } } return matches; } /** * Return the property keys as a sorted list. */ public static final List getSortedKeys(Properties properties) { List keys = new ArrayList(properties.stringPropertyNames()); Collections.sort(keys); return keys; } public static final String toString(Properties properties) { List keys = getSortedKeys(properties); StringBuilder sb = new StringBuilder(); for (String key : keys) { String value = Str.flatten(properties.getProperty(key)); sb.append(key + "=" + value + "\n"); } return sb.toString(); } public static final void info(Properties properties) { properties = toEmpty(properties); logger.info("--- Displaying {} properties ---\n\n{}", properties.size(), toString(properties)); } public static final void debug(Properties properties) { properties = toEmpty(properties); logger.debug("--- Displaying {} properties ---\n\n{}", properties.size(), toString(properties)); } /** * Store the properties to the indicated file using the platform default encoding. */ public static final void store(Properties properties, File file) { store(properties, file, null); } /** * Store the properties to the indicated file using the indicated encoding. */ public static final void store(Properties properties, File file, String encoding) { store(properties, file, encoding, null); } /** * Store the properties to the indicated file using the indicated encoding with the indicated comment appearing at the top of the file. */ public static final void store(Properties properties, File file, String encoding, String comment) { OutputStream out = null; Writer writer = null; try { out = FileUtils.openOutputStream(file); String path = file.getCanonicalPath(); boolean xml = isXml(path); Properties sorted = getSortedProperties(properties); comment = getComment(encoding, comment, xml); if (xml) { logger.info("Storing XML properties - [{}] encoding={}", path, StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING)); if (encoding == null) { sorted.storeToXML(out, comment); } else { sorted.storeToXML(out, comment, encoding); } } else { writer = LocationUtils.getWriter(out, encoding); logger.info("Storing properties - [{}] encoding={}", path, StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING)); sorted.store(writer, comment); } } catch (IOException e) { throw new IllegalStateException("Unexpected IO error", e); } finally { IOUtils.closeQuietly(writer); IOUtils.closeQuietly(out); } } /** * Return a new properties object containing the properties from getEnvAsProperties() and * System.getProperties(). Properties from System.getProperties() override properties from * getEnvAsProperties if there are duplicates. */ public static final Properties getGlobalProperties() { return getProperties(Constants.DEFAULT_GLOBAL_PROPERTIES_MODE); } /** * Return a new properties object containing the properties passed in, plus any properties returned by getEnvAsProperties() * and System.getProperties(). Properties from getEnvAsProperties() override properties and * properties from System.getProperties() override everything. */ public static final Properties getGlobalProperties(Properties properties) { return getProperties(properties, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE); } /** * Return a new properties object containing the properties passed in, plus any global properties as requested. If mode is * NONE the new properties are a duplicate of the properties passed in. If mode is ENVIRONMENT * the new properties contain the original properties plus any properties returned by getEnvProperties(). If * mode is SYSTEM the new properties contain the original properties plus System.getProperties(). * If mode is BOTH the new properties contain the original properties plus getEnvProperties() and * System.getProperties(). */ public static final Properties getProperties(Properties properties, GlobalPropertiesMode mode) { Properties newProperties = duplicate(properties); List modifiers = getPropertyProcessors(mode); for (PropertyProcessor modifier : modifiers) { modifier.process(newProperties); } return newProperties; } /** * Return a new properties object containing global properties as requested. If mode is NONE the new * properties are empty. If mode is ENVIRONMENT the new properties contain the properties returned by * getEnvProperties(). If mode is SYSTEM the new properties contain * System.getProperties(). If mode is BOTH the new properties contain * getEnvProperties plus System.getProperties() with system properties overriding environment variables if the * same case sensitive property key is supplied in both places. */ public static final Properties getProperties(GlobalPropertiesMode mode) { return getProperties(new Properties(), mode); } /** * Search global properties to find a value for key according to the mode passed in. */ public static final String getProperty(String key, GlobalPropertiesMode mode) { return getProperty(key, new Properties(), mode); } /** * Search properties plus global properties to find a value for key according to the mode passed in. If the * property is present in both, the value from the global properties is returned. */ public static final String getProperty(String key, Properties properties, GlobalPropertiesMode mode) { return getProperties(properties, mode).getProperty(key); } /** * Return modifiers that add environment variables, system properties, or both, according to the mode passed in. */ public static final List getPropertyProcessors(GlobalPropertiesMode mode) { List processors = new ArrayList(); switch (mode) { case NONE: return processors; case ENVIRONMENT: processors.add(new AddPropertiesProcessor(getEnvAsProperties())); return processors; case SYSTEM: processors.add(new AddPropertiesProcessor(System.getProperties())); return processors; case BOTH: processors.add(new AddPropertiesProcessor(getEnvAsProperties())); processors.add(new AddPropertiesProcessor(System.getProperties())); return processors; default: throw new IllegalStateException(mode + " is unknown"); } } /** * Convert the Map to a Properties object. */ public static final Properties convert(Map map) { Properties props = new Properties(); for (String key : map.keySet()) { String value = map.get(key); props.setProperty(key, value); } return props; } /** * Return a new properties object that duplicates the properties passed in. */ public static final Properties duplicate(Properties properties) { Properties newProperties = new Properties(); newProperties.putAll(properties); return newProperties; } /** * Return a new properties object containing environment variables as properties prefixed with env */ public static Properties getEnvAsProperties() { return getEnvAsProperties(ENV_PREFIX); } /** * Return a new properties object containing environment variables as properties prefixed with prefix */ public static Properties getEnvAsProperties(String prefix) { Properties properties = convert(System.getenv()); return getPrefixedProperties(properties, prefix); } /** * Return true if, and only if, location ends with .xml (case insensitive). */ public static final boolean isXml(String location) { return StringUtils.endsWithIgnoreCase(location, XML_EXTENSION); } /** * Return a new Properties object loaded from location. */ public static final Properties load(String location) { return load(location, null); } /** * Return a new Properties object loaded from location using encoding. */ public static final Properties load(String location, String encoding) { InputStream in = null; Reader reader = null; try { Properties properties = new Properties(); boolean xml = isXml(location); if (xml) { in = LocationUtils.getInputStream(location); logger.info("Loading XML properties - [{}]", location); properties.loadFromXML(in); } else { logger.info("Loading properties - [{}] encoding={}", location, StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING)); reader = LocationUtils.getBufferedReader(location, encoding); properties.load(reader); } return properties; } catch (IOException e) { throw new IllegalStateException("Unexpected IO error", e); } finally { IOUtils.closeQuietly(in); IOUtils.closeQuietly(reader); } } /** * Return a new Properties object containing properties prefixed with prefix. If prefix is blank, * the new properties object duplicates the properties passed in. */ public static final Properties getPrefixedProperties(Properties properties, String prefix) { if (StringUtils.isBlank(prefix)) { return duplicate(properties); } Properties newProperties = new Properties(); for (String key : properties.stringPropertyNames()) { String value = properties.getProperty(key); String newKey = StringUtils.startsWith(key, prefix + ".") ? key : prefix + "." + key; newProperties.setProperty(newKey, value); } return newProperties; } /** * Return a new properties object where the keys have been converted to upper case and periods have been replaced with an underscore. */ public static final Properties reformatKeysAsEnvVars(Properties properties) { Properties newProperties = new Properties(); for (String key : properties.stringPropertyNames()) { String value = properties.getProperty(key); String newKey = StringUtils.upperCase(StringUtils.replace(key, ".", "-")); newProperties.setProperty(newKey, value); } return newProperties; } /** * Before setting the newValue, check to see if there is a conflict with an existing value. If there is no existing value, add the * property. If there is a conflict, check propertyOverwriteMode to make sure we have permission to override the value. */ public static final void addOrOverrideProperty(Properties properties, String key, String newValue, Mode propertyOverwriteMode) { String oldValue = properties.getProperty(key); if (StringUtils.equals(newValue, oldValue)) { // Nothing to do! New value is the same as old value. return; } boolean overwrite = !StringUtils.isBlank(oldValue); // TODO Yuck! Do something smarter here String logNewValue = newValue; String logOldValue = oldValue; if (StringUtils.containsIgnoreCase(key, "password")) { logNewValue = LoggerUtils.getPassword(null, logNewValue); logOldValue = LoggerUtils.getPassword(null, logOldValue); } if (overwrite) { // This property already has a value, and it is different from the new value // Check to make sure we are allowed to override the old value before doing so Object[] args = new Object[] { key, Str.flatten(logNewValue), Str.flatten(logOldValue) }; ModeUtils.validate(propertyOverwriteMode, "Overriding [{}={}] was [{}]", args, "Override of existing property [" + key + "] is not allowed."); } else { // There is no existing value for this key logger.info("Adding [{}={}]", key, Str.flatten(logNewValue)); } properties.setProperty(key, newValue); } private static final String getDefaultComment(String encoding, boolean xml) { if (encoding == null) { if (xml) { // Java defaults XML properties files to UTF-8 if no encoding is provided return "encoding.default=" + DEFAULT_XML_ENCODING; } else { // For normal properties files the platform default encoding is used return "encoding.default=" + DEFAULT_ENCODING; } } else { return "encoding.specified=" + encoding; } } private static final String getComment(String comment, String encoding, boolean xml) { if (StringUtils.isBlank(comment)) { return getDefaultComment(encoding, xml); } else { return comment + "\n#" + getDefaultComment(encoding, xml); } } /** * This is private because SortedProperties does not fully honor the contract for Properties */ private static final SortedProperties getSortedProperties(Properties properties) { SortedProperties sp = new PropertyUtils().new SortedProperties(); sp.putAll(properties); return sp; } /** * This is private since it does not honor the full contract for Properties. PropertyUtils uses it internally * to store properties in sorted order. */ private class SortedProperties extends Properties { private static final long serialVersionUID = 1330825236411537386L; /** * Properties.storeToXML() uses keySet() */ @Override public Set keySet() { return Collections.unmodifiableSet(new TreeSet(super.keySet())); } /** * Properties.store() uses keys() */ @Override public synchronized Enumeration keys() { return Collections.enumeration(new TreeSet(super.keySet())); } } }