View Javadoc
1   /**
2    * Copyright 2010-2014 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.SortedMap;
34  import java.util.TreeSet;
35  
36  import org.apache.commons.io.FileUtils;
37  import org.apache.commons.io.IOUtils;
38  import org.apache.commons.lang3.StringUtils;
39  import org.jasypt.util.text.TextEncryptor;
40  import org.kuali.common.util.properties.rice.RiceLoader;
41  import org.kuali.common.util.property.Constants;
42  import org.kuali.common.util.property.GlobalPropertiesMode;
43  import org.kuali.common.util.property.ImmutableProperties;
44  import org.kuali.common.util.property.PropertiesContext;
45  import org.kuali.common.util.property.PropertyFormat;
46  import org.kuali.common.util.property.processor.AddPropertiesProcessor;
47  import org.kuali.common.util.property.processor.PropertyProcessor;
48  import org.slf4j.Logger;
49  import org.slf4j.LoggerFactory;
50  import org.springframework.util.PropertyPlaceholderHelper;
51  
52  import com.google.common.base.Optional;
53  import com.google.common.collect.Maps;
54  
55  /**
56   * 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
57   * understand. When storing and loading, locations ending in <code>.xml</code> are automatically handled using <code>storeToXML()</code> and <code>loadFromXML()</code>,
58   * respectively. <code>Properties</code> are always stored in sorted order with the <code>encoding</code> indicated via a comment.
59   */
60  public class PropertyUtils {
61  
62  	private static final Logger logger = LoggerFactory.getLogger(PropertyUtils.class);
63  
64  	public static final String ADDITIONAL_LOCATIONS = "properties.additional.locations";
65  	public static final String ADDITIONAL_LOCATIONS_ENCODING = ADDITIONAL_LOCATIONS + ".encoding";
66  	public static final Properties EMPTY = new ImmutableProperties(new Properties());
67  
68  	private static final String XML_EXTENSION = ".xml";
69  	private static final PropertyPlaceholderHelper HELPER = new PropertyPlaceholderHelper("${", "}", ":", false);
70  	public static final String ENV_PREFIX = "env";
71  	private static final String DEFAULT_ENCODING = Charset.defaultCharset().name();
72  	private static final String DEFAULT_XML_ENCODING = Encodings.UTF8;
73  
74  	/**
75  	 * If there is no value for <code>key</code> or the value is NULL or NONE, return Optional.absent(), otherwise return Optional.of(value)
76  	 */
77  	public static Optional<String> getOptionalString(Properties properties, String key) {
78  		if (properties.getProperty(key) == null) {
79  			return Optional.absent();
80  		} else {
81  			return Optional.of(properties.getProperty(key));
82  		}
83  	}
84  
85  	public static Optional<String> getString(Properties properties, String key, Optional<String> provided) {
86  		Optional<String> value = getOptionalString(properties, key);
87  		if (value.isPresent()) {
88  			return value;
89  		} else {
90  			return provided;
91  		}
92  	}
93  
94  	/**
95  	 * If the properties passed in are already immutable, just return them, otherwise, return a new ImmutableProperties object
96  	 */
97  	public static Properties toImmutable(Properties properties) {
98  		return (properties instanceof ImmutableProperties) ? properties : new ImmutableProperties(properties);
99  	}
100 
101 	/**
102 	 * The list returned by this method is unmodifiable and contains only <code>ImmutableProperties</code>
103 	 */
104 	public static List<Properties> toImmutable(List<Properties> properties) {
105 		List<Properties> immutables = new ArrayList<Properties>();
106 		for (Properties p : properties) {
107 			immutables.add(toImmutable(p));
108 		}
109 		return Collections.unmodifiableList(immutables);
110 	}
111 
112 	/**
113 	 * Return true if the value for <code>key</code> evaluates to the string <code>true</code> (ignoring case). The properties passed in along with the system and environment
114 	 * variables are all inspected with the value for system or environment variables "winning" over the value from the properties passed in.
115 	 */
116 	public static boolean getGlobalBoolean(String key, Properties properties) {
117 		String defaultValue = properties.getProperty(key);
118 		String value = getGlobalProperty(key, defaultValue);
119 		return Boolean.parseBoolean(value);
120 	}
121 
122 	public static boolean getBoolean(String key, Properties properties, boolean defaultValue) {
123 		String value = properties.getProperty(key);
124 		if (value == null) {
125 			return defaultValue;
126 		} else {
127 			return Boolean.parseBoolean(value);
128 		}
129 	}
130 
131 	/**
132 	 * Return true if both contain an identical set of string keys and values, or both are <code>null</code>, false otherwise.
133 	 */
134 	public static boolean equals(Properties one, Properties two) {
135 
136 		// Return true if they are the same object
137 		if (one == two) {
138 			return true;
139 		}
140 
141 		// Return true if they are both null
142 		if (one == null && two == null) {
143 			return true;
144 		}
145 
146 		// If we get here, both are not null (but one or the other might be)
147 
148 		// Return false if one is null but not the other
149 		if (one == null || two == null) {
150 			return false;
151 		}
152 
153 		// If we get here, neither one is null
154 
155 		// Extract the string property keys
156 		List<String> keys1 = getSortedKeys(one);
157 		List<String> keys2 = getSortedKeys(two);
158 
159 		// If the sizes are different, return false
160 		if (keys1.size() != keys2.size()) {
161 			return false;
162 		}
163 
164 		// If we get here, they have the same number of string property keys
165 
166 		// The sizes are the same, just pick one
167 		int size = keys1.size();
168 
169 		// Iterate through the keys comparing both the keys and values for equality
170 		for (int i = 0; i < size; i++) {
171 
172 			// Extract the keys
173 			String key1 = keys1.get(i);
174 			String key2 = keys2.get(i);
175 
176 			// Compare the keys for equality (this works because the keys are in sorted order)
177 			if (!StringUtils.equals(key1, key2)) {
178 				return false;
179 			}
180 
181 			// Extract the values
182 			String val1 = one.getProperty(key1);
183 			String val2 = two.getProperty(key2);
184 
185 			// Compare the values for equality
186 			if (!StringUtils.equals(val1, val2)) {
187 				return false;
188 			}
189 		}
190 
191 		// If we get here we know 3 things:
192 
193 		// 1 - Both have the exact same number of string based keys/values
194 		// 2 - Both have an identical set of string based keys
195 		// 3 - Both have the exact same string value for each string key
196 
197 		// This means they are equal, return true
198 		return true;
199 
200 	}
201 
202 	public static String getProperty(Properties properties, String key, String defaultValue) {
203 		String value = properties.getProperty(key);
204 		if (StringUtils.isBlank(value)) {
205 			return defaultValue;
206 		} else {
207 			return value;
208 		}
209 	}
210 
211 	public static boolean isEmpty(Properties properties) {
212 		return properties == null || properties.size() == 0;
213 	}
214 
215 	public static String getRiceXML(Properties properties) {
216 		StringBuilder sb = new StringBuilder();
217 		sb.append("<config>\n");
218 		List<String> keys = getSortedKeys(properties);
219 		for (String key : keys) {
220 			String value = properties.getProperty(key);
221 			// Convert to CDATA if the value contains characters that would blow up an XML parser
222 			if (StringUtils.contains(value, "<") || StringUtils.contains(value, "&")) {
223 				value = Str.cdata(value);
224 			}
225 			sb.append("  <param name=" + Str.quote(key) + ">");
226 			sb.append(value);
227 			sb.append("</param>\n");
228 		}
229 		sb.append("</config>\n");
230 		return sb.toString();
231 	}
232 
233 	public static String getRequiredResolvedProperty(Properties properties, String key) {
234 		return getRequiredResolvedProperty(properties, key, null);
235 	}
236 
237 	public static String getRequiredResolvedProperty(Properties properties, String key, String defaultValue) {
238 		String value = properties.getProperty(key);
239 		value = StringUtils.isBlank(value) ? defaultValue : value;
240 		if (StringUtils.isBlank(value)) {
241 			throw new IllegalArgumentException("[" + key + "] is not set");
242 		} else {
243 			return HELPER.replacePlaceholders(value, properties);
244 		}
245 	}
246 
247 	/**
248 	 * Process the properties passed in so they are ready for use by a Spring context.<br>
249 	 * 
250 	 * 1 - Override with system/environment properties<br>
251 	 * 2 - Decrypt any ENC(...) values<br>
252 	 * 3 - Resolve all property values throwing an exception if any are unresolvable.<br>
253 	 */
254 	public static void prepareContextProperties(Properties properties, String encoding) {
255 
256 		// Override with additional properties (if any)
257 		properties.putAll(getAdditionalProperties(properties, encoding));
258 
259 		// Override with system/environment properties
260 		properties.putAll(getGlobalProperties());
261 
262 		// Are we decrypting property values?
263 		decrypt(properties);
264 
265 		// Are we resolving placeholders
266 		resolve(properties);
267 	}
268 
269 	/**
270 	 * Process the properties passed in so they are ready for use by a Spring context.<br>
271 	 * 
272 	 * 1 - Override with system/environment properties<br>
273 	 * 2 - Decrypt any ENC(...) values<br>
274 	 * 3 - Resolve all property values throwing an exception if any are unresolvable.<br>
275 	 */
276 	public static void prepareContextProperties(Properties properties) {
277 		prepareContextProperties(properties, null);
278 	}
279 
280 	public static void resolve(Properties properties, PropertyPlaceholderHelper helper) {
281 		List<String> keys = getSortedKeys(properties);
282 		for (String key : keys) {
283 			String original = properties.getProperty(key);
284 			String resolved = helper.replacePlaceholders(original, properties);
285 			if (!StringUtils.equals(original, resolved)) {
286 				logger.debug("Resolved [{}]", key);
287 				properties.setProperty(key, resolved);
288 			}
289 		}
290 	}
291 
292 	public static void removeSystemProperty(String key) {
293 		if (System.getProperty(key) != null) {
294 			logger.info("Removing system property [{}]", key);
295 			System.getProperties().remove(key);
296 		}
297 	}
298 
299 	public static void removeSystemProperties(List<String> keys) {
300 		for (String key : keys) {
301 			removeSystemProperty(key);
302 		}
303 	}
304 
305 	@Deprecated
306 	public static void resolve(Properties properties) {
307 		// Are we resolving placeholders?
308 		boolean resolve = new Boolean(getRequiredResolvedProperty(properties, "properties.resolve", "true"));
309 		if (resolve) {
310 			org.kuali.common.util.property.processor.ResolvePlaceholdersProcessor rpp = new org.kuali.common.util.property.processor.ResolvePlaceholdersProcessor();
311 			rpp.setHelper(HELPER);
312 			rpp.process(properties);
313 		}
314 	}
315 
316 	@Deprecated
317 	public static void decrypt(Properties properties) {
318 		// Are we decrypting property values?
319 		boolean decrypt = Boolean.parseBoolean(getRequiredResolvedProperty(properties, "properties.decrypt", "false"));
320 		if (decrypt) {
321 			// If they asked to decrypt, a password is required
322 			String password = getRequiredResolvedProperty(properties, "properties.enc.password");
323 
324 			// Strength is optional (defaults to BASIC)
325 			String defaultStrength = org.kuali.common.util.enc.EncStrength.BASIC.name();
326 			String strength = getRequiredResolvedProperty(properties, "properties.enc.strength", defaultStrength);
327 			org.kuali.common.util.enc.EncStrength es = org.kuali.common.util.enc.EncStrength.valueOf(strength);
328 			TextEncryptor decryptor = org.kuali.common.util.enc.EncUtils.getTextEncryptor(password, es);
329 			decrypt(properties, decryptor);
330 		}
331 	}
332 
333 	public static Properties getAdditionalProperties(Properties properties) {
334 		return getAdditionalProperties(properties, null);
335 	}
336 
337 	public static Properties getAdditionalProperties(Properties properties, String encoding) {
338 		String csv = properties.getProperty(ADDITIONAL_LOCATIONS);
339 		if (StringUtils.isBlank(csv)) {
340 			return new Properties();
341 		}
342 		if (StringUtils.isBlank(encoding)) {
343 			encoding = properties.getProperty(ADDITIONAL_LOCATIONS_ENCODING, DEFAULT_XML_ENCODING);
344 		}
345 		List<String> locations = CollectionUtils.getTrimmedListFromCSV(csv);
346 		PropertiesContext context = new PropertiesContext(locations, encoding);
347 		return load(context);
348 	}
349 
350 	public static void appendToOrSetProperty(Properties properties, String key, String value) {
351 		Assert.hasText(value);
352 		String existingValue = properties.getProperty(key);
353 		if (existingValue == null) {
354 			existingValue = "";
355 		}
356 		String newValue = existingValue + value;
357 		properties.setProperty(key, newValue);
358 	}
359 
360 	@Deprecated
361 	public static Properties load(List<org.kuali.common.util.property.ProjectProperties> pps) {
362 
363 		// Create some storage for the Properties object we will be returning
364 		Properties properties = new Properties();
365 
366 		// Cycle through the list of project properties, loading them as we go
367 		for (org.kuali.common.util.property.ProjectProperties pp : pps) {
368 
369 			logger.debug("oracle.dba.url.1={}", properties.getProperty("oracle.dba.url"));
370 
371 			// Extract the properties context object
372 			PropertiesContext ctx = pp.getPropertiesContext();
373 
374 			// Retain the original properties object from the context
375 			Properties original = PropertyUtils.duplicate(PropertyUtils.toEmpty(ctx.getProperties()));
376 
377 			// Override any existing property values with properties stored directly on the context
378 			Properties combined = PropertyUtils.combine(properties, ctx.getProperties());
379 
380 			// Store the combined properties on the context itself
381 			ctx.setProperties(combined);
382 
383 			// Load properties as dictated by the context
384 			Properties loaded = load(ctx);
385 
386 			logger.debug("oracle.dba.url.2={}", loaded.getProperty("oracle.dba.url"));
387 
388 			// Override any existing property values with those we just loaded
389 			properties.putAll(loaded);
390 
391 			// Override any existing property values with the properties that were stored directly on the context
392 			properties.putAll(original);
393 
394 		}
395 
396 		// Return the property values we now have
397 		return properties;
398 	}
399 
400 	public static Properties load(PropertiesContext context) {
401 		// If there are no locations specified, add the properties supplied directly on the context (if there are any)
402 		if (CollectionUtils.isEmpty(context.getLocations())) {
403 			return PropertyUtils.toEmpty(context.getProperties());
404 		}
405 
406 		// Make sure we are configured correctly
407 		Assert.notNull(context.getHelper(), "helper is null");
408 		Assert.notNull(context.getLocations(), "locations are null");
409 		Assert.notNull(context.getEncoding(), "encoding is null");
410 		Assert.notNull(context.getMissingLocationsMode(), "missingLocationsMode is null");
411 
412 		// Get system/environment properties
413 		Properties global = PropertyUtils.getGlobalProperties();
414 
415 		// Convert null to an empty properties object (if necessary)
416 		context.setProperties(PropertyUtils.toEmpty(context.getProperties()));
417 
418 		// Create new storage for the properties we are loading
419 		Properties result = new Properties();
420 
421 		// Add in any properties stored directly on the context itself (these get overridden by properties loaded elsewhere)
422 		result.putAll(context.getProperties());
423 
424 		// Cycle through the locations, loading and storing properties as we go
425 		for (String location : context.getLocations()) {
426 
427 			// Get a combined Properties object capable of resolving any placeholders that exist in the property location strings
428 			Properties resolverProperties = PropertyUtils.combine(context.getProperties(), result, global);
429 
430 			// Make sure we have a fully resolved location to load Properties from
431 			String resolvedLocation = context.getHelper().replacePlaceholders(location, resolverProperties);
432 
433 			// If the location exists, load properties from it
434 			if (LocationUtils.exists(resolvedLocation)) {
435 
436 				// Load this set of Properties
437 				Properties properties = PropertyUtils.load(resolvedLocation, context.getEncoding());
438 
439 				// Add these properties to the result. This follows the traditional "last one in wins" strategy
440 				result.putAll(properties);
441 			} else {
442 
443 				// Handle missing locations (might be fine, may need to emit a logging statement, may need to error out)
444 				ModeUtils.validate(context.getMissingLocationsMode(), "Non-existent location [" + resolvedLocation + "]");
445 			}
446 		}
447 
448 		// Return the properties we loaded
449 		return result;
450 	}
451 
452 	/**
453 	 * Decrypt any encrypted property values. Encrypted values are surrounded by ENC(...), like:
454 	 * 
455 	 * <pre>
456 	 * my.value = ENC(DGA$S24FaIO)
457 	 * </pre>
458 	 */
459 	public static void decrypt(Properties properties, TextEncryptor encryptor) {
460 		decrypt(properties, encryptor, null, null);
461 	}
462 
463 	/**
464 	 * Return a new <code>Properties</code> object (never null) containing only those properties whose values are encrypted. Encrypted values are surrounded by ENC(...), like:
465 	 * 
466 	 * <pre>
467 	 * my.value = ENC(DGA$S24FaIO)
468 	 * </pre>
469 	 */
470 	public static Properties getEncryptedProperties(Properties properties) {
471 		List<String> keys = getEncryptedKeys(properties);
472 		Properties encrypted = new Properties();
473 		for (String key : keys) {
474 			String value = properties.getProperty(key);
475 			encrypted.setProperty(key, value);
476 		}
477 		return encrypted;
478 	}
479 
480 	/**
481 	 * Return a list containing only those keys whose values are encrypted. Encrypted values are surrounded by ENC(...), like:
482 	 * 
483 	 * <pre>
484 	 * my.value = ENC(DGA$S24FaIO)
485 	 * </pre>
486 	 * 
487 	 * @deprecated
488 	 */
489 	@Deprecated
490 	public static List<String> getEncryptedKeys(Properties properties) {
491 		List<String> all = getSortedKeys(properties);
492 		List<String> encrypted = new ArrayList<String>();
493 		for (String key : all) {
494 			String value = properties.getProperty(key);
495 			if (org.kuali.common.util.enc.EncUtils.isEncrypted(value)) {
496 				encrypted.add(key);
497 			}
498 		}
499 		return encrypted;
500 	}
501 
502 	/**
503 	 * Decrypt any encrypted property values matching the <code>includes</code>, <code>excludes</code> patterns. Encrypted values are surrounded by ENC(...).
504 	 * 
505 	 * <pre>
506 	 * my.value = ENC(DGA$S24FaIO)
507 	 * </pre>
508 	 * 
509 	 * @deprecated
510 	 */
511 	@Deprecated
512 	public static void decrypt(Properties properties, TextEncryptor encryptor, List<String> includes, List<String> excludes) {
513 		List<String> keys = getSortedKeys(properties, includes, excludes);
514 		for (String key : keys) {
515 			String value = properties.getProperty(key);
516 			if (org.kuali.common.util.enc.EncUtils.isEncrypted(value)) {
517 				String decryptedValue = decryptPropertyValue(encryptor, value);
518 				properties.setProperty(key, decryptedValue);
519 			}
520 		}
521 	}
522 
523 	/**
524 	 * Return true if the value starts with <code>ENC(</code> and ends with <code>)</code>, false otherwise.
525 	 * 
526 	 * @deprecated Use EncUtils.isEncrypted(value) instead
527 	 */
528 	@Deprecated
529 	public static boolean isEncryptedPropertyValue(String value) {
530 		return org.kuali.common.util.enc.EncUtils.isEncrypted(value);
531 	}
532 
533 	/**
534 	 * <p>
535 	 * A trivial way to conceal property values. Can be reversed using <code>reveal()</code>. Do <b>NOT</b> use this method in an attempt to obscure sensitive data. The algorithm
536 	 * is completely trivial and exceedingly simple to reverse engineer. Not to mention, the <code>reveal()</code> method can reproduce the original string without requiring any
537 	 * secret knowledge.
538 	 * </p>
539 	 * 
540 	 * <p>
541 	 * The use case here is to help prevent someone with otherwise mostly good intentions from altering a piece of information in a way they should not. This is <b>NOT</b> intended
542 	 * to defeat any serious attempt at discovering the original text.
543 	 * </p>
544 	 * 
545 	 * <p>
546 	 * Think a hungry sales or marketing rep who stumbles across a config file with the entry <code>vending.machine.refill.day=wed</code> in it and tries to change that to
547 	 * <code>mon</code> in order to beat a case of the munchies. :)
548 	 * </p>
549 	 * 
550 	 * <p>
551 	 * If the entry says <code>vending.machine.refill.day=cnc--jrq</code> instead of <code>vending.machine.refill.day=wed</code> they are far more likely to ask around before they
552 	 * change it <b>OR</b> just give up and head out to lunch instead.
553 	 * </p>
554 	 * 
555 	 * @see reveal
556 	 */
557 	public static void conceal(Properties properties) {
558 		List<String> keys = getSortedKeys(properties);
559 		for (String key : keys) {
560 			String value = properties.getProperty(key);
561 			String concealed = Str.conceal(value);
562 			properties.setProperty(key, concealed);
563 		}
564 	}
565 
566 	/**
567 	 * Reveal property values that were concealed by the <code>conceal</code> method
568 	 * 
569 	 * <pre>
570 	 * foo=cnc--one.onm -> foo=bar.baz
571 	 * </pre>
572 	 */
573 	public static void reveal(Properties properties) {
574 		List<String> keys = getSortedKeys(properties);
575 		for (String key : keys) {
576 			String value = properties.getProperty(key);
577 			String revealed = Str.reveal(value);
578 			properties.setProperty(key, revealed);
579 		}
580 	}
581 
582 	/**
583 	 * Encrypt all of the property values. Encrypted values are surrounded by ENC(...).
584 	 * 
585 	 * <pre>
586 	 * my.value = ENC(DGA$S24FaIO)
587 	 * </pre>
588 	 */
589 	public static void encrypt(Properties properties, TextEncryptor encryptor) {
590 		encrypt(properties, encryptor, null, null);
591 	}
592 
593 	/**
594 	 * Encrypt properties as dictated by <code>includes</code> and <code>excludes</code>. Encrypted values are surrounded by ENC(...).
595 	 * 
596 	 * <pre>
597 	 * my.value = ENC(DGA$S24FaIO)
598 	 * </pre>
599 	 */
600 	public static void encrypt(Properties properties, TextEncryptor encryptor, List<String> includes, List<String> excludes) {
601 		List<String> keys = getSortedKeys(properties, includes, excludes);
602 		for (String key : keys) {
603 			String originalValue = properties.getProperty(key);
604 			String encryptedValue = encryptPropertyValue(encryptor, originalValue);
605 			properties.setProperty(key, encryptedValue);
606 		}
607 	}
608 
609 	/**
610 	 * Return the decrypted version of the property value. Encrypted values are surrounded by ENC(...).
611 	 * 
612 	 * <pre>
613 	 * my.value = ENC(DGA$S24FaIO)
614 	 * </pre>
615 	 */
616 	public static String decryptPropertyValue(TextEncryptor encryptor, String value) {
617 		String unwrapped = unwrapEncryptedValue(value);
618 
619 		// Return the decrypted value
620 		return encryptor.decrypt(unwrapped);
621 	}
622 
623 	/**
624 	 * Remove the leading <code>ENC(</code> prefix and the trailing <code>)</code> from the encrypted value passed in.
625 	 * 
626 	 * <pre>
627 	 * ENC(DGA$S24FaIO) -> DGA$S24FaIO
628 	 * </pre>
629 	 * 
630 	 * @deprecated Use EncUtils.unwrap(value) instead
631 	 */
632 	@Deprecated
633 	public static String unwrapEncryptedValue(String encryptedValue) {
634 		return org.kuali.common.util.enc.EncUtils.unwrap(encryptedValue);
635 	}
636 
637 	/**
638 	 * Return the encrypted version of the property value. A value is considered "encrypted" when it appears surrounded by ENC(...).
639 	 * 
640 	 * <pre>
641 	 * my.value = ENC(DGA$S24FaIO)
642 	 * </pre>
643 	 */
644 	public static String encryptPropertyValue(TextEncryptor encryptor, String value) {
645 		String encryptedValue = encryptor.encrypt(value);
646 		return wrapEncryptedPropertyValue(encryptedValue);
647 	}
648 
649 	/**
650 	 * Return the value enclosed with ENC()
651 	 * 
652 	 * <pre>
653 	 * DGA$S24FaIO -> ENC(DGA$S24FaIO)
654 	 * </pre>
655 	 * 
656 	 * @deprecated Use EncUtils.wrap(value) instead
657 	 */
658 	@Deprecated
659 	public static String wrapEncryptedPropertyValue(String encryptedValue) {
660 		return org.kuali.common.util.enc.EncUtils.wrap(encryptedValue);
661 	}
662 
663 	public static void overrideWithGlobalValues(Properties properties, GlobalPropertiesMode mode) {
664 		List<String> keys = PropertyUtils.getSortedKeys(properties);
665 		Properties global = PropertyUtils.getProperties(mode);
666 		for (String key : keys) {
667 			String globalValue = global.getProperty(key);
668 			if (!StringUtils.isBlank(globalValue)) {
669 				properties.setProperty(key, globalValue);
670 			}
671 		}
672 	}
673 
674 	public static final Properties[] toArray(List<Properties> properties) {
675 		return properties.toArray(new Properties[properties.size()]);
676 	}
677 
678 	public static final Properties combine(Properties properties, List<Properties> list) {
679 		List<Properties> newList = new ArrayList<Properties>(CollectionUtils.toEmptyList(list));
680 		newList.add(0, toEmpty(properties));
681 		return combine(newList);
682 	}
683 
684 	public static final Properties combine(List<Properties> properties) {
685 		Properties combined = new Properties();
686 		for (Properties p : properties) {
687 			combined.putAll(toEmpty(p));
688 		}
689 		return combined;
690 	}
691 
692 	public static final Properties combine(Properties... properties) {
693 		return combine(Arrays.asList(properties));
694 	}
695 
696 	public static final void process(Properties properties, PropertyProcessor processor) {
697 		process(properties, Collections.singletonList(processor));
698 	}
699 
700 	public static final void process(Properties properties, List<PropertyProcessor> processors) {
701 		executeProcessors(properties, processors);
702 	}
703 
704 	public static final void executeProcessors(Properties properties, List<PropertyProcessor> processors) {
705 		for (PropertyProcessor processor : CollectionUtils.toEmptyList(processors)) {
706 			processor.process(properties);
707 		}
708 	}
709 
710 	public static final Properties toEmpty(Properties properties) {
711 		return properties == null ? new Properties() : properties;
712 	}
713 
714 	public static final boolean isSingleUnresolvedPlaceholder(String string) {
715 		return isSingleUnresolvedPlaceholder(string, Constants.DEFAULT_PLACEHOLDER_PREFIX, Constants.DEFAULT_PLACEHOLDER_SUFFIX);
716 	}
717 
718 	public static final boolean isSingleUnresolvedPlaceholder(String string, String prefix, String suffix) {
719 		int prefixMatches = StringUtils.countMatches(string, prefix);
720 		int suffixMatches = StringUtils.countMatches(string, suffix);
721 		boolean startsWith = StringUtils.startsWith(string, prefix);
722 		boolean endsWith = StringUtils.endsWith(string, suffix);
723 		return prefixMatches == 1 && suffixMatches == 1 && startsWith && endsWith;
724 	}
725 
726 	public static final boolean containsUnresolvedPlaceholder(String string) {
727 		return containsUnresolvedPlaceholder(string, Constants.DEFAULT_PLACEHOLDER_PREFIX, Constants.DEFAULT_PLACEHOLDER_SUFFIX);
728 	}
729 
730 	public static final boolean containsUnresolvedPlaceholder(String string, String prefix, String suffix) {
731 		int beginIndex = StringUtils.indexOf(string, prefix);
732 		if (beginIndex == -1) {
733 			return false;
734 		}
735 		return StringUtils.indexOf(string, suffix) != -1;
736 	}
737 
738 	/**
739 	 * 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
740 	 * perform property resolution as indicated by <code>Constants.DEFAULT_GLOBAL_PROPERTIES_MODE</code>
741 	 */
742 	public static final Properties getResolvedProperties(Properties properties) {
743 		return getResolvedProperties(properties, Constants.DEFAULT_PROPERTY_PLACEHOLDER_HELPER, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
744 	}
745 
746 	/**
747 	 * 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
748 	 * perform property resolution as indicated by <code>globalPropertiesMode</code>
749 	 */
750 	public static final Properties getResolvedProperties(Properties properties, GlobalPropertiesMode globalPropertiesMode) {
751 		return getResolvedProperties(properties, Constants.DEFAULT_PROPERTY_PLACEHOLDER_HELPER, globalPropertiesMode);
752 	}
753 
754 	/**
755 	 * 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
756 	 * perform property resolution as indicated by <code>Constants.DEFAULT_GLOBAL_PROPERTIES_MODE</code>
757 	 */
758 	public static final Properties getResolvedProperties(Properties properties, PropertyPlaceholderHelper helper) {
759 		return getResolvedProperties(properties, helper, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
760 	}
761 
762 	/**
763 	 * 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
764 	 * perform property resolution as indicated by <code>globalPropertiesMode</code>
765 	 */
766 	public static final Properties getResolvedProperties(Properties properties, PropertyPlaceholderHelper helper, GlobalPropertiesMode globalPropertiesMode) {
767 		Properties global = getProperties(properties, globalPropertiesMode);
768 		List<String> keys = getSortedKeys(properties);
769 		Properties newProperties = new Properties();
770 		for (String key : keys) {
771 			String originalValue = properties.getProperty(key);
772 			String resolvedValue = helper.replacePlaceholders(originalValue, global);
773 			if (!resolvedValue.equals(originalValue)) {
774 				logger.debug("Resolved property '" + key + "' [{}] -> [{}]", Str.flatten(originalValue), Str.flatten(resolvedValue));
775 				newProperties.setProperty(key, resolvedValue);
776 			}
777 		}
778 		return newProperties;
779 	}
780 
781 	/**
782 	 * Return the property values from <code>keys</code>
783 	 */
784 	public static final List<String> getValues(Properties properties, List<String> keys) {
785 		List<String> values = new ArrayList<String>();
786 		for (String key : keys) {
787 			values.add(properties.getProperty(key));
788 		}
789 		return values;
790 	}
791 
792 	/**
793 	 * Return a sorted <code>List</code> of keys from <code>properties</code> that end with <code>suffix</code>.
794 	 */
795 	public static final List<String> getEndsWithKeys(Properties properties, String suffix) {
796 		List<String> keys = getSortedKeys(properties);
797 		List<String> matches = new ArrayList<String>();
798 		for (String key : keys) {
799 			if (StringUtils.endsWith(key, suffix)) {
800 				matches.add(key);
801 			}
802 		}
803 		return matches;
804 	}
805 
806 	/**
807 	 * 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.
808 	 */
809 	public static final void trim(Properties properties, String includesCSV, String excludesCSV) {
810 		List<String> includes = CollectionUtils.getTrimmedListFromCSV(includesCSV);
811 		List<String> excludes = CollectionUtils.getTrimmedListFromCSV(excludesCSV);
812 		trim(properties, includes, excludes);
813 	}
814 
815 	/**
816 	 * Alter the <code>properties</code> passed in to contain only the desired property values.
817 	 */
818 	public static final void trim(Properties properties, List<String> includes, List<String> excludes) {
819 		List<String> keys = getSortedKeys(properties);
820 		for (String key : keys) {
821 			if (!include(key, includes, excludes)) {
822 				logger.debug("Removing [{}]", key);
823 				properties.remove(key);
824 			}
825 		}
826 	}
827 
828 	/**
829 	 * Return true if <code>value</code> should be included, false otherwise.<br>
830 	 * If <code>excludes</code> is not empty and matches <code>value</code> return false.<br>
831 	 * If <code>value</code> has not been explicitly excluded, check the <code>includes</code> list.<br>
832 	 * If <code>includes</code> is empty return true.<br>
833 	 * 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>
834 	 * A single wildcard <code>*</code> is supported for <code>includes</code> and <code>excludes</code>.<br>
835 	 */
836 	public static final boolean include(String value, List<String> includes, List<String> excludes) {
837 		if (isSingleWildcardMatch(value, excludes)) {
838 			// No point incurring the overhead of matching an include pattern
839 			return false;
840 		} else {
841 			// If includes is empty always return true
842 			return CollectionUtils.isEmpty(includes) || isSingleWildcardMatch(value, includes);
843 		}
844 	}
845 
846 	public static final boolean isSingleWildcardMatch(String s, List<String> patterns) {
847 		for (String pattern : CollectionUtils.toEmptyList(patterns)) {
848 			if (isSingleWildcardMatch(s, pattern)) {
849 				return true;
850 			}
851 		}
852 		return false;
853 	}
854 
855 	/**
856 	 * 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
857 	 * {@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
858 	 * {@code IllegalArgumentException}.
859 	 * 
860 	 * <pre>
861 	 * PropertyUtils.isSingleWildcardMatch(null, null)          = true
862 	 * PropertyUtils.isSingleWildcardMatch(null, *)             = false
863 	 * PropertyUtils.isSingleWildcardMatch(*, null)             = false
864 	 * PropertyUtils.isSingleWildcardMatch(*, "*")              = true
865 	 * PropertyUtils.isSingleWildcardMatch("abcdef", "bcd")     = false
866 	 * PropertyUtils.isSingleWildcardMatch("abcdef", "*def")    = true
867 	 * PropertyUtils.isSingleWildcardMatch("abcdef", "abc*")    = true
868 	 * PropertyUtils.isSingleWildcardMatch("abcdef", "ab*ef")   = true
869 	 * PropertyUtils.isSingleWildcardMatch("abcdef", "abc*def") = true
870 	 * PropertyUtils.isSingleWildcardMatch(*, "**")             = IllegalArgumentException
871 	 * </pre>
872 	 */
873 	public static final boolean isSingleWildcardMatch(String value, String pattern) {
874 		if (value == null && pattern == null) {
875 			// both are null
876 			return true;
877 		} else if (value == null || pattern == null) {
878 			// One is null, but not the other
879 			return false;
880 		} else if (pattern.equals(Constants.WILDCARD)) {
881 			// Neither one is null and pattern is the unqualified wildcard.
882 			// Value is irrelevant, always return true
883 			return true;
884 		} else if (StringUtils.countMatches(pattern, Constants.WILDCARD) > 1) {
885 			// More than one wildcard in the pattern is not supported
886 			throw new IllegalArgumentException("Pattern [" + pattern + "] is not supported.  Only one wildcard is allowed in the pattern");
887 		} else if (!StringUtils.contains(pattern, Constants.WILDCARD)) {
888 			// Neither one is null and there is no wildcard in the pattern. They must match exactly
889 			return StringUtils.equals(value, pattern);
890 		} else {
891 			// The pattern contains 1 (and only 1) wildcard
892 			// Make sure value starts with the characters to the left of the wildcard
893 			// and ends with the characters to the right of the wildcard
894 			int pos = StringUtils.indexOf(pattern, Constants.WILDCARD);
895 			int suffixPos = pos + Constants.WILDCARD.length();
896 			boolean nullPrefix = pos == 0;
897 			boolean nullSuffix = suffixPos >= pattern.length();
898 			String prefix = nullPrefix ? null : StringUtils.substring(pattern, 0, pos);
899 			String suffix = nullSuffix ? null : StringUtils.substring(pattern, suffixPos);
900 			boolean prefixMatch = nullPrefix || StringUtils.startsWith(value, prefix);
901 			boolean suffixMatch = nullSuffix || StringUtils.endsWith(value, suffix);
902 			return prefixMatch && suffixMatch;
903 		}
904 	}
905 
906 	/**
907 	 * Return property keys that should be included as a sorted list.
908 	 */
909 	public static final Properties getProperties(Properties properties, String include, String exclude) {
910 		List<String> keys = getSortedKeys(properties, include, exclude);
911 		Properties newProperties = new Properties();
912 		for (String key : keys) {
913 			String value = properties.getProperty(key);
914 			newProperties.setProperty(key, value);
915 		}
916 		return newProperties;
917 	}
918 
919 	/**
920 	 * Return property keys that should be included as a sorted list.
921 	 */
922 	public static final List<String> getSortedKeys(Properties properties, String include, String exclude) {
923 		return getSortedKeys(properties, CollectionUtils.toEmptyList(include), CollectionUtils.toEmptyList(exclude));
924 	}
925 
926 	/**
927 	 * Return property keys that should be included as a sorted list.
928 	 */
929 	public static final List<String> getSortedKeys(Properties properties, List<String> includes, List<String> excludes) {
930 		List<String> keys = getSortedKeys(properties);
931 		List<String> includedKeys = new ArrayList<String>();
932 		for (String key : keys) {
933 			if (include(key, includes, excludes)) {
934 				includedKeys.add(key);
935 			}
936 		}
937 		return includedKeys;
938 	}
939 
940 	/**
941 	 * Return a sorted <code>List</code> of keys from <code>properties</code> that start with <code>prefix</code>
942 	 */
943 	public static final List<String> getStartsWithKeys(Properties properties, String prefix) {
944 		List<String> keys = getSortedKeys(properties);
945 		List<String> matches = new ArrayList<String>();
946 		for (String key : keys) {
947 			if (StringUtils.startsWith(key, prefix)) {
948 				matches.add(key);
949 			}
950 		}
951 		return matches;
952 	}
953 
954 	/**
955 	 * Return the property keys as a sorted list.
956 	 */
957 	public static final List<String> getSortedKeys(Properties properties) {
958 		List<String> keys = new ArrayList<String>(properties.stringPropertyNames());
959 		Collections.sort(keys);
960 		return keys;
961 	}
962 
963 	public static final String toString(Properties properties) {
964 		List<String> keys = getSortedKeys(properties);
965 		StringBuilder sb = new StringBuilder();
966 		for (String key : keys) {
967 			String value = Str.flatten(properties.getProperty(key));
968 			sb.append(key + "=" + value + "\n");
969 		}
970 		return sb.toString();
971 	}
972 
973 	public static final void info(Properties properties) {
974 		properties = toEmpty(properties);
975 		logger.info("--- Displaying {} properties ---\n\n{}", properties.size(), toString(properties));
976 	}
977 
978 	public static final void debug(Properties properties) {
979 		properties = toEmpty(properties);
980 		logger.debug("--- Displaying {} properties ---\n\n{}", properties.size(), toString(properties));
981 	}
982 
983 	/**
984 	 * Store the properties to the indicated file using the platform default encoding.
985 	 */
986 	public static final void store(Properties properties, File file) {
987 		store(properties, file, null);
988 	}
989 
990 	/**
991 	 * Store the properties to the indicated file using the platform default encoding.
992 	 */
993 	public static final void storeSilently(Properties properties, File file) {
994 		store(properties, file, null, null, true);
995 	}
996 
997 	/**
998 	 * Store the properties to the indicated file using the indicated encoding.
999 	 */
1000 	public static final void store(Properties properties, File file, String encoding) {
1001 		store(properties, file, encoding, null);
1002 	}
1003 
1004 	/**
1005 	 * Store the properties to the indicated file using the indicated encoding with the indicated comment appearing at the top of the file.
1006 	 */
1007 	public static final void store(Properties properties, File file, String encoding, String comment) {
1008 		store(properties, file, encoding, comment, false);
1009 	}
1010 
1011 	/**
1012 	 * Store the properties to the indicated file using the indicated encoding with the indicated comment appearing at the top of the file.
1013 	 */
1014 	public static final void store(Properties properties, File file, String encoding, String comment, boolean silent) {
1015 		OutputStream out = null;
1016 		Writer writer = null;
1017 		try {
1018 			out = FileUtils.openOutputStream(file);
1019 			String path = file.getCanonicalPath();
1020 			boolean xml = isXml(path);
1021 			Properties sorted = getSortedProperties(properties);
1022 			comment = getComment(encoding, comment, xml);
1023 			if (xml) {
1024 				if (!silent) {
1025 					logger.info("Storing XML properties - [{}] encoding={}", path, StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING));
1026 				}
1027 				if (encoding == null) {
1028 					sorted.storeToXML(out, comment);
1029 				} else {
1030 					sorted.storeToXML(out, comment, encoding);
1031 				}
1032 			} else {
1033 				writer = LocationUtils.getWriter(out, encoding);
1034 				if (!silent) {
1035 					logger.info("Storing properties - [{}] encoding={}", path, StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING));
1036 				}
1037 				sorted.store(writer, comment);
1038 			}
1039 		} catch (IOException e) {
1040 			throw new IllegalStateException("Unexpected IO error", e);
1041 		} finally {
1042 			IOUtils.closeQuietly(writer);
1043 			IOUtils.closeQuietly(out);
1044 		}
1045 	}
1046 
1047 	/**
1048 	 * Examine both system properties and environment variables to get a value for <code>key</code>. Return <code>null</code> if nothing is found.
1049 	 * 
1050 	 * <pre>
1051 	 *   foo.bar -> System property check for "foo.bar"
1052 	 *   foo.bar -> Environment check for "FOO_BAR"
1053 	 * </pre>
1054 	 */
1055 	public static final String getGlobalProperty(String key) {
1056 		return getGlobalProperty(key, null);
1057 	}
1058 
1059 	/**
1060 	 * Examine both system properties and environment variables to get a value for <code>key</code>. Return <code>defaultValue</code> if nothing is found
1061 	 * 
1062 	 * <pre>
1063 	 *   foo.bar -> System property check for "foo.bar"
1064 	 *   foo.bar -> Environment check for "FOO_BAR"
1065 	 * </pre>
1066 	 */
1067 	public static final String getGlobalProperty(String key, String defaultValue) {
1068 		Assert.noNullsWithMsg("key is required", key);
1069 
1070 		// Check to see if there is a system property for this key
1071 		String systemValue = System.getProperty(key);
1072 
1073 		// If so, we are done
1074 		if (systemValue != null) {
1075 			return systemValue;
1076 		}
1077 
1078 		// Reformat the key as an environment variable key
1079 		String environmentVariable = convertToEnvironmentVariable(key);
1080 
1081 		// Check to see if we have a match for an environment variable
1082 		String environmentValue = System.getenv(environmentVariable);
1083 
1084 		if (environmentValue != null) {
1085 			// If so, return the value of the environment variable
1086 			return environmentValue;
1087 		} else {
1088 			// If not, return the default value
1089 			return defaultValue;
1090 		}
1091 	}
1092 
1093 	/**
1094 	 * Return a new properties object containing the properties from <code>getEnvAsProperties()</code> and <code>System.getProperties()</code>. Properties from
1095 	 * <code>System.getProperties()</code> override properties from <code>getEnvAsProperties</code> if there are duplicates.
1096 	 */
1097 	public static final Properties getGlobalProperties() {
1098 		return getProperties(Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
1099 	}
1100 
1101 	/**
1102 	 * Return a new properties object containing the properties passed in, plus any properties returned by <code>getEnvAsProperties()</code> and <code>System.getProperties()</code>
1103 	 * . Properties from <code>getEnvAsProperties()</code> override <code>properties</code> and properties from <code>System.getProperties()</code> override everything.
1104 	 */
1105 	public static final Properties getGlobalProperties(Properties properties) {
1106 		return getProperties(properties, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
1107 	}
1108 
1109 	/**
1110 	 * 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
1111 	 * 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
1112 	 * <code>getEnvProperties()</code>. If <code>mode</code> is <code>SYSTEM</code> the new properties contain the original properties plus <code>System.getProperties()</code>. If
1113 	 * <code>mode</code> is <code>BOTH</code> the new properties contain the original properties plus <code>getEnvProperties()</code> and <code>System.getProperties()</code>.
1114 	 */
1115 	public static final Properties getProperties(Properties properties, GlobalPropertiesMode mode) {
1116 		Properties newProperties = duplicate(properties);
1117 		List<PropertyProcessor> modifiers = getPropertyProcessors(mode);
1118 		for (PropertyProcessor modifier : modifiers) {
1119 			modifier.process(newProperties);
1120 		}
1121 		return newProperties;
1122 	}
1123 
1124 	/**
1125 	 * 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
1126 	 * <code>ENVIRONMENT</code> the new properties contain the properties returned by <code>getEnvProperties()</code>. If <code>mode</code> is <code>SYSTEM</code> the new
1127 	 * properties contain <code>System.getProperties()</code>. If <code>mode</code> is <code>BOTH</code> the new properties contain <code>getEnvProperties</code> plus
1128 	 * <code>System.getProperties()</code> with system properties overriding environment variables if the same case sensitive property key is supplied in both places.
1129 	 */
1130 	public static final Properties getProperties(GlobalPropertiesMode mode) {
1131 		return getProperties(new Properties(), mode);
1132 	}
1133 
1134 	/**
1135 	 * Search global properties to find a value for <code>key</code> according to the mode passed in.
1136 	 */
1137 	public static final String getProperty(String key, GlobalPropertiesMode mode) {
1138 		return getProperty(key, new Properties(), mode);
1139 	}
1140 
1141 	/**
1142 	 * 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
1143 	 * from the global properties is returned.
1144 	 */
1145 	public static final String getProperty(String key, Properties properties, GlobalPropertiesMode mode) {
1146 		return getProperties(properties, mode).getProperty(key);
1147 	}
1148 
1149 	/**
1150 	 * Return modifiers that add environment variables, system properties, or both, according to the mode passed in.
1151 	 */
1152 	public static final List<PropertyProcessor> getPropertyProcessors(GlobalPropertiesMode mode) {
1153 		List<PropertyProcessor> processors = new ArrayList<PropertyProcessor>();
1154 		switch (mode) {
1155 		case NONE:
1156 			return processors;
1157 		case ENVIRONMENT:
1158 			processors.add(new AddPropertiesProcessor(getEnvAsProperties()));
1159 			return processors;
1160 		case SYSTEM:
1161 			processors.add(new AddPropertiesProcessor(System.getProperties()));
1162 			return processors;
1163 		case BOTH:
1164 			processors.add(new AddPropertiesProcessor(getEnvAsProperties()));
1165 			processors.add(new AddPropertiesProcessor(System.getProperties()));
1166 			return processors;
1167 		default:
1168 			throw new IllegalStateException(mode + " is unknown");
1169 		}
1170 	}
1171 
1172 	/**
1173 	 * Convert the <code>Map</code> to a <code>Properties</code> object.
1174 	 */
1175 	public static final Properties convert(Map<String, String> map) {
1176 		return convertToProperties(map);
1177 	}
1178 
1179 	/**
1180 	 * Convert the <code>Map</code> to a <code>Properties</code> object.
1181 	 */
1182 	public static final Properties convertToProperties(Map<String, String> map) {
1183 		Properties props = new Properties();
1184 		for (String key : map.keySet()) {
1185 			props.setProperty(key, map.get(key));
1186 		}
1187 		return props;
1188 	}
1189 
1190 	/**
1191 	 * Convert the <code>Properties</code> to a <code>Map</code> object.
1192 	 */
1193 	public static Map<String, String> convert(Properties properties) {
1194 		return newHashMap(properties);
1195 	}
1196 
1197 	/**
1198 	 * Convert the {@code Properties} to a {@code Map<String,String>} object.
1199 	 */
1200 	public static Map<String, String> newHashMap(Properties properties) {
1201 		Map<String, String> map = Maps.newHashMap();
1202 		for (String key : properties.stringPropertyNames()) {
1203 			map.put(key, properties.getProperty(key));
1204 		}
1205 		return map;
1206 	}
1207 
1208 	/**
1209 	 * Convert the {@code Properties} to a {@code Map<String,String>} object.
1210 	 */
1211 	public static SortedMap<String, String> newTreeMap(Properties properties) {
1212 		SortedMap<String, String> map = Maps.newTreeMap();
1213 		for (String key : properties.stringPropertyNames()) {
1214 			map.put(key, properties.getProperty(key));
1215 		}
1216 		return map;
1217 	}
1218 
1219 	/**
1220 	 * Return a new properties object that duplicates the properties passed in.
1221 	 */
1222 	public static final Properties duplicate(Properties properties) {
1223 		Properties newProperties = new Properties();
1224 		newProperties.putAll(properties);
1225 		return newProperties;
1226 	}
1227 
1228 	/**
1229 	 * Return a new properties object containing environment variables as properties prefixed with <code>env</code>
1230 	 */
1231 	public static Properties getEnvAsProperties() {
1232 		return getEnvAsProperties(ENV_PREFIX);
1233 	}
1234 
1235 	/**
1236 	 * Return a new properties object containing environment variables as properties prefixed with <code>prefix</code>
1237 	 */
1238 	public static Properties getEnvAsProperties(String prefix) {
1239 		Properties properties = convert(System.getenv());
1240 		return getPrefixedProperties(properties, prefix);
1241 	}
1242 
1243 	/**
1244 	 * Return true if, and only if, location ends with <code>.xml</code> (case insensitive).
1245 	 */
1246 	public static final boolean isXml(String location) {
1247 		return StringUtils.endsWithIgnoreCase(location, XML_EXTENSION);
1248 	}
1249 
1250 	/**
1251 	 * Return true if, and only if, location ends with <code>rice-properties.xml</code> (case insensitive).
1252 	 */
1253 	public static final boolean isRiceProperties(String location) {
1254 		return StringUtils.endsWithIgnoreCase(location, Constants.RICE_PROPERTIES_SUFFIX);
1255 	}
1256 
1257 	public static final Properties loadRiceProps(File file) {
1258 		return RiceLoader.load(file);
1259 	}
1260 
1261 	public static final Properties loadRiceProps(String location) {
1262 		return RiceLoader.load(location);
1263 	}
1264 
1265 	/**
1266 	 * Return a new <code>Properties</code> object loaded from <code>file</code> where the properties are stored in Rice XML style syntax
1267 	 * 
1268 	 * @deprecated use loadRiceProps() instead
1269 	 */
1270 	@Deprecated
1271 	public static final Properties loadRiceProperties(File file) {
1272 		return loadRiceProperties(LocationUtils.getCanonicalPath(file));
1273 	}
1274 
1275 	/**
1276 	 * Return a new <code>Properties</code> object loaded from <code>location</code> where the properties are stored in Rice XML style syntax
1277 	 * 
1278 	 * @deprecated use loadRiceProps() instead
1279 	 */
1280 	@Deprecated
1281 	public static final Properties loadRiceProperties(String location) {
1282 		logger.info("Loading Rice properties [{}] encoding={}", location, DEFAULT_XML_ENCODING);
1283 		String contents = LocationUtils.toString(location, DEFAULT_XML_ENCODING);
1284 		String config = StringUtils.substringBetween(contents, "<config>", "</config>");
1285 		String[] tokens = StringUtils.substringsBetween(config, "<param", "<param");
1286 
1287 		Properties properties = new Properties();
1288 		for (String token : tokens) {
1289 			String key = StringUtils.substringBetween(token, "name=\"", "\">");
1290 			validateRiceProperties(token, key);
1291 			String value = StringUtils.substringBetween(token + "</param>", "\">", "</param>");
1292 			properties.setProperty(key, value);
1293 		}
1294 		return properties;
1295 	}
1296 
1297 	/**
1298 	 * Make sure they are just loading simple properties and are not using any of the unsupported "features". Can't have a key named config.location, and can't use the system,
1299 	 * override, or random attributes.
1300 	 */
1301 	protected static final void validateRiceProperties(String token, String key) {
1302 		if (StringUtils.equalsIgnoreCase("config.location", key)) {
1303 			throw new IllegalArgumentException("config.location is not supported");
1304 		}
1305 		if (StringUtils.contains(token, "override=\"")) {
1306 			throw new IllegalArgumentException("override attribute is not supported");
1307 		}
1308 		if (StringUtils.contains(token, "system=\"")) {
1309 			throw new IllegalArgumentException("system attribute is not supported");
1310 		}
1311 		if (StringUtils.contains(token, "random=\"")) {
1312 			throw new IllegalArgumentException("random attribute is not supported");
1313 		}
1314 	}
1315 
1316 	/**
1317 	 * Return a new <code>Properties</code> object loaded from <code>file</code>.
1318 	 */
1319 	public static final Properties load(File file) {
1320 		return load(file, null);
1321 	}
1322 
1323 	/**
1324 	 * Return a new <code>Properties</code> object loaded from <code>file</code>.
1325 	 */
1326 	public static final Properties loadSilently(File file) {
1327 		return loadSilently(LocationUtils.getCanonicalPath(file));
1328 	}
1329 
1330 	/**
1331 	 * Return a new <code>Properties</code> object loaded from <code>file</code>.
1332 	 */
1333 	public static final Properties loadSilently(String location) {
1334 		return load(location, null, PropertyFormat.NORMAL, true);
1335 	}
1336 
1337 	/**
1338 	 * Return a new <code>Properties</code> object loaded from <code>file</code> using the given encoding.
1339 	 */
1340 	public static final Properties load(File file, String encoding) {
1341 		String location = LocationUtils.getCanonicalPath(file);
1342 		return load(location, encoding);
1343 	}
1344 
1345 	/**
1346 	 * Return a new <code>Properties</code> object loaded from <code>location</code>.
1347 	 */
1348 	public static final Properties load(String location) {
1349 		return load(location, null);
1350 	}
1351 
1352 	/**
1353 	 * If location exists, return a new <code>Properties</code> object loaded from <code>location</code>, otherwise return a new <code>Properties</code> object
1354 	 */
1355 	public static final Properties loadOrCreateSilently(String location) {
1356 		if (LocationUtils.exists(location)) {
1357 			return load(location, null, PropertyFormat.NORMAL, true);
1358 		} else {
1359 			return new Properties();
1360 		}
1361 	}
1362 
1363 	/**
1364 	 * Return a new <code>Properties</code> object loaded from <code>locations</code> using <code>encoding</code>.
1365 	 */
1366 	public static final Properties load(List<String> locations, String encoding) {
1367 		Properties properties = new Properties();
1368 		for (String location : locations) {
1369 			properties.putAll(load(location, encoding));
1370 		}
1371 		return properties;
1372 	}
1373 
1374 	/**
1375 	 * Return a new <code>Properties</code> object loaded from <code>location</code> using <code>encoding</code>.
1376 	 */
1377 	public static final Properties load(String location, String encoding) {
1378 		return load(location, encoding, PropertyFormat.NORMAL);
1379 	}
1380 
1381 	/**
1382 	 * Return a new <code>Properties</code> object loaded from <code>location</code> using <code>encoding</code>.
1383 	 */
1384 	public static final Properties load(String location, String encoding, PropertyFormat format) {
1385 		return load(location, encoding, format, false);
1386 	}
1387 
1388 	/**
1389 	 * Return a new <code>Properties</code> object loaded from <code>location</code> using <code>encoding</code>.
1390 	 */
1391 	public static final Properties load(String location, String encoding, PropertyFormat format, boolean silent) {
1392 		InputStream in = null;
1393 		Reader reader = null;
1394 		try {
1395 			Properties properties = new Properties();
1396 			boolean xml = isXml(location);
1397 			boolean riceProperties = isRiceProperties(location);
1398 			location = getCanonicalLocation(location);
1399 			if (PropertyFormat.RICE.equals(format) || riceProperties) {
1400 				properties = loadRiceProperties(location);
1401 			} else if (xml) {
1402 				in = LocationUtils.getInputStream(location);
1403 				if (!silent) {
1404 					logger.info("Loading XML properties - [{}]", location);
1405 				}
1406 				properties.loadFromXML(in);
1407 			} else {
1408 				if (!silent) {
1409 					logger.info("Loading properties - [{}] encoding={}", location, StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING));
1410 				}
1411 				reader = LocationUtils.getBufferedReader(location, encoding);
1412 				properties.load(reader);
1413 			}
1414 			return properties;
1415 		} catch (IOException e) {
1416 			throw new IllegalStateException("Unexpected IO error", e);
1417 		} finally {
1418 			IOUtils.closeQuietly(in);
1419 			IOUtils.closeQuietly(reader);
1420 		}
1421 	}
1422 
1423 	protected static String getCanonicalLocation(String location) {
1424 		if (LocationUtils.isExistingFile(location)) {
1425 			return LocationUtils.getCanonicalPath(new File(location));
1426 		} else {
1427 			return location;
1428 		}
1429 	}
1430 
1431 	/**
1432 	 * 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
1433 	 * the properties passed in.
1434 	 */
1435 	public static final Properties getPrefixedProperties(Properties properties, String prefix) {
1436 		if (StringUtils.isBlank(prefix)) {
1437 			return duplicate(properties);
1438 		}
1439 		Properties newProperties = new Properties();
1440 		for (String key : properties.stringPropertyNames()) {
1441 			String value = properties.getProperty(key);
1442 			String newKey = StringUtils.startsWith(key, prefix + ".") ? key : prefix + "." + key;
1443 			newProperties.setProperty(newKey, value);
1444 		}
1445 		return newProperties;
1446 	}
1447 
1448 	/**
1449 	 * Replace periods with an underscore and convert to uppercase
1450 	 * 
1451 	 * <pre>
1452 	 *   foo.bar -> FOO_BAR
1453 	 * </pre>
1454 	 */
1455 	public static final String convertToEnvironmentVariable(String key) {
1456 		return StringUtils.upperCase(StringUtils.replace(key, ".", "_"));
1457 	}
1458 
1459 	/**
1460 	 * Replace periods with an underscore, convert to uppercase, and prefix with <code>env</code>
1461 	 * 
1462 	 * <pre>
1463 	 *   foo.bar -> env.FOO_BAR
1464 	 * </pre>
1465 	 */
1466 	public static final String getEnvironmentVariableKey(String key) {
1467 		return ENV_PREFIX + "." + convertToEnvironmentVariable(key);
1468 	}
1469 
1470 	/**
1471 	 * Return a new properties object where the keys have been converted to upper case and periods have been replaced with an underscore.
1472 	 */
1473 	public static final Properties reformatKeysAsEnvVars(Properties properties) {
1474 		Properties newProperties = new Properties();
1475 		for (String key : properties.stringPropertyNames()) {
1476 			String value = properties.getProperty(key);
1477 			String newKey = convertToEnvironmentVariable(key);
1478 			newProperties.setProperty(newKey, value);
1479 		}
1480 		return newProperties;
1481 	}
1482 
1483 	/**
1484 	 * 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
1485 	 * <code>propertyOverwriteMode</code> to make sure we have permission to override the value.
1486 	 */
1487 	public static final void addOrOverrideProperty(Properties properties, String key, String newValue, Mode propertyOverwriteMode) {
1488 		addOrOverrideProperty(properties, key, newValue, propertyOverwriteMode, 0);
1489 	}
1490 
1491 	public static final void addOrOverrideProperty(Properties properties, String key, String newValue, Mode overrideMode, int indent) {
1492 		String oldValue = properties.getProperty(key);
1493 		if (StringUtils.equals(newValue, oldValue)) {
1494 			// Nothing to do! New value is the same as old value.
1495 			return;
1496 		}
1497 		boolean overwrite = !StringUtils.isBlank(oldValue);
1498 
1499 		String logNewValue = newValue;
1500 		String logOldValue = oldValue;
1501 
1502 		// TODO Yuck! Do something smarter here
1503 		if (obscure(key)) {
1504 			logNewValue = "*********";
1505 			logOldValue = "*********";
1506 		}
1507 
1508 		if (overwrite) {
1509 			// This property already has a value, and it is different from the new value
1510 			// Check to make sure we are allowed to override the old value before doing so
1511 			Object[] args = new Object[] { StringUtils.repeat(" ", indent), key, Str.flatten(logNewValue), Str.flatten(logOldValue) };
1512 			ModeUtils.validate(overrideMode, "{}override [{}] -> [{}]", args, "Override of existing property [" + key + "] is not allowed.");
1513 		} else {
1514 			// There is no existing value for this key
1515 			logger.debug("Adding [{}={}]", key, Str.flatten(logNewValue));
1516 		}
1517 		properties.setProperty(key, newValue);
1518 	}
1519 
1520 	protected static boolean obscure(String key) {
1521 		if (StringUtils.containsIgnoreCase(key, "password")) {
1522 			return true;
1523 		}
1524 		if (StringUtils.containsIgnoreCase(key, "secret")) {
1525 			return true;
1526 		}
1527 		if (StringUtils.containsIgnoreCase(key, "private")) {
1528 			return true;
1529 		}
1530 		return false;
1531 	}
1532 
1533 	private static final String getDefaultComment(String encoding, boolean xml) {
1534 		if (encoding == null) {
1535 			if (xml) {
1536 				// Java defaults XML properties files to UTF-8 if no encoding is provided
1537 				return "encoding.default=" + DEFAULT_XML_ENCODING;
1538 			} else {
1539 				// For normal properties files the platform default encoding is used
1540 				return "encoding.default=" + DEFAULT_ENCODING;
1541 			}
1542 		} else {
1543 			return "encoding.specified=" + encoding;
1544 		}
1545 	}
1546 
1547 	private static final String getComment(String encoding, String comment, boolean xml) {
1548 		if (StringUtils.isBlank(comment)) {
1549 			return getDefaultComment(encoding, xml);
1550 		} else {
1551 			return comment + "\n#" + getDefaultComment(encoding, xml);
1552 		}
1553 	}
1554 
1555 	/**
1556 	 * This is private because <code>SortedProperties</code> does not fully honor the contract for <code>Properties</code>
1557 	 */
1558 	private static final SortedProperties getSortedProperties(Properties properties) {
1559 		SortedProperties sp = new PropertyUtils().new SortedProperties();
1560 		sp.putAll(properties);
1561 		return sp;
1562 	}
1563 
1564 	/**
1565 	 * 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.
1566 	 */
1567 	private class SortedProperties extends Properties {
1568 
1569 		private static final long serialVersionUID = 1330825236411537386L;
1570 
1571 		/**
1572 		 * <code>Properties.storeToXML()</code> uses <code>keySet()</code>
1573 		 */
1574 		@Override
1575 		public Set<Object> keySet() {
1576 			return Collections.unmodifiableSet(new TreeSet<Object>(super.keySet()));
1577 		}
1578 
1579 		/**
1580 		 * <code>Properties.store()</code> uses <code>keys()</code>
1581 		 */
1582 		@Override
1583 		public synchronized Enumeration<Object> keys() {
1584 			return Collections.enumeration(new TreeSet<Object>(super.keySet()));
1585 		}
1586 	}
1587 
1588 	/**
1589 	 * Set properties in the given Properties to CSV versions of the lists in the ComparisonResults
1590 	 * 
1591 	 * @param properties
1592 	 *            the Properties to populate
1593 	 * @param listComparison
1594 	 *            the ComparisonResults to use for data
1595 	 * @param propertyNames
1596 	 *            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:
1597 	 *            key for the DELETED list
1598 	 */
1599 	public static final void addListComparisonProperties(Properties properties, ComparisonResults listComparison, List<String> propertyNames) {
1600 		// make sure that there are three names in the list of property names
1601 		Assert.isTrue(propertyNames.size() == 3);
1602 
1603 		properties.setProperty(propertyNames.get(0), CollectionUtils.getCSV(listComparison.getAdded()));
1604 		properties.setProperty(propertyNames.get(1), CollectionUtils.getCSV(listComparison.getSame()));
1605 		properties.setProperty(propertyNames.get(2), CollectionUtils.getCSV(listComparison.getDeleted()));
1606 	}
1607 
1608 }