View Javadoc

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