View Javadoc

1   /**
2    * Copyright 2010-2012 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.common.util;
17  
18  import java.io.File;
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.io.OutputStream;
22  import java.io.Reader;
23  import java.io.Writer;
24  import java.nio.charset.Charset;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.Collections;
28  import java.util.Enumeration;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Properties;
32  import java.util.Set;
33  import java.util.TreeSet;
34  
35  import org.apache.commons.io.FileUtils;
36  import org.apache.commons.io.IOUtils;
37  import org.apache.commons.lang3.StringUtils;
38  import org.jasypt.util.text.TextEncryptor;
39  import org.kuali.common.util.property.Constants;
40  import org.kuali.common.util.property.GlobalPropertiesMode;
41  import org.kuali.common.util.property.PropertiesLoaderContext;
42  import org.kuali.common.util.property.processor.AddPropertiesProcessor;
43  import org.kuali.common.util.property.processor.PropertyProcessor;
44  import org.slf4j.Logger;
45  import org.slf4j.LoggerFactory;
46  import org.springframework.util.PropertyPlaceholderHelper;
47  
48  /**
49   * Simplify handling of <code>Properties</code> especially as it relates to storing and loading. <code>Properties</code> can be loaded from any url Spring resource loading can
50   * understand. When storing and loading, locations ending in <code>.xml</code> are automatically handled using <code>storeToXML()</code> and <code>loadFromXML()</code>,
51   * respectively. <code>Properties</code> are always stored in sorted order with the <code>encoding</code> indicated via a comment.
52   */
53  public class PropertyUtils {
54  
55  	private static final Logger logger = LoggerFactory.getLogger(PropertyUtils.class);
56  
57  	private static final String XML_EXTENSION = ".xml";
58  	private static final String ENV_PREFIX = "env";
59  	private static final String DEFAULT_ENCODING = Charset.defaultCharset().name();
60  	private static final String DEFAULT_XML_ENCODING = "UTF-8";
61  
62  	public static Properties load(PropertiesLoaderContext context) {
63  		Assert.notNull(context.getHelper(), "helper is null");
64  		Assert.notNull(context.getLocations(), "locations are null");
65  		Assert.notNull(context.getEncoding(), "encoding is null");
66  		Assert.notNull(context.getMissingLocationsMode(), "missingLocationsMode is null");
67  		Properties global = PropertyUtils.getGlobalProperties();
68  		context.setProperties(PropertyUtils.toEmpty(context.getProperties()));
69  		Properties result = new Properties();
70  		for (String location : context.getLocations()) {
71  			Properties resolverProperties = PropertyUtils.combine(context.getProperties(), result, global);
72  			String resolvedLocation = context.getHelper().replacePlaceholders(location, resolverProperties);
73  			if (LocationUtils.exists(resolvedLocation)) {
74  				Properties properties = PropertyUtils.load(resolvedLocation, context.getEncoding());
75  				result.putAll(context.getProperties());
76  				result.putAll(properties);
77  			} else {
78  				ModeUtils.validate(context.getMissingLocationsMode(), "Skipping non-existent location [" + resolvedLocation + "]");
79  			}
80  		}
81  		return result;
82  	}
83  
84  	/**
85  	 * Decrypt any encrypted property values. Encrypted values are surrounded by ENC(...), like:
86  	 * 
87  	 * <pre>
88  	 * my.value = ENC(DGA"$S24FaIO)
89  	 * </pre>
90  	 */
91  	public static void decrypt(Properties properties, TextEncryptor encryptor) {
92  		decrypt(properties, encryptor, null, null);
93  	}
94  
95  	/**
96  	 * Return a new <code>Properties</code> object (never null) containing only those properties whose values are encrypted. Encrypted values are surrounded by ENC(...), like:
97  	 * 
98  	 * <pre>
99  	 * my.value = ENC(DGA"$S24FaIO)
100 	 * </pre>
101 	 */
102 	public static Properties getEncryptedProperties(Properties properties) {
103 		List<String> keys = getSortedKeys(properties);
104 		Properties encrypted = new Properties();
105 		for (String key : keys) {
106 			String value = properties.getProperty(key);
107 			if (isEncryptedPropertyValue(value)) {
108 				encrypted.setProperty(key, value);
109 			}
110 		}
111 		return encrypted;
112 	}
113 
114 	/**
115 	 * Decrypt any encrypted property values matching the <code>includes</code>, <code>excludes</code> patterns. Encrypted values are surrounded by ENC(...).
116 	 * 
117 	 * <pre>
118 	 * my.value = ENC(DGA"$S24FaIO)
119 	 * </pre>
120 	 */
121 	public static void decrypt(Properties properties, TextEncryptor encryptor, List<String> includes, List<String> excludes) {
122 		List<String> keys = getSortedKeys(properties, includes, excludes);
123 		for (String key : keys) {
124 			String value = properties.getProperty(key);
125 			if (isEncryptedPropertyValue(value)) {
126 				String decryptedValue = decryptPropertyValue(encryptor, value);
127 				properties.setProperty(key, decryptedValue);
128 			}
129 		}
130 	}
131 
132 	/**
133 	 * Return true if the value starts with <code>ENC(</code> and ends with <code>)</code>, false otherwise.
134 	 */
135 	public static boolean isEncryptedPropertyValue(String value) {
136 		return StringUtils.startsWith(value, Constants.ENCRYPTION_PREFIX) && StringUtils.endsWith(value, Constants.ENCRYPTION_SUFFIX);
137 	}
138 
139 	/**
140 	 * Encrypt all of the property values. Encrypted values are surrounded by ENC(...).
141 	 * 
142 	 * <pre>
143 	 * my.value = ENC(DGA"$S24FaIO)
144 	 * </pre>
145 	 */
146 	public static void encrypt(Properties properties, TextEncryptor encryptor) {
147 		encrypt(properties, encryptor, null, null);
148 	}
149 
150 	/**
151 	 * Encrypt properties as dictated by <code>includes</code> and <code>excludes</code>. Encrypted values are surrounded by ENC(...).
152 	 * 
153 	 * <pre>
154 	 * my.value = ENC(DGA"$S24FaIO)
155 	 * </pre>
156 	 */
157 	public static void encrypt(Properties properties, TextEncryptor encryptor, List<String> includes, List<String> excludes) {
158 		List<String> keys = getSortedKeys(properties, includes, excludes);
159 		for (String key : keys) {
160 			String originalValue = properties.getProperty(key);
161 			String encryptedValue = encryptPropertyValue(encryptor, originalValue);
162 			properties.setProperty(key, encryptedValue);
163 		}
164 	}
165 
166 	/**
167 	 * Return the decrypted version of the property value. Encrypted values are surrounded by ENC(...).
168 	 * 
169 	 * <pre>
170 	 * my.value = ENC(DGA"$S24FaIO)
171 	 * </pre>
172 	 */
173 	public static String decryptPropertyValue(TextEncryptor encryptor, String value) {
174 		// Ensure this property value really is encrypted
175 		Assert.isTrue(StringUtils.startsWith(value, Constants.ENCRYPTION_PREFIX), "value does not start with " + Constants.ENCRYPTION_PREFIX);
176 		Assert.isTrue(StringUtils.endsWith(value, Constants.ENCRYPTION_SUFFIX), "value does not end with " + Constants.ENCRYPTION_SUFFIX);
177 
178 		// Extract the value inside the ENC(...) wrapping
179 		int start = Constants.ENCRYPTION_PREFIX.length();
180 		int end = StringUtils.length(value) - Constants.ENCRYPTION_SUFFIX.length();
181 		String unwrapped = StringUtils.substring(value, start, end);
182 
183 		// Return the decrypted value
184 		return encryptor.decrypt(unwrapped);
185 	}
186 
187 	/**
188 	 * Return the encrypted version of the property value. A value is considered "encrypted" when it appears surrounded by ENC(...).
189 	 * 
190 	 * <pre>
191 	 * my.value = ENC(DGA"$S24FaIO)
192 	 * </pre>
193 	 */
194 	public static String encryptPropertyValue(TextEncryptor encryptor, String value) {
195 		String encryptedValue = encryptor.encrypt(value);
196 		StringBuilder sb = new StringBuilder();
197 		sb.append(Constants.ENCRYPTION_PREFIX);
198 		sb.append(encryptedValue);
199 		sb.append(Constants.ENCRYPTION_SUFFIX);
200 		return sb.toString();
201 	}
202 
203 	public static void overrideWithGlobalValues(Properties properties, GlobalPropertiesMode mode) {
204 		List<String> keys = PropertyUtils.getSortedKeys(properties);
205 		Properties global = PropertyUtils.getProperties(mode);
206 		for (String key : keys) {
207 			String globalValue = global.getProperty(key);
208 			if (!StringUtils.isBlank(globalValue)) {
209 				properties.setProperty(key, globalValue);
210 			}
211 		}
212 	}
213 
214 	public static final Properties combine(List<Properties> properties) {
215 		Properties combined = new Properties();
216 		for (Properties p : properties) {
217 			combined.putAll(PropertyUtils.toEmpty(p));
218 		}
219 		return combined;
220 	}
221 
222 	public static final Properties combine(Properties... properties) {
223 		return combine(Arrays.asList(properties));
224 	}
225 
226 	public static final void process(Properties properties, PropertyProcessor processor) {
227 		process(properties, Collections.singletonList(processor));
228 	}
229 
230 	public static final void process(Properties properties, List<PropertyProcessor> processors) {
231 		for (PropertyProcessor processor : CollectionUtils.toEmptyList(processors)) {
232 			processor.process(properties);
233 		}
234 	}
235 
236 	public static final Properties toEmpty(Properties properties) {
237 		return properties == null ? new Properties() : properties;
238 	}
239 
240 	public static final boolean isSingleUnresolvedPlaceholder(String string) {
241 		return isSingleUnresolvedPlaceholder(string, Constants.DEFAULT_PLACEHOLDER_PREFIX, Constants.DEFAULT_PLACEHOLDER_SUFFIX);
242 	}
243 
244 	public static final boolean isSingleUnresolvedPlaceholder(String string, String prefix, String suffix) {
245 		int prefixMatches = StringUtils.countMatches(string, prefix);
246 		int suffixMatches = StringUtils.countMatches(string, suffix);
247 		boolean startsWith = StringUtils.startsWith(string, prefix);
248 		boolean endsWith = StringUtils.endsWith(string, suffix);
249 		return prefixMatches == 1 && suffixMatches == 1 && startsWith && endsWith;
250 	}
251 
252 	public static final boolean containsUnresolvedPlaceholder(String string) {
253 		return containsUnresolvedPlaceholder(string, Constants.DEFAULT_PLACEHOLDER_PREFIX, Constants.DEFAULT_PLACEHOLDER_SUFFIX);
254 	}
255 
256 	public static final boolean containsUnresolvedPlaceholder(String string, String prefix, String suffix) {
257 		int beginIndex = StringUtils.indexOf(string, prefix);
258 		if (beginIndex == -1) {
259 			return false;
260 		}
261 		return StringUtils.indexOf(string, suffix) != -1;
262 	}
263 
264 	/**
265 	 * Return a new <code>Properties</code> object containing only those properties where the resolved value is different from the original value. Using global properties to
266 	 * perform property resolution as indicated by <code>Constants.DEFAULT_GLOBAL_PROPERTIES_MODE</code>
267 	 */
268 	public static final Properties getResolvedProperties(Properties properties) {
269 		return getResolvedProperties(properties, Constants.DEFAULT_PROPERTY_PLACEHOLDER_HELPER, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
270 	}
271 
272 	/**
273 	 * Return a new <code>Properties</code> object containing only those properties where the resolved value is different from the original value. Using global properties to
274 	 * perform property resolution as indicated by <code>globalPropertiesMode</code>
275 	 */
276 	public static final Properties getResolvedProperties(Properties properties, GlobalPropertiesMode globalPropertiesMode) {
277 		return getResolvedProperties(properties, Constants.DEFAULT_PROPERTY_PLACEHOLDER_HELPER, globalPropertiesMode);
278 	}
279 
280 	/**
281 	 * Return a new <code>Properties</code> object containing only those properties where the resolved value is different from the original value. Using global properties to
282 	 * perform property resolution as indicated by <code>Constants.DEFAULT_GLOBAL_PROPERTIES_MODE</code>
283 	 */
284 	public static final Properties getResolvedProperties(Properties properties, PropertyPlaceholderHelper helper) {
285 		return getResolvedProperties(properties, helper, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
286 	}
287 
288 	/**
289 	 * Return a new <code>Properties</code> object containing only those properties where the resolved value is different from the original value. Using global properties to
290 	 * perform property resolution as indicated by <code>globalPropertiesMode</code>
291 	 */
292 	public static final Properties getResolvedProperties(Properties properties, PropertyPlaceholderHelper helper, GlobalPropertiesMode globalPropertiesMode) {
293 		Properties global = PropertyUtils.getProperties(properties, globalPropertiesMode);
294 		List<String> keys = PropertyUtils.getSortedKeys(properties);
295 		Properties newProperties = new Properties();
296 		for (String key : keys) {
297 			String originalValue = properties.getProperty(key);
298 			String resolvedValue = helper.replacePlaceholders(originalValue, global);
299 			if (!resolvedValue.equals(originalValue)) {
300 				logger.debug("Resolved property '" + key + "' [{}] -> [{}]", Str.flatten(originalValue), Str.flatten(resolvedValue));
301 				newProperties.setProperty(key, resolvedValue);
302 			}
303 		}
304 		return newProperties;
305 	}
306 
307 	/**
308 	 * Return the property values from <code>keys</code>
309 	 */
310 	public static final List<String> getValues(Properties properties, List<String> keys) {
311 		List<String> values = new ArrayList<String>();
312 		for (String key : keys) {
313 			values.add(properties.getProperty(key));
314 		}
315 		return values;
316 	}
317 
318 	/**
319 	 * Return a sorted <code>List</code> of keys from <code>properties</code> that end with <code>suffix</code>.
320 	 */
321 	public static final List<String> getEndsWithKeys(Properties properties, String suffix) {
322 		List<String> keys = getSortedKeys(properties);
323 		List<String> matches = new ArrayList<String>();
324 		for (String key : keys) {
325 			if (StringUtils.endsWith(key, suffix)) {
326 				matches.add(key);
327 			}
328 		}
329 		return matches;
330 	}
331 
332 	/**
333 	 * Alter the <code>properties</code> passed in to contain only the desired property values. <code>includes</code> and <code>excludes</code> are comma separated values.
334 	 */
335 	public static final void trim(Properties properties, String includesCSV, String excludesCSV) {
336 		List<String> includes = CollectionUtils.getTrimmedListFromCSV(includesCSV);
337 		List<String> excludes = CollectionUtils.getTrimmedListFromCSV(excludesCSV);
338 		trim(properties, includes, excludes);
339 	}
340 
341 	/**
342 	 * Alter the <code>properties</code> passed in to contain only the desired property values.
343 	 */
344 	public static final void trim(Properties properties, List<String> includes, List<String> excludes) {
345 		List<String> keys = getSortedKeys(properties);
346 		for (String key : keys) {
347 			if (!include(key, includes, excludes)) {
348 				logger.debug("Removing [{}]", key);
349 				properties.remove(key);
350 			}
351 		}
352 	}
353 
354 	/**
355 	 * Return true if <code>value</code> should be included, false otherwise.<br>
356 	 * If <code>excludes</code> is not empty and matches <code>value</code> return false.<br>
357 	 * If <code>value</code> has not been explicitly excluded, check the <code>includes</code> list.<br>
358 	 * If <code>includes</code> is empty return true.<br>
359 	 * If <code>includes</code> is not empty, return true if, and only if, <code>value</code> matches a pattern from the <code>includes</code> list.<br>
360 	 * A single wildcard <code>*</code> is supported for <code>includes</code> and <code>excludes</code>.<br>
361 	 */
362 	public static final boolean include(String value, List<String> includes, List<String> excludes) {
363 		if (isSingleWildcardMatch(value, excludes)) {
364 			// No point incurring the overhead of matching an include pattern
365 			return false;
366 		} else {
367 			// If includes is empty always return true
368 			return CollectionUtils.isEmpty(includes) || isSingleWildcardMatch(value, includes);
369 		}
370 	}
371 
372 	public static final boolean isSingleWildcardMatch(String s, List<String> patterns) {
373 		for (String pattern : CollectionUtils.toEmptyList(patterns)) {
374 			if (isSingleWildcardMatch(s, pattern)) {
375 				return true;
376 			}
377 		}
378 		return false;
379 	}
380 
381 	/**
382 	 * Match {@code value} against {@code pattern} where {@code pattern} can optionally contain a single wildcard {@code *}. If both are {@code null} return {@code true}. If one of
383 	 * {@code value} or {@code pattern} is {@code null} but the other isn't, return {@code false}. Any {@code pattern} containing more than a single wildcard throws
384 	 * {@code IllegalArgumentException}.
385 	 * 
386 	 * <pre>
387 	 * PropertyUtils.isSingleWildcardMatch(null, null)          = true
388 	 * PropertyUtils.isSingleWildcardMatch(null, *)             = false
389 	 * PropertyUtils.isSingleWildcardMatch(*, null)             = false
390 	 * PropertyUtils.isSingleWildcardMatch(*, "*")              = true
391 	 * PropertyUtils.isSingleWildcardMatch("abcdef", "bcd")     = false
392 	 * PropertyUtils.isSingleWildcardMatch("abcdef", "*def")    = true
393 	 * PropertyUtils.isSingleWildcardMatch("abcdef", "abc*")    = true
394 	 * PropertyUtils.isSingleWildcardMatch("abcdef", "ab*ef")   = true
395 	 * PropertyUtils.isSingleWildcardMatch("abcdef", "abc*def") = true
396 	 * PropertyUtils.isSingleWildcardMatch(*, "**")             = IllegalArgumentException
397 	 * </pre>
398 	 */
399 	public static final boolean isSingleWildcardMatch(String value, String pattern) {
400 		if (value == null && pattern == null) {
401 			// both are null
402 			return true;
403 		} else if (value != null && pattern == null || value == null && pattern != null) {
404 			// One is null, but not the other
405 			return false;
406 		} else if (pattern.equals(Constants.WILDCARD)) {
407 			// neither one is null and pattern is the wildcard. Value is irrelevant
408 			return true;
409 		} else if (StringUtils.countMatches(pattern, Constants.WILDCARD) > 1) {
410 			// More than one wildcard in the pattern is not supported
411 			throw new IllegalArgumentException("Pattern [" + pattern + "] is not supported.  Only one wildcard is allowed in the pattern");
412 		} else if (!StringUtils.contains(pattern, Constants.WILDCARD)) {
413 			// Neither one is null and there is no wildcard in the pattern. They must match exactly
414 			return StringUtils.equals(value, pattern);
415 		} else {
416 			// The pattern contains 1 (and only 1) wildcard
417 			// Make sure value starts with the characters to the left of the wildcard
418 			// and ends with the characters to the right of the wildcard
419 			int pos = StringUtils.indexOf(pattern, Constants.WILDCARD);
420 			int suffixPos = pos + Constants.WILDCARD.length();
421 			boolean nullPrefix = pos == 0;
422 			boolean nullSuffix = suffixPos >= pattern.length();
423 			String prefix = nullPrefix ? null : StringUtils.substring(pattern, 0, pos);
424 			String suffix = nullSuffix ? null : StringUtils.substring(pattern, suffixPos);
425 			boolean prefixMatch = nullPrefix || StringUtils.startsWith(value, prefix);
426 			boolean suffixMatch = nullSuffix || StringUtils.endsWith(value, suffix);
427 			return prefixMatch && suffixMatch;
428 		}
429 	}
430 
431 	/**
432 	 * Return property keys that should be included as a sorted list.
433 	 */
434 	public static final Properties getProperties(Properties properties, String include, String exclude) {
435 		List<String> keys = getSortedKeys(properties, include, exclude);
436 		Properties newProperties = new Properties();
437 		for (String key : keys) {
438 			String value = properties.getProperty(key);
439 			newProperties.setProperty(key, value);
440 		}
441 		return newProperties;
442 	}
443 
444 	/**
445 	 * Return property keys that should be included as a sorted list.
446 	 */
447 	public static final List<String> getSortedKeys(Properties properties, String include, String exclude) {
448 		return getSortedKeys(properties, CollectionUtils.toEmptyList(include), CollectionUtils.toEmptyList(exclude));
449 	}
450 
451 	/**
452 	 * Return property keys that should be included as a sorted list.
453 	 */
454 	public static final List<String> getSortedKeys(Properties properties, List<String> includes, List<String> excludes) {
455 		List<String> keys = getSortedKeys(properties);
456 		List<String> includedKeys = new ArrayList<String>();
457 		for (String key : keys) {
458 			if (include(key, includes, excludes)) {
459 				includedKeys.add(key);
460 			}
461 		}
462 		return includedKeys;
463 	}
464 
465 	/**
466 	 * Return a sorted <code>List</code> of keys from <code>properties</code> that start with <code>prefix</code>
467 	 */
468 	public static final List<String> getStartsWithKeys(Properties properties, String prefix) {
469 		List<String> keys = getSortedKeys(properties);
470 		List<String> matches = new ArrayList<String>();
471 		for (String key : keys) {
472 			if (StringUtils.startsWith(key, prefix)) {
473 				matches.add(key);
474 			}
475 		}
476 		return matches;
477 	}
478 
479 	/**
480 	 * Return the property keys as a sorted list.
481 	 */
482 	public static final List<String> getSortedKeys(Properties properties) {
483 		List<String> keys = new ArrayList<String>(properties.stringPropertyNames());
484 		Collections.sort(keys);
485 		return keys;
486 	}
487 
488 	public static final String toString(Properties properties) {
489 		List<String> keys = getSortedKeys(properties);
490 		StringBuilder sb = new StringBuilder();
491 		for (String key : keys) {
492 			String value = Str.flatten(properties.getProperty(key));
493 			sb.append(key + "=" + value + "\n");
494 		}
495 		return sb.toString();
496 	}
497 
498 	public static final void info(Properties properties) {
499 		properties = toEmpty(properties);
500 		logger.info("--- Displaying {} properties ---\n\n{}", properties.size(), toString(properties));
501 	}
502 
503 	public static final void debug(Properties properties) {
504 		properties = toEmpty(properties);
505 		logger.debug("--- Displaying {} properties ---\n\n{}", properties.size(), toString(properties));
506 	}
507 
508 	/**
509 	 * Store the properties to the indicated file using the platform default encoding.
510 	 */
511 	public static final void store(Properties properties, File file) {
512 		store(properties, file, null);
513 	}
514 
515 	/**
516 	 * Store the properties to the indicated file using the indicated encoding.
517 	 */
518 	public static final void store(Properties properties, File file, String encoding) {
519 		store(properties, file, encoding, null);
520 	}
521 
522 	/**
523 	 * Store the properties to the indicated file using the indicated encoding with the indicated comment appearing at the top of the file.
524 	 */
525 	public static final void store(Properties properties, File file, String encoding, String comment) {
526 		OutputStream out = null;
527 		Writer writer = null;
528 		try {
529 			out = FileUtils.openOutputStream(file);
530 			String path = file.getCanonicalPath();
531 			boolean xml = isXml(path);
532 			Properties sorted = getSortedProperties(properties);
533 			comment = getComment(encoding, comment, xml);
534 			if (xml) {
535 				logger.info("Storing XML properties - [{}] encoding={}", path, StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING));
536 				if (encoding == null) {
537 					sorted.storeToXML(out, comment);
538 				} else {
539 					sorted.storeToXML(out, comment, encoding);
540 				}
541 			} else {
542 				writer = LocationUtils.getWriter(out, encoding);
543 				logger.info("Storing properties - [{}] encoding={}", path, StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING));
544 				sorted.store(writer, comment);
545 			}
546 		} catch (IOException e) {
547 			throw new IllegalStateException("Unexpected IO error", e);
548 		} finally {
549 			IOUtils.closeQuietly(writer);
550 			IOUtils.closeQuietly(out);
551 		}
552 	}
553 
554 	/**
555 	 * Return a new properties object containing the properties from <code>getEnvAsProperties()</code> and <code>System.getProperties()</code>. Properties from
556 	 * <code>System.getProperties()</code> override properties from <code>getEnvAsProperties</code> if there are duplicates.
557 	 */
558 	public static final Properties getGlobalProperties() {
559 		return getProperties(Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
560 	}
561 
562 	/**
563 	 * Return a new properties object containing the properties passed in, plus any properties returned by <code>getEnvAsProperties()</code> and <code>System.getProperties()</code>
564 	 * . Properties from <code>getEnvAsProperties()</code> override <code>properties</code> and properties from <code>System.getProperties()</code> override everything.
565 	 */
566 	public static final Properties getGlobalProperties(Properties properties) {
567 		return getProperties(properties, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
568 	}
569 
570 	/**
571 	 * Return a new properties object containing the properties passed in, plus any global properties as requested. If <code>mode</code> is <code>NONE</code> the new properties are
572 	 * a duplicate of the properties passed in. If <code>mode</code> is <code>ENVIRONMENT</code> the new properties contain the original properties plus any properties returned by
573 	 * <code>getEnvProperties()</code>. If <code>mode</code> is <code>SYSTEM</code> the new properties contain the original properties plus <code>System.getProperties()</code>. If
574 	 * <code>mode</code> is <code>BOTH</code> the new properties contain the original properties plus <code>getEnvProperties()</code> and <code>System.getProperties()</code>.
575 	 */
576 	public static final Properties getProperties(Properties properties, GlobalPropertiesMode mode) {
577 		Properties newProperties = duplicate(properties);
578 		List<PropertyProcessor> modifiers = getPropertyProcessors(mode);
579 		for (PropertyProcessor modifier : modifiers) {
580 			modifier.process(newProperties);
581 		}
582 		return newProperties;
583 	}
584 
585 	/**
586 	 * Return a new properties object containing global properties as requested. If <code>mode</code> is <code>NONE</code> the new properties are empty. If <code>mode</code> is
587 	 * <code>ENVIRONMENT</code> the new properties contain the properties returned by <code>getEnvProperties()</code>. If <code>mode</code> is <code>SYSTEM</code> the new
588 	 * properties contain <code>System.getProperties()</code>. If <code>mode</code> is <code>BOTH</code> the new properties contain <code>getEnvProperties</code> plus
589 	 * <code>System.getProperties()</code> with system properties overriding environment variables if the same case sensitive property key is supplied in both places.
590 	 */
591 	public static final Properties getProperties(GlobalPropertiesMode mode) {
592 		return getProperties(new Properties(), mode);
593 	}
594 
595 	/**
596 	 * Search global properties to find a value for <code>key</code> according to the mode passed in.
597 	 */
598 	public static final String getProperty(String key, GlobalPropertiesMode mode) {
599 		return getProperty(key, new Properties(), mode);
600 	}
601 
602 	/**
603 	 * Search <code>properties</code> plus global properties to find a value for <code>key</code> according to the mode passed in. If the property is present in both, the value
604 	 * from the global properties is returned.
605 	 */
606 	public static final String getProperty(String key, Properties properties, GlobalPropertiesMode mode) {
607 		return getProperties(properties, mode).getProperty(key);
608 	}
609 
610 	/**
611 	 * Return modifiers that add environment variables, system properties, or both, according to the mode passed in.
612 	 */
613 	public static final List<PropertyProcessor> getPropertyProcessors(GlobalPropertiesMode mode) {
614 		List<PropertyProcessor> processors = new ArrayList<PropertyProcessor>();
615 		switch (mode) {
616 		case NONE:
617 			return processors;
618 		case ENVIRONMENT:
619 			processors.add(new AddPropertiesProcessor(getEnvAsProperties()));
620 			return processors;
621 		case SYSTEM:
622 			processors.add(new AddPropertiesProcessor(System.getProperties()));
623 			return processors;
624 		case BOTH:
625 			processors.add(new AddPropertiesProcessor(getEnvAsProperties()));
626 			processors.add(new AddPropertiesProcessor(System.getProperties()));
627 			return processors;
628 		default:
629 			throw new IllegalStateException(mode + " is unknown");
630 		}
631 	}
632 
633 	/**
634 	 * Convert the <code>Map</code> to a <code>Properties</code> object.
635 	 */
636 	public static final Properties convert(Map<String, String> map) {
637 		Properties props = new Properties();
638 		for (String key : map.keySet()) {
639 			String value = map.get(key);
640 			props.setProperty(key, value);
641 		}
642 		return props;
643 	}
644 
645 	/**
646 	 * Return a new properties object that duplicates the properties passed in.
647 	 */
648 	public static final Properties duplicate(Properties properties) {
649 		Properties newProperties = new Properties();
650 		newProperties.putAll(properties);
651 		return newProperties;
652 	}
653 
654 	/**
655 	 * Return a new properties object containing environment variables as properties prefixed with <code>env</code>
656 	 */
657 	public static Properties getEnvAsProperties() {
658 		return getEnvAsProperties(ENV_PREFIX);
659 	}
660 
661 	/**
662 	 * Return a new properties object containing environment variables as properties prefixed with <code>prefix</code>
663 	 */
664 	public static Properties getEnvAsProperties(String prefix) {
665 		Properties properties = convert(System.getenv());
666 		return getPrefixedProperties(properties, prefix);
667 	}
668 
669 	/**
670 	 * Return true if, and only if, location ends with <code>.xml</code> (case insensitive).
671 	 */
672 	public static final boolean isXml(String location) {
673 		return StringUtils.endsWithIgnoreCase(location, XML_EXTENSION);
674 	}
675 
676 	/**
677 	 * Return a new <code>Properties</code> object loaded from <code>file</code>.
678 	 */
679 	public static final Properties load(File file) {
680 		return load(file, null);
681 	}
682 
683 	/**
684 	 * Return a new <code>Properties</code> object loaded from <code>file</code> using the given encoding.
685 	 */
686 	public static final Properties load(File file, String encoding) {
687 		String location = LocationUtils.getCanonicalPath(file);
688 		return load(location, encoding);
689 	}
690 
691 	/**
692 	 * Return a new <code>Properties</code> object loaded from <code>location</code>.
693 	 */
694 	public static final Properties load(String location) {
695 		return load(location, null);
696 	}
697 
698 	/**
699 	 * Return a new <code>Properties</code> object loaded from <code>location</code> using <code>encoding</code>.
700 	 */
701 	public static final Properties load(String location, String encoding) {
702 		InputStream in = null;
703 		Reader reader = null;
704 		try {
705 			Properties properties = new Properties();
706 			boolean xml = isXml(location);
707 			location = getCanonicalLocation(location);
708 			if (xml) {
709 				in = LocationUtils.getInputStream(location);
710 				logger.info("Loading XML properties - [{}]", location);
711 				properties.loadFromXML(in);
712 			} else {
713 				logger.info("Loading properties - [{}] encoding={}", location, StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING));
714 				reader = LocationUtils.getBufferedReader(location, encoding);
715 				properties.load(reader);
716 			}
717 			return properties;
718 		} catch (IOException e) {
719 			throw new IllegalStateException("Unexpected IO error", e);
720 		} finally {
721 			IOUtils.closeQuietly(in);
722 			IOUtils.closeQuietly(reader);
723 		}
724 	}
725 
726 	protected static String getCanonicalLocation(String location) {
727 		if (LocationUtils.isExistingFile(location)) {
728 			return LocationUtils.getCanonicalPath(new File(location));
729 		} else {
730 			return location;
731 		}
732 	}
733 
734 	/**
735 	 * Return a new <code>Properties</code> object containing properties prefixed with <code>prefix</code>. If <code>prefix</code> is blank, the new properties object duplicates
736 	 * the properties passed in.
737 	 */
738 	public static final Properties getPrefixedProperties(Properties properties, String prefix) {
739 		if (StringUtils.isBlank(prefix)) {
740 			return duplicate(properties);
741 		}
742 		Properties newProperties = new Properties();
743 		for (String key : properties.stringPropertyNames()) {
744 			String value = properties.getProperty(key);
745 			String newKey = StringUtils.startsWith(key, prefix + ".") ? key : prefix + "." + key;
746 			newProperties.setProperty(newKey, value);
747 		}
748 		return newProperties;
749 	}
750 
751 	/**
752 	 * Return a new properties object where the keys have been converted to upper case and periods have been replaced with an underscore.
753 	 */
754 	public static final Properties reformatKeysAsEnvVars(Properties properties) {
755 		Properties newProperties = new Properties();
756 		for (String key : properties.stringPropertyNames()) {
757 			String value = properties.getProperty(key);
758 			String newKey = StringUtils.upperCase(StringUtils.replace(key, ".", "-"));
759 			newProperties.setProperty(newKey, value);
760 		}
761 		return newProperties;
762 	}
763 
764 	/**
765 	 * 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
766 	 * <code>propertyOverwriteMode</code> to make sure we have permission to override the value.
767 	 */
768 	public static final void addOrOverrideProperty(Properties properties, String key, String newValue, Mode propertyOverwriteMode) {
769 		String oldValue = properties.getProperty(key);
770 		if (StringUtils.equals(newValue, oldValue)) {
771 			// Nothing to do! New value is the same as old value.
772 			return;
773 		}
774 		boolean overwrite = !StringUtils.isBlank(oldValue);
775 
776 		// TODO Yuck! Do something smarter here
777 		String logNewValue = newValue;
778 		String logOldValue = oldValue;
779 		if (obscure(key)) {
780 			logNewValue = "PROTECTED";
781 			logOldValue = "PROTECTED";
782 		}
783 
784 		if (overwrite) {
785 			// This property already has a value, and it is different from the new value
786 			// Check to make sure we are allowed to override the old value before doing so
787 			Object[] args = new Object[] { key, Str.flatten(logNewValue), Str.flatten(logOldValue) };
788 			ModeUtils.validate(propertyOverwriteMode, "Overriding [{}={}] was [{}]", args, "Override of existing property [" + key + "] is not allowed.");
789 		} else {
790 			// There is no existing value for this key
791 			logger.info("Adding [{}={}]", key, Str.flatten(logNewValue));
792 		}
793 		properties.setProperty(key, newValue);
794 	}
795 
796 	protected static boolean obscure(String key) {
797 		if (StringUtils.containsIgnoreCase(key, ".password")) {
798 			return true;
799 		}
800 		if (StringUtils.containsIgnoreCase(key, ".secret")) {
801 			return true;
802 		}
803 		if (StringUtils.containsIgnoreCase(key, ".private")) {
804 			return true;
805 		}
806 		return false;
807 	}
808 
809 	private static final String getDefaultComment(String encoding, boolean xml) {
810 		if (encoding == null) {
811 			if (xml) {
812 				// Java defaults XML properties files to UTF-8 if no encoding is provided
813 				return "encoding.default=" + DEFAULT_XML_ENCODING;
814 			} else {
815 				// For normal properties files the platform default encoding is used
816 				return "encoding.default=" + DEFAULT_ENCODING;
817 			}
818 		} else {
819 			return "encoding.specified=" + encoding;
820 		}
821 	}
822 
823 	private static final String getComment(String encoding, String comment, boolean xml) {
824 		if (StringUtils.isBlank(comment)) {
825 			return getDefaultComment(encoding, xml);
826 		} else {
827 			return comment + "\n#" + getDefaultComment(encoding, xml);
828 		}
829 	}
830 
831 	/**
832 	 * This is private because <code>SortedProperties</code> does not fully honor the contract for <code>Properties</code>
833 	 */
834 	private static final SortedProperties getSortedProperties(Properties properties) {
835 		SortedProperties sp = new PropertyUtils().new SortedProperties();
836 		sp.putAll(properties);
837 		return sp;
838 	}
839 
840 	/**
841 	 * This is private since it does not honor the full contract for <code>Properties</code>. <code>PropertyUtils</code> uses it internally to store properties in sorted order.
842 	 */
843 	private class SortedProperties extends Properties {
844 
845 		private static final long serialVersionUID = 1330825236411537386L;
846 
847 		/**
848 		 * <code>Properties.storeToXML()</code> uses <code>keySet()</code>
849 		 */
850 		@Override
851 		public Set<Object> keySet() {
852 			return Collections.unmodifiableSet(new TreeSet<Object>(super.keySet()));
853 		}
854 
855 		/**
856 		 * <code>Properties.store()</code> uses <code>keys()</code>
857 		 */
858 		@Override
859 		public synchronized Enumeration<Object> keys() {
860 			return Collections.enumeration(new TreeSet<Object>(super.keySet()));
861 		}
862 	}
863 
864 	/**
865 	 * Set properties in the given Properties to CSV versions of the lists in the ComparisonResults
866 	 * 
867 	 * @param properties
868 	 *            the Properties to populate
869 	 * @param listComparison
870 	 *            the ComparisonResults to use for data
871 	 * @param propertyNames
872 	 *            the list of property keys to set. Exactly 3 names are required, and the assumed order is: index 0: key for the ADDED list index 1: key for the SAME list index 2:
873 	 *            key for the DELETED list
874 	 */
875 	public static final void addListComparisonProperties(Properties properties, ComparisonResults listComparison, List<String> propertyNames) {
876 		// make sure that there are three names in the list of property names
877 		Assert.isTrue(propertyNames.size() == 3);
878 
879 		properties.setProperty(propertyNames.get(0), CollectionUtils.getCSV(listComparison.getAdded()));
880 		properties.setProperty(propertyNames.get(1), CollectionUtils.getCSV(listComparison.getSame()));
881 		properties.setProperty(propertyNames.get(2), CollectionUtils.getCSV(listComparison.getDeleted()));
882 	}
883 
884 }