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