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