001    /**
002     * Copyright 2010-2012 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.common.util;
017    
018    import java.io.File;
019    import java.io.IOException;
020    import java.io.InputStream;
021    import java.io.OutputStream;
022    import java.io.Reader;
023    import java.io.Writer;
024    import java.nio.charset.Charset;
025    import java.util.ArrayList;
026    import java.util.Arrays;
027    import java.util.Collections;
028    import java.util.Enumeration;
029    import java.util.List;
030    import java.util.Map;
031    import java.util.Properties;
032    import java.util.Set;
033    import java.util.TreeSet;
034    
035    import org.apache.commons.io.FileUtils;
036    import org.apache.commons.io.IOUtils;
037    import org.apache.commons.lang3.StringUtils;
038    import org.kuali.common.util.property.Constants;
039    import org.kuali.common.util.property.GlobalPropertiesMode;
040    import org.kuali.common.util.property.processor.AddPropertiesProcessor;
041    import org.kuali.common.util.property.processor.PropertyProcessor;
042    import org.slf4j.Logger;
043    import org.slf4j.LoggerFactory;
044    import org.springframework.util.PropertyPlaceholderHelper;
045    
046    /**
047     * Simplify handling of <code>Properties</code> especially as it relates to storing and loading. <code>Properties</code>
048     * can be loaded from any url Spring resource loading can understand. When storing and loading, locations ending in
049     * <code>.xml</code> are automatically handled using <code>storeToXML()</code> and <code>loadFromXML()</code>,
050     * respectively. <code>Properties</code> are always stored in sorted order with the <code>encoding</code> indicated via
051     * a comment.
052     */
053    public class PropertyUtils {
054    
055            private static final Logger logger = LoggerFactory.getLogger(PropertyUtils.class);
056    
057            private static final String XML_EXTENSION = ".xml";
058            private static final String ENV_PREFIX = "env";
059            private static final String DEFAULT_ENCODING = Charset.defaultCharset().name();
060            private static final String DEFAULT_XML_ENCODING = "UTF-8";
061    
062            public static void overrideWithGlobalValues(Properties properties, GlobalPropertiesMode mode) {
063                    List<String> keys = PropertyUtils.getSortedKeys(properties);
064                    Properties global = PropertyUtils.getProperties(mode);
065                    for (String key : keys) {
066                            String globalValue = global.getProperty(key);
067                            if (!StringUtils.isBlank(globalValue)) {
068                                    properties.setProperty(key, globalValue);
069                            }
070                    }
071            }
072    
073            public static final Properties combine(List<Properties> properties) {
074                    Properties combined = new Properties();
075                    for (Properties p : properties) {
076                            combined.putAll(PropertyUtils.toEmpty(p));
077                    }
078                    return combined;
079            }
080    
081            public static final Properties combine(Properties... properties) {
082                    return combine(Arrays.asList(properties));
083            }
084    
085            public static final void process(Properties properties, PropertyProcessor processor) {
086                    process(properties, Collections.singletonList(processor));
087            }
088    
089            public static final void process(Properties properties, List<PropertyProcessor> processors) {
090                    for (PropertyProcessor processor : CollectionUtils.toEmptyList(processors)) {
091                            processor.process(properties);
092                    }
093            }
094    
095            public static final Properties toEmpty(Properties properties) {
096                    return properties == null ? new Properties() : properties;
097            }
098    
099            public static final boolean isSingleUnresolvedPlaceholder(String string) {
100                    return isSingleUnresolvedPlaceholder(string, Constants.DEFAULT_PLACEHOLDER_PREFIX,
101                                    Constants.DEFAULT_PLACEHOLDER_SUFFIX);
102            }
103    
104            public static final boolean isSingleUnresolvedPlaceholder(String string, String prefix, String suffix) {
105                    int prefixMatches = StringUtils.countMatches(string, prefix);
106                    int suffixMatches = StringUtils.countMatches(string, suffix);
107                    boolean startsWith = StringUtils.startsWith(string, prefix);
108                    boolean endsWith = StringUtils.endsWith(string, suffix);
109                    return prefixMatches == 1 && suffixMatches == 1 && startsWith && endsWith;
110            }
111    
112            public static final boolean containsUnresolvedPlaceholder(String string) {
113                    return containsUnresolvedPlaceholder(string, Constants.DEFAULT_PLACEHOLDER_PREFIX,
114                                    Constants.DEFAULT_PLACEHOLDER_SUFFIX);
115            }
116    
117            public static final boolean containsUnresolvedPlaceholder(String string, String prefix, String suffix) {
118                    int beginIndex = StringUtils.indexOf(string, prefix);
119                    if (beginIndex == -1) {
120                            return false;
121                    }
122                    return StringUtils.indexOf(string, suffix) != -1;
123            }
124    
125            /**
126             * Return a new <code>Properties</code> object containing only those properties where the resolved value is
127             * different from the original value. Using global properties to perform property resolution as indicated by
128             * <code>Constants.DEFAULT_GLOBAL_PROPERTIES_MODE</code>
129             */
130            public static final Properties getResolvedProperties(Properties properties) {
131                    return getResolvedProperties(properties, Constants.DEFAULT_PROPERTY_PLACEHOLDER_HELPER,
132                                    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
137             * different from the original value. Using global properties to perform property resolution as indicated by
138             * <code>globalPropertiesMode</code>
139             */
140            public static final Properties getResolvedProperties(Properties properties,
141                            GlobalPropertiesMode globalPropertiesMode) {
142                    return getResolvedProperties(properties, Constants.DEFAULT_PROPERTY_PLACEHOLDER_HELPER, globalPropertiesMode);
143            }
144    
145            /**
146             * Return a new <code>Properties</code> object containing only those properties where the resolved value is
147             * different from the original value. Using global properties to perform property resolution as indicated by
148             * <code>Constants.DEFAULT_GLOBAL_PROPERTIES_MODE</code>
149             */
150            public static final Properties getResolvedProperties(Properties properties, PropertyPlaceholderHelper helper) {
151                    return getResolvedProperties(properties, helper, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
152            }
153    
154            /**
155             * Return a new <code>Properties</code> object containing only those properties where the resolved value is
156             * different from the original value. Using global properties to perform property resolution as indicated by
157             * <code>globalPropertiesMode</code>
158             */
159            public static final Properties getResolvedProperties(Properties properties, PropertyPlaceholderHelper helper,
160                            GlobalPropertiesMode globalPropertiesMode) {
161                    Properties global = PropertyUtils.getProperties(properties, globalPropertiesMode);
162                    List<String> keys = PropertyUtils.getSortedKeys(properties);
163                    Properties newProperties = new Properties();
164                    for (String key : keys) {
165                            String originalValue = properties.getProperty(key);
166                            String resolvedValue = helper.replacePlaceholders(originalValue, global);
167                            if (!resolvedValue.equals(originalValue)) {
168                                    logger.debug("Resolved property '" + key + "' [{}] -> [{}]", Str.flatten(originalValue),
169                                                    Str.flatten(resolvedValue));
170                                    newProperties.setProperty(key, resolvedValue);
171                            }
172                    }
173                    return newProperties;
174            }
175    
176            /**
177             * Return the property values from <code>keys</code>
178             */
179            public static final List<String> getValues(Properties properties, List<String> keys) {
180                    List<String> values = new ArrayList<String>();
181                    for (String key : keys) {
182                            values.add(properties.getProperty(key));
183                    }
184                    return values;
185            }
186    
187            /**
188             * Return a sorted <code>List</code> of keys from <code>properties</code> that end with <code>suffix</code>.
189             */
190            public static final List<String> getEndsWithKeys(Properties properties, String suffix) {
191                    List<String> keys = getSortedKeys(properties);
192                    List<String> matches = new ArrayList<String>();
193                    for (String key : keys) {
194                            if (StringUtils.endsWith(key, suffix)) {
195                                    matches.add(key);
196                            }
197                    }
198                    return matches;
199            }
200    
201            /**
202             * Alter the <code>properties</code> passed in to contain only the desired property values. <code>includes</code>
203             * and <code>excludes</code> are comma separated values.
204             */
205            public static final void trim(Properties properties, String includesCSV, String excludesCSV) {
206                    List<String> includes = CollectionUtils.getTrimmedListFromCSV(includesCSV);
207                    List<String> excludes = CollectionUtils.getTrimmedListFromCSV(excludesCSV);
208                    trim(properties, includes, excludes);
209            }
210    
211            /**
212             * Alter the <code>properties</code> passed in to contain only the desired property values.
213             */
214            public static final void trim(Properties properties, List<String> includes, List<String> excludes) {
215                    List<String> keys = getSortedKeys(properties);
216                    for (String key : keys) {
217                            if (!include(key, includes, excludes)) {
218                                    logger.debug("Removing [{}]", key);
219                                    properties.remove(key);
220                            }
221                    }
222            }
223    
224            /**
225             * Return true if <code>value</code> should be included, false otherwise.<br>
226             * If <code>excludes</code> is not empty and matches <code>value</code> return false.<br>
227             * If <code>value</code> has not been explicitly excluded, check the <code>includes</code> list.<br>
228             * If <code>includes</code> is empty return true.<br>
229             * If <code>includes</code> is not empty, return true if, and only if, <code>value</code> matches a pattern from the
230             * <code>includes</code> list.<br>
231             * A single wildcard <code>*</code> is supported for <code>includes</code> and <code>excludes</code>.<br>
232             */
233            public static final boolean include(String value, List<String> includes, List<String> excludes) {
234                    if (isSingleWildcardMatch(value, excludes)) {
235                            // No point incurring the overhead of matching an include pattern
236                            return false;
237                    } else {
238                            // If includes is empty always return true
239                            return CollectionUtils.isEmpty(includes) || isSingleWildcardMatch(value, includes);
240                    }
241            }
242    
243            public static final boolean isSingleWildcardMatch(String s, List<String> patterns) {
244                    for (String pattern : CollectionUtils.toEmptyList(patterns)) {
245                            if (isSingleWildcardMatch(s, pattern)) {
246                                    return true;
247                            }
248                    }
249                    return false;
250            }
251    
252            /**
253             * Match {@code value} against {@code pattern} where {@code pattern} can optionally contain a single wildcard
254             * {@code *}. If both are {@code null} return {@code true}. If one of {@code value} or {@code pattern} is
255             * {@code null} but the other isn't, return {@code false}. Any {@code pattern} containing more than a single
256             * wildcard throws {@code IllegalArgumentException}.
257             * 
258             * <pre>
259             * PropertyUtils.isSingleWildcardMatch(null, null)          = true
260             * PropertyUtils.isSingleWildcardMatch(null, *)             = false
261             * PropertyUtils.isSingleWildcardMatch(*, null)             = false
262             * PropertyUtils.isSingleWildcardMatch(*, "*")              = true
263             * PropertyUtils.isSingleWildcardMatch("abcdef", "bcd")     = false
264             * PropertyUtils.isSingleWildcardMatch("abcdef", "*def")    = true
265             * PropertyUtils.isSingleWildcardMatch("abcdef", "abc*")    = true
266             * PropertyUtils.isSingleWildcardMatch("abcdef", "ab*ef")   = true
267             * PropertyUtils.isSingleWildcardMatch("abcdef", "abc*def") = true
268             * PropertyUtils.isSingleWildcardMatch(*, "**")             = IllegalArgumentException
269             * </pre>
270             */
271            public static final boolean isSingleWildcardMatch(String value, String pattern) {
272                    if (value == null && pattern == null) {
273                            // both are null
274                            return true;
275                    } else if (value != null && pattern == null || value == null && pattern != null) {
276                            // One is null, but not the other
277                            return false;
278                    } else if (pattern.equals(Constants.WILDCARD)) {
279                            // neither one is null and pattern is the wildcard. Value is irrelevant
280                            return true;
281                    } else if (StringUtils.countMatches(pattern, Constants.WILDCARD) > 1) {
282                            // More than one wildcard in the pattern is not supported
283                            throw new IllegalArgumentException("Pattern [" + pattern
284                                            + "] is not supported.  Only one wildcard is allowed in the pattern");
285                    } else if (!StringUtils.contains(pattern, Constants.WILDCARD)) {
286                            // Neither one is null and there is no wildcard in the pattern. They must match exactly
287                            return StringUtils.equals(value, pattern);
288                    } else {
289                            // The pattern contains 1 (and only 1) wildcard
290                            // Make sure value starts with the characters to the left of the wildcard
291                            // and ends with the characters to the right of the wildcard
292                            int pos = StringUtils.indexOf(pattern, Constants.WILDCARD);
293                            int suffixPos = pos + Constants.WILDCARD.length();
294                            boolean nullPrefix = pos == 0;
295                            boolean nullSuffix = suffixPos >= pattern.length();
296                            String prefix = nullPrefix ? null : StringUtils.substring(pattern, 0, pos);
297                            String suffix = nullSuffix ? null : StringUtils.substring(pattern, suffixPos);
298                            boolean prefixMatch = nullPrefix || StringUtils.startsWith(value, prefix);
299                            boolean suffixMatch = nullSuffix || StringUtils.endsWith(value, suffix);
300                            return prefixMatch && suffixMatch;
301                    }
302            }
303    
304            /**
305             * Return property keys that should be included as a sorted list.
306             */
307            public static final Properties getProperties(Properties properties, String include, String exclude) {
308                    List<String> keys = getSortedKeys(properties, include, exclude);
309                    Properties newProperties = new Properties();
310                    for (String key : keys) {
311                            String value = properties.getProperty(key);
312                            newProperties.setProperty(key, value);
313                    }
314                    return newProperties;
315            }
316    
317            /**
318             * Return property keys that should be included as a sorted list.
319             */
320            public static final List<String> getSortedKeys(Properties properties, String include, String exclude) {
321                    return getSortedKeys(properties, CollectionUtils.toEmptyList(include), CollectionUtils.toEmptyList(exclude));
322            }
323    
324            /**
325             * Return property keys that should be included as a sorted list.
326             */
327            public static final List<String> getSortedKeys(Properties properties, List<String> includes, List<String> excludes) {
328                    List<String> keys = getSortedKeys(properties);
329                    List<String> includedKeys = new ArrayList<String>();
330                    for (String key : keys) {
331                            if (include(key, includes, excludes)) {
332                                    includedKeys.add(key);
333                            }
334                    }
335                    return includedKeys;
336            }
337    
338            /**
339             * Return a sorted <code>List</code> of keys from <code>properties</code> that start with <code>prefix</code>
340             */
341            public static final List<String> getStartsWithKeys(Properties properties, String prefix) {
342                    List<String> keys = getSortedKeys(properties);
343                    List<String> matches = new ArrayList<String>();
344                    for (String key : keys) {
345                            if (StringUtils.startsWith(key, prefix)) {
346                                    matches.add(key);
347                            }
348                    }
349                    return matches;
350            }
351    
352            /**
353             * Return the property keys as a sorted list.
354             */
355            public static final List<String> getSortedKeys(Properties properties) {
356                    List<String> keys = new ArrayList<String>(properties.stringPropertyNames());
357                    Collections.sort(keys);
358                    return keys;
359            }
360    
361            public static final String toString(Properties properties) {
362                    List<String> keys = getSortedKeys(properties);
363                    StringBuilder sb = new StringBuilder();
364                    for (String key : keys) {
365                            String value = Str.flatten(properties.getProperty(key));
366                            sb.append(key + "=" + value + "\n");
367                    }
368                    return sb.toString();
369            }
370    
371            public static final void info(Properties properties) {
372                    properties = toEmpty(properties);
373                    logger.info("--- Displaying {} properties ---\n\n{}", properties.size(), toString(properties));
374            }
375    
376            public static final void debug(Properties properties) {
377                    properties = toEmpty(properties);
378                    logger.debug("--- Displaying {} properties ---\n\n{}", properties.size(), toString(properties));
379            }
380    
381            /**
382             * Store the properties to the indicated file using the platform default encoding.
383             */
384            public static final void store(Properties properties, File file) {
385                    store(properties, file, null);
386            }
387    
388            /**
389             * Store the properties to the indicated file using the indicated encoding.
390             */
391            public static final void store(Properties properties, File file, String encoding) {
392                    store(properties, file, encoding, null);
393            }
394    
395            /**
396             * Store the properties to the indicated file using the indicated encoding with the indicated comment appearing at
397             * the top of the file.
398             */
399            public static final void store(Properties properties, File file, String encoding, String comment) {
400                    OutputStream out = null;
401                    Writer writer = null;
402                    try {
403                            out = FileUtils.openOutputStream(file);
404                            String path = file.getCanonicalPath();
405                            boolean xml = isXml(path);
406                            Properties sorted = getSortedProperties(properties);
407                            comment = getComment(encoding, comment, xml);
408                            if (xml) {
409                                    logger.info("Storing XML properties - [{}] encoding={}", path,
410                                                    StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING));
411                                    if (encoding == null) {
412                                            sorted.storeToXML(out, comment);
413                                    } else {
414                                            sorted.storeToXML(out, comment, encoding);
415                                    }
416                            } else {
417                                    writer = LocationUtils.getWriter(out, encoding);
418                                    logger.info("Storing properties - [{}] encoding={}", path,
419                                                    StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING));
420                                    sorted.store(writer, comment);
421                            }
422                    } catch (IOException e) {
423                            throw new IllegalStateException("Unexpected IO error", e);
424                    } finally {
425                            IOUtils.closeQuietly(writer);
426                            IOUtils.closeQuietly(out);
427                    }
428            }
429    
430            /**
431             * Return a new properties object containing the properties from <code>getEnvAsProperties()</code> and
432             * <code>System.getProperties()</code>. Properties from <code>System.getProperties()</code> override properties from
433             * <code>getEnvAsProperties</code> if there are duplicates.
434             */
435            public static final Properties getGlobalProperties() {
436                    return getProperties(Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
437            }
438    
439            /**
440             * Return a new properties object containing the properties passed in, plus any properties returned by
441             * <code>getEnvAsProperties()</code> and <code>System.getProperties()</code>. Properties from
442             * <code>getEnvAsProperties()</code> override <code>properties</code> and properties from
443             * <code>System.getProperties()</code> override everything.
444             */
445            public static final Properties getGlobalProperties(Properties properties) {
446                    return getProperties(properties, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
447            }
448    
449            /**
450             * Return a new properties object containing the properties passed in, plus any global properties as requested. If
451             * <code>mode</code> is <code>NONE</code> the new properties are a duplicate of the properties passed in. If
452             * <code>mode</code> is <code>ENVIRONMENT</code> the new properties contain the original properties plus any
453             * properties returned by <code>getEnvProperties()</code>. If <code>mode</code> is <code>SYSTEM</code> the new
454             * properties contain the original properties plus <code>System.getProperties()</code>. If <code>mode</code> is
455             * <code>BOTH</code> the new properties contain the original properties plus <code>getEnvProperties()</code> and
456             * <code>System.getProperties()</code>.
457             */
458            public static final Properties getProperties(Properties properties, GlobalPropertiesMode mode) {
459                    Properties newProperties = duplicate(properties);
460                    List<PropertyProcessor> modifiers = getPropertyProcessors(mode);
461                    for (PropertyProcessor modifier : modifiers) {
462                            modifier.process(newProperties);
463                    }
464                    return newProperties;
465            }
466    
467            /**
468             * Return a new properties object containing global properties as requested. If <code>mode</code> is
469             * <code>NONE</code> the new properties are empty. If <code>mode</code> is <code>ENVIRONMENT</code> the new
470             * properties contain the properties returned by <code>getEnvProperties()</code>. If <code>mode</code> is
471             * <code>SYSTEM</code> the new properties contain <code>System.getProperties()</code>. If <code>mode</code> is
472             * <code>BOTH</code> the new properties contain <code>getEnvProperties</code> plus
473             * <code>System.getProperties()</code> with system properties overriding environment variables if the same case
474             * sensitive property key is supplied in both places.
475             */
476            public static final Properties getProperties(GlobalPropertiesMode mode) {
477                    return getProperties(new Properties(), mode);
478            }
479    
480            /**
481             * Search global properties to find a value for <code>key</code> according to the mode passed in.
482             */
483            public static final String getProperty(String key, GlobalPropertiesMode mode) {
484                    return getProperty(key, new Properties(), mode);
485            }
486    
487            /**
488             * Search <code>properties</code> plus global properties to find a value for <code>key</code> according to the mode
489             * passed in. If the property is present in both, the value from the global properties is returned.
490             */
491            public static final String getProperty(String key, Properties properties, GlobalPropertiesMode mode) {
492                    return getProperties(properties, mode).getProperty(key);
493            }
494    
495            /**
496             * Return modifiers that add environment variables, system properties, or both, according to the mode passed in.
497             */
498            public static final List<PropertyProcessor> getPropertyProcessors(GlobalPropertiesMode mode) {
499                    List<PropertyProcessor> processors = new ArrayList<PropertyProcessor>();
500                    switch (mode) {
501                    case NONE:
502                            return processors;
503                    case ENVIRONMENT:
504                            processors.add(new AddPropertiesProcessor(getEnvAsProperties()));
505                            return processors;
506                    case SYSTEM:
507                            processors.add(new AddPropertiesProcessor(System.getProperties()));
508                            return processors;
509                    case BOTH:
510                            processors.add(new AddPropertiesProcessor(getEnvAsProperties()));
511                            processors.add(new AddPropertiesProcessor(System.getProperties()));
512                            return processors;
513                    default:
514                            throw new IllegalStateException(mode + " is unknown");
515                    }
516            }
517    
518            /**
519             * Convert the <code>Map</code> to a <code>Properties</code> object.
520             */
521            public static final Properties convert(Map<String, String> map) {
522                    Properties props = new Properties();
523                    for (String key : map.keySet()) {
524                            String value = map.get(key);
525                            props.setProperty(key, value);
526                    }
527                    return props;
528            }
529    
530            /**
531             * Return a new properties object that duplicates the properties passed in.
532             */
533            public static final Properties duplicate(Properties properties) {
534                    Properties newProperties = new Properties();
535                    newProperties.putAll(properties);
536                    return newProperties;
537            }
538    
539            /**
540             * Return a new properties object containing environment variables as properties prefixed with <code>env</code>
541             */
542            public static Properties getEnvAsProperties() {
543                    return getEnvAsProperties(ENV_PREFIX);
544            }
545    
546            /**
547             * Return a new properties object containing environment variables as properties prefixed with <code>prefix</code>
548             */
549            public static Properties getEnvAsProperties(String prefix) {
550                    Properties properties = convert(System.getenv());
551                    return getPrefixedProperties(properties, prefix);
552            }
553    
554            /**
555             * Return true if, and only if, location ends with <code>.xml</code> (case insensitive).
556             */
557            public static final boolean isXml(String location) {
558                    return StringUtils.endsWithIgnoreCase(location, XML_EXTENSION);
559            }
560    
561            /**
562             * Return a new <code>Properties</code> object loaded from <code>file</code>.
563             */
564            public static final Properties load(File file) {
565                    return load(file, null);
566            }
567    
568            /**
569             * Return a new <code>Properties</code> object loaded from <code>file</code> using the given encoding.
570             */
571            public static final Properties load(File file, String encoding) {
572                    String location = LocationUtils.getCanonicalPath(file);
573                    return load(location, encoding);
574            }
575    
576            /**
577             * Return a new <code>Properties</code> object loaded from <code>location</code>.
578             */
579            public static final Properties load(String location) {
580                    return load(location, null);
581            }
582    
583            /**
584             * Return a new <code>Properties</code> object loaded from <code>location</code> using <code>encoding</code>.
585             */
586            public static final Properties load(String location, String encoding) {
587                    InputStream in = null;
588                    Reader reader = null;
589                    try {
590                            Properties properties = new Properties();
591                            boolean xml = isXml(location);
592                            location = getCanonicalLocation(location);
593                            if (xml) {
594                                    in = LocationUtils.getInputStream(location);
595                                    logger.info("Loading XML properties - [{}]", location);
596                                    properties.loadFromXML(in);
597                            } else {
598                                    logger.info("Loading properties - [{}] encoding={}", location,
599                                                    StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING));
600                                    reader = LocationUtils.getBufferedReader(location, encoding);
601                                    properties.load(reader);
602                            }
603                            return properties;
604                    } catch (IOException e) {
605                            throw new IllegalStateException("Unexpected IO error", e);
606                    } finally {
607                            IOUtils.closeQuietly(in);
608                            IOUtils.closeQuietly(reader);
609                    }
610            }
611    
612            protected static String getCanonicalLocation(String location) {
613                    if (LocationUtils.isExistingFile(location)) {
614                            return LocationUtils.getCanonicalPath(new File(location));
615                    } else {
616                            return location;
617                    }
618            }
619    
620            /**
621             * Return a new <code>Properties</code> object containing properties prefixed with <code>prefix</code>. If
622             * <code>prefix</code> is blank, the new properties object duplicates the properties passed in.
623             */
624            public static final Properties getPrefixedProperties(Properties properties, String prefix) {
625                    if (StringUtils.isBlank(prefix)) {
626                            return duplicate(properties);
627                    }
628                    Properties newProperties = new Properties();
629                    for (String key : properties.stringPropertyNames()) {
630                            String value = properties.getProperty(key);
631                            String newKey = StringUtils.startsWith(key, prefix + ".") ? key : prefix + "." + key;
632                            newProperties.setProperty(newKey, value);
633                    }
634                    return newProperties;
635            }
636    
637            /**
638             * Return a new properties object where the keys have been converted to upper case and periods have been replaced
639             * with an underscore.
640             */
641            public static final Properties reformatKeysAsEnvVars(Properties properties) {
642                    Properties newProperties = new Properties();
643                    for (String key : properties.stringPropertyNames()) {
644                            String value = properties.getProperty(key);
645                            String newKey = StringUtils.upperCase(StringUtils.replace(key, ".", "-"));
646                            newProperties.setProperty(newKey, value);
647                    }
648                    return newProperties;
649            }
650    
651            /**
652             * Before setting the newValue, check to see if there is a conflict with an existing value. If there is no existing
653             * value, add the property. If there is a conflict, check <code>propertyOverwriteMode</code> to make sure we have
654             * permission to override the value.
655             */
656            public static final void addOrOverrideProperty(Properties properties, String key, String newValue,
657                            Mode propertyOverwriteMode) {
658                    String oldValue = properties.getProperty(key);
659                    if (StringUtils.equals(newValue, oldValue)) {
660                            // Nothing to do! New value is the same as old value.
661                            return;
662                    }
663                    boolean overwrite = !StringUtils.isBlank(oldValue);
664    
665                    // TODO Yuck! Do something smarter here
666                    String logNewValue = newValue;
667                    String logOldValue = oldValue;
668                    if (obscure(key)) {
669                            logNewValue = "PROTECTED";
670                            logOldValue = "PROTECTED";
671                    }
672    
673                    if (overwrite) {
674                            // This property already has a value, and it is different from the new value
675                            // Check to make sure we are allowed to override the old value before doing so
676                            Object[] args = new Object[] { key, Str.flatten(logNewValue), Str.flatten(logOldValue) };
677                            ModeUtils.validate(propertyOverwriteMode, "Overriding [{}={}] was [{}]", args,
678                                            "Override of existing property [" + key + "] is not allowed.");
679                    } else {
680                            // There is no existing value for this key
681                            logger.info("Adding [{}={}]", key, Str.flatten(logNewValue));
682                    }
683                    properties.setProperty(key, newValue);
684            }
685    
686            protected static boolean obscure(String key) {
687                    if (StringUtils.containsIgnoreCase(key, ".password")) {
688                            return true;
689                    }
690                    if (StringUtils.containsIgnoreCase(key, ".secret")) {
691                            return true;
692                    }
693                    if (StringUtils.containsIgnoreCase(key, ".private")) {
694                            return true;
695                    }
696                    return false;
697            }
698    
699            private static final String getDefaultComment(String encoding, boolean xml) {
700                    if (encoding == null) {
701                            if (xml) {
702                                    // Java defaults XML properties files to UTF-8 if no encoding is provided
703                                    return "encoding.default=" + DEFAULT_XML_ENCODING;
704                            } else {
705                                    // For normal properties files the platform default encoding is used
706                                    return "encoding.default=" + DEFAULT_ENCODING;
707                            }
708                    } else {
709                            return "encoding.specified=" + encoding;
710                    }
711            }
712    
713            private static final String getComment(String encoding, String comment, boolean xml) {
714                    if (StringUtils.isBlank(comment)) {
715                            return getDefaultComment(encoding, xml);
716                    } else {
717                            return comment + "\n#" + getDefaultComment(encoding, xml);
718                    }
719            }
720    
721            /**
722             * This is private because <code>SortedProperties</code> does not fully honor the contract for
723             * <code>Properties</code>
724             */
725            private static final SortedProperties getSortedProperties(Properties properties) {
726                    SortedProperties sp = new PropertyUtils().new SortedProperties();
727                    sp.putAll(properties);
728                    return sp;
729            }
730    
731            /**
732             * This is private since it does not honor the full contract for <code>Properties</code>. <code>PropertyUtils</code>
733             * uses it internally to store properties in sorted order.
734             */
735            private class SortedProperties extends Properties {
736    
737                    private static final long serialVersionUID = 1330825236411537386L;
738    
739                    /**
740                     * <code>Properties.storeToXML()</code> uses <code>keySet()</code>
741                     */
742                    @Override
743                    public Set<Object> keySet() {
744                            return Collections.unmodifiableSet(new TreeSet<Object>(super.keySet()));
745                    }
746    
747                    /**
748                     * <code>Properties.store()</code> uses <code>keys()</code>
749                     */
750                    @Override
751                    public synchronized Enumeration<Object> keys() {
752                            return Collections.enumeration(new TreeSet<Object>(super.keySet()));
753                    }
754            }
755    
756            /**
757             * Set properties in the given Properties to CSV versions of the lists in the ComparisonResults
758             * 
759             * @param properties
760             *            the Properties to populate
761             * @param listComparison
762             *            the ComparisonResults to use for data
763             * @param propertyNames
764             *            the list of property keys to set. Exactly 3 names are required, and the assumed order is: index 0: key
765             *            for the ADDED list index 1: key for the SAME list index 2: key for the DELETED list
766             */
767            public static final void addListComparisonProperties(Properties properties, ComparisonResults listComparison,
768                            List<String> propertyNames) {
769                    // make sure that there are three names in the list of property names
770                    Assert.isTrue(propertyNames.size() == 3);
771    
772                    properties.setProperty(propertyNames.get(0), CollectionUtils.getCSV(listComparison.getAdded()));
773                    properties.setProperty(propertyNames.get(1), CollectionUtils.getCSV(listComparison.getSame()));
774                    properties.setProperty(propertyNames.get(2), CollectionUtils.getCSV(listComparison.getDeleted()));
775            }
776    
777    }