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