001    /**
002     * Copyright 2010-2013 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.ProjectProperties;
042    import org.kuali.common.util.property.PropertiesContext;
043    import org.kuali.common.util.property.PropertyFormat;
044    import org.kuali.common.util.property.processor.AddPropertiesProcessor;
045    import org.kuali.common.util.property.processor.PropertyProcessor;
046    import org.kuali.common.util.property.processor.ResolvePlaceholdersProcessor;
047    import org.slf4j.Logger;
048    import org.slf4j.LoggerFactory;
049    import org.springframework.util.Assert;
050    import org.springframework.util.PropertyPlaceholderHelper;
051    
052    /**
053     * 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
054     * understand. When storing and loading, locations ending in <code>.xml</code> are automatically handled using <code>storeToXML()</code> and <code>loadFromXML()</code>,
055     * respectively. <code>Properties</code> are always stored in sorted order with the <code>encoding</code> indicated via a comment.
056     */
057    public class PropertyUtils {
058    
059            private static final Logger logger = LoggerFactory.getLogger(PropertyUtils.class);
060    
061            public static final String ADDITIONAL_LOCATIONS = "properties.additional.locations";
062            public static final String ADDITIONAL_LOCATIONS_ENCODING = ADDITIONAL_LOCATIONS + ".encoding";
063    
064            private static final String XML_EXTENSION = ".xml";
065            private static final PropertyPlaceholderHelper HELPER = new PropertyPlaceholderHelper("${", "}", ":", false);
066            private static final String ENV_PREFIX = "env";
067            private static final String DEFAULT_ENCODING = Charset.defaultCharset().name();
068            private static final String DEFAULT_XML_ENCODING = "UTF-8";
069    
070            public static String getRiceXML(Properties properties) {
071                    StringBuilder sb = new StringBuilder();
072                    sb.append("<config>\n");
073                    List<String> keys = getSortedKeys(properties);
074                    for (String key : keys) {
075                            String value = properties.getProperty(key);
076                            // Convert to CDATA if the value contains characters that would blow up an XML parser
077                            if (StringUtils.contains(value, "<") || StringUtils.contains(value, "&")) {
078                                    value = Str.cdata(value);
079                            }
080                            sb.append("  <param name=" + Str.quote(key) + ">");
081                            sb.append(value);
082                            sb.append("</param>\n");
083                    }
084                    sb.append("</config>\n");
085                    return sb.toString();
086            }
087    
088            public static String getRequiredResolvedProperty(Properties properties, String key) {
089                    return getRequiredResolvedProperty(properties, key, null);
090            }
091    
092            public static String getRequiredResolvedProperty(Properties properties, String key, String defaultValue) {
093                    String value = properties.getProperty(key);
094                    value = StringUtils.isBlank(value) ? defaultValue : value;
095                    if (StringUtils.isBlank(value)) {
096                            throw new IllegalArgumentException("[" + key + "] is not set");
097                    } else {
098                            return HELPER.replacePlaceholders(value, properties);
099                    }
100            }
101    
102            /**
103             * Process the properties passed in so they are ready for use by a Spring context.<br>
104             * 
105             * 1 - Override with system/environment properties<br>
106             * 2 - Decrypt any ENC(...) values<br>
107             * 3 - Resolve all property values throwing an exception if any are unresolvable.<br>
108             */
109            public static void prepareContextProperties(Properties properties, String encoding) {
110    
111                    // Override with additional properties (if any)
112                    properties.putAll(getAdditionalProperties(properties, encoding));
113    
114                    // Override with system/environment properties
115                    properties.putAll(getGlobalProperties());
116    
117                    // Are we decrypting property values?
118                    decrypt(properties);
119    
120                    // Are we resolving placeholders
121                    resolve(properties);
122            }
123    
124            /**
125             * Process the properties passed in so they are ready for use by a Spring context.<br>
126             * 
127             * 1 - Override with system/environment properties<br>
128             * 2 - Decrypt any ENC(...) values<br>
129             * 3 - Resolve all property values throwing an exception if any are unresolvable.<br>
130             */
131            public static void prepareContextProperties(Properties properties) {
132                    prepareContextProperties(properties, null);
133            }
134    
135            public static void resolve(Properties properties) {
136                    // Are we resolving placeholders?
137                    boolean resolve = new Boolean(getRequiredResolvedProperty(properties, "properties.resolve", "true"));
138                    if (resolve) {
139                            ResolvePlaceholdersProcessor rpp = new ResolvePlaceholdersProcessor();
140                            rpp.setHelper(HELPER);
141                            rpp.process(properties);
142                    }
143            }
144    
145            public static void decrypt(Properties properties) {
146                    // Are we decrypting property values?
147                    boolean decrypt = Boolean.parseBoolean(getRequiredResolvedProperty(properties, "properties.decrypt", "false"));
148                    if (decrypt) {
149                            // If they asked to decrypt, a password is required
150                            String password = getRequiredResolvedProperty(properties, "properties.enc.password");
151    
152                            // Strength is optional (defaults to BASIC)
153                            String defaultStrength = EncryptionStrength.BASIC.name();
154                            String strength = getRequiredResolvedProperty(properties, "properties.enc.strength", defaultStrength);
155                            EncryptionStrength es = EncryptionStrength.valueOf(strength);
156                            TextEncryptor decryptor = EncUtils.getTextEncryptor(es, password);
157                            PropertyUtils.decrypt(properties, decryptor);
158                    }
159            }
160    
161            public static Properties getAdditionalProperties(Properties properties) {
162                    return getAdditionalProperties(properties, null);
163            }
164    
165            public static Properties getAdditionalProperties(Properties properties, String encoding) {
166                    String csv = properties.getProperty(ADDITIONAL_LOCATIONS);
167                    if (StringUtils.isBlank(csv)) {
168                            return new Properties();
169                    }
170                    if (StringUtils.isBlank(encoding)) {
171                            encoding = properties.getProperty(ADDITIONAL_LOCATIONS_ENCODING, DEFAULT_XML_ENCODING);
172                    }
173                    List<String> locations = CollectionUtils.getTrimmedListFromCSV(csv);
174                    PropertiesContext context = new PropertiesContext(locations, encoding);
175                    return load(context);
176            }
177    
178            public static void appendToOrSetProperty(Properties properties, String key, String value) {
179                    Assert.hasText(value);
180                    String existingValue = properties.getProperty(key);
181                    if (existingValue == null) {
182                            existingValue = "";
183                    }
184                    String newValue = existingValue + value;
185                    properties.setProperty(key, newValue);
186            }
187    
188            public static Properties load(List<ProjectProperties> pps) {
189    
190                    // Create some storage for the Properties object we will be returning
191                    Properties properties = new Properties();
192    
193                    // Cycle through the list of project properties, loading them as we go
194                    for (ProjectProperties pp : pps) {
195    
196                            logger.debug("oracle.dba.url.1={}", properties.getProperty("oracle.dba.url"));
197    
198                            // Extract the properties context object
199                            PropertiesContext ctx = pp.getPropertiesContext();
200    
201                            // Retain the original properties object from the context
202                            Properties original = PropertyUtils.duplicate(PropertyUtils.toEmpty(ctx.getProperties()));
203    
204                            // Override any existing property values with properties stored directly on the context
205                            Properties combined = PropertyUtils.combine(properties, ctx.getProperties());
206    
207                            // Store the combined properties on the context itself
208                            ctx.setProperties(combined);
209    
210                            // Load properties as dictated by the context
211                            Properties loaded = load(ctx);
212    
213                            logger.debug("oracle.dba.url.2={}", loaded.getProperty("oracle.dba.url"));
214    
215                            // Override any existing property values with those we just loaded
216                            properties.putAll(loaded);
217    
218                            // Override any existing property values with the properties that were stored directly on the context
219                            properties.putAll(original);
220    
221                    }
222    
223                    // Return the property values we now have
224                    return properties;
225            }
226    
227            public static Properties load(PropertiesContext context) {
228                    // If there are no locations specified, add the properties supplied directly on the context (if there are any)
229                    if (CollectionUtils.isEmpty(context.getLocations())) {
230                            return PropertyUtils.toEmpty(context.getProperties());
231                    }
232    
233                    // Make sure we are configured correctly
234                    Assert.notNull(context.getHelper(), "helper is null");
235                    Assert.notNull(context.getLocations(), "locations are null");
236                    Assert.notNull(context.getEncoding(), "encoding is null");
237                    Assert.notNull(context.getMissingLocationsMode(), "missingLocationsMode is null");
238    
239                    // Get system/environment properties
240                    Properties global = PropertyUtils.getGlobalProperties();
241    
242                    // Convert null to an empty properties object (if necessary)
243                    context.setProperties(PropertyUtils.toEmpty(context.getProperties()));
244    
245                    // Create new storage for the properties we are loading
246                    Properties result = new Properties();
247    
248                    // Add in any properties stored directly on the context itself (these get overridden by properties loaded elsewhere)
249                    result.putAll(context.getProperties());
250    
251                    // Cycle through the locations, loading and storing properties as we go
252                    for (String location : context.getLocations()) {
253    
254                            // Get a combined Properties object capable of resolving any placeholders that exist in the property location strings
255                            Properties resolverProperties = PropertyUtils.combine(context.getProperties(), result, global);
256    
257                            // Make sure we have a fully resolved location to load Properties from
258                            String resolvedLocation = context.getHelper().replacePlaceholders(location, resolverProperties);
259    
260                            // If the location exists, load properties from it
261                            if (LocationUtils.exists(resolvedLocation)) {
262    
263                                    // Load this set of Properties
264                                    Properties properties = PropertyUtils.load(resolvedLocation, context.getEncoding());
265    
266                                    // Add these properties to the result. This follows the traditional "last one in wins" strategy
267                                    result.putAll(properties);
268                            } else {
269    
270                                    // Handle missing locations (might be fine, may need to emit a logging statement, may need to error out)
271                                    ModeUtils.validate(context.getMissingLocationsMode(), "Non-existent location [" + resolvedLocation + "]");
272                            }
273                    }
274    
275                    // Return the properties we loaded
276                    return result;
277            }
278    
279            /**
280             * Decrypt any encrypted property values. Encrypted values are surrounded by ENC(...), like:
281             * 
282             * <pre>
283             * my.value = ENC(DGA"$S24FaIO)
284             * </pre>
285             */
286            public static void decrypt(Properties properties, TextEncryptor encryptor) {
287                    decrypt(properties, encryptor, null, null);
288            }
289    
290            /**
291             * Return a new <code>Properties</code> object (never null) containing only those properties whose values are encrypted. Encrypted values are surrounded by ENC(...), like:
292             * 
293             * <pre>
294             * my.value = ENC(DGA"$S24FaIO)
295             * </pre>
296             */
297            public static Properties getEncryptedProperties(Properties properties) {
298                    List<String> keys = getSortedKeys(properties);
299                    Properties encrypted = new Properties();
300                    for (String key : keys) {
301                            String value = properties.getProperty(key);
302                            if (isEncryptedPropertyValue(value)) {
303                                    encrypted.setProperty(key, value);
304                            }
305                    }
306                    return encrypted;
307            }
308    
309            /**
310             * Decrypt any encrypted property values matching the <code>includes</code>, <code>excludes</code> patterns. Encrypted values are surrounded by ENC(...).
311             * 
312             * <pre>
313             * my.value = ENC(DGA"$S24FaIO)
314             * </pre>
315             */
316            public static void decrypt(Properties properties, TextEncryptor encryptor, List<String> includes, List<String> excludes) {
317                    List<String> keys = getSortedKeys(properties, includes, excludes);
318                    for (String key : keys) {
319                            String value = properties.getProperty(key);
320                            if (isEncryptedPropertyValue(value)) {
321                                    String decryptedValue = decryptPropertyValue(encryptor, value);
322                                    properties.setProperty(key, decryptedValue);
323                            }
324                    }
325            }
326    
327            /**
328             * Return true if the value starts with <code>ENC(</code> and ends with <code>)</code>, false otherwise.
329             */
330            public static boolean isEncryptedPropertyValue(String value) {
331                    return StringUtils.startsWith(value, Constants.ENCRYPTION_PREFIX) && StringUtils.endsWith(value, Constants.ENCRYPTION_SUFFIX);
332            }
333    
334            /**
335             * Encrypt all of the property values. Encrypted values are surrounded by ENC(...).
336             * 
337             * <pre>
338             * my.value = ENC(DGA"$S24FaIO)
339             * </pre>
340             */
341            public static void encrypt(Properties properties, TextEncryptor encryptor) {
342                    encrypt(properties, encryptor, null, null);
343            }
344    
345            /**
346             * Encrypt properties as dictated by <code>includes</code> and <code>excludes</code>. Encrypted values are surrounded by ENC(...).
347             * 
348             * <pre>
349             * my.value = ENC(DGA"$S24FaIO)
350             * </pre>
351             */
352            public static void encrypt(Properties properties, TextEncryptor encryptor, List<String> includes, List<String> excludes) {
353                    List<String> keys = getSortedKeys(properties, includes, excludes);
354                    for (String key : keys) {
355                            String originalValue = properties.getProperty(key);
356                            String encryptedValue = encryptPropertyValue(encryptor, originalValue);
357                            properties.setProperty(key, encryptedValue);
358                    }
359            }
360    
361            /**
362             * Return the decrypted version of the property value. Encrypted values are surrounded by ENC(...).
363             * 
364             * <pre>
365             * my.value = ENC(DGA"$S24FaIO)
366             * </pre>
367             */
368            public static String decryptPropertyValue(TextEncryptor encryptor, String value) {
369                    // Ensure this property value really is encrypted
370                    Assert.isTrue(StringUtils.startsWith(value, Constants.ENCRYPTION_PREFIX), "value does not start with " + Constants.ENCRYPTION_PREFIX);
371                    Assert.isTrue(StringUtils.endsWith(value, Constants.ENCRYPTION_SUFFIX), "value does not end with " + Constants.ENCRYPTION_SUFFIX);
372    
373                    // Extract the value inside the ENC(...) wrapping
374                    int start = Constants.ENCRYPTION_PREFIX.length();
375                    int end = StringUtils.length(value) - Constants.ENCRYPTION_SUFFIX.length();
376                    String unwrapped = StringUtils.substring(value, start, end);
377    
378                    // Return the decrypted value
379                    return encryptor.decrypt(unwrapped);
380            }
381    
382            /**
383             * Return the encrypted version of the property value. A value is considered "encrypted" when it appears surrounded by ENC(...).
384             * 
385             * <pre>
386             * my.value = ENC(DGA"$S24FaIO)
387             * </pre>
388             */
389            public static String encryptPropertyValue(TextEncryptor encryptor, String value) {
390                    String encryptedValue = encryptor.encrypt(value);
391                    StringBuilder sb = new StringBuilder();
392                    sb.append(Constants.ENCRYPTION_PREFIX);
393                    sb.append(encryptedValue);
394                    sb.append(Constants.ENCRYPTION_SUFFIX);
395                    return sb.toString();
396            }
397    
398            public static void overrideWithGlobalValues(Properties properties, GlobalPropertiesMode mode) {
399                    List<String> keys = PropertyUtils.getSortedKeys(properties);
400                    Properties global = PropertyUtils.getProperties(mode);
401                    for (String key : keys) {
402                            String globalValue = global.getProperty(key);
403                            if (!StringUtils.isBlank(globalValue)) {
404                                    properties.setProperty(key, globalValue);
405                            }
406                    }
407            }
408    
409            public static final Properties combine(List<Properties> properties) {
410                    Properties combined = new Properties();
411                    for (Properties p : properties) {
412                            combined.putAll(PropertyUtils.toEmpty(p));
413                    }
414                    return combined;
415            }
416    
417            public static final Properties combine(Properties... properties) {
418                    return combine(Arrays.asList(properties));
419            }
420    
421            public static final void process(Properties properties, PropertyProcessor processor) {
422                    process(properties, Collections.singletonList(processor));
423            }
424    
425            public static final void process(Properties properties, List<PropertyProcessor> processors) {
426                    for (PropertyProcessor processor : CollectionUtils.toEmptyList(processors)) {
427                            processor.process(properties);
428                    }
429            }
430    
431            public static final Properties toEmpty(Properties properties) {
432                    return properties == null ? new Properties() : properties;
433            }
434    
435            public static final boolean isSingleUnresolvedPlaceholder(String string) {
436                    return isSingleUnresolvedPlaceholder(string, Constants.DEFAULT_PLACEHOLDER_PREFIX, Constants.DEFAULT_PLACEHOLDER_SUFFIX);
437            }
438    
439            public static final boolean isSingleUnresolvedPlaceholder(String string, String prefix, String suffix) {
440                    int prefixMatches = StringUtils.countMatches(string, prefix);
441                    int suffixMatches = StringUtils.countMatches(string, suffix);
442                    boolean startsWith = StringUtils.startsWith(string, prefix);
443                    boolean endsWith = StringUtils.endsWith(string, suffix);
444                    return prefixMatches == 1 && suffixMatches == 1 && startsWith && endsWith;
445            }
446    
447            public static final boolean containsUnresolvedPlaceholder(String string) {
448                    return containsUnresolvedPlaceholder(string, Constants.DEFAULT_PLACEHOLDER_PREFIX, Constants.DEFAULT_PLACEHOLDER_SUFFIX);
449            }
450    
451            public static final boolean containsUnresolvedPlaceholder(String string, String prefix, String suffix) {
452                    int beginIndex = StringUtils.indexOf(string, prefix);
453                    if (beginIndex == -1) {
454                            return false;
455                    }
456                    return StringUtils.indexOf(string, suffix) != -1;
457            }
458    
459            /**
460             * 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
461             * perform property resolution as indicated by <code>Constants.DEFAULT_GLOBAL_PROPERTIES_MODE</code>
462             */
463            public static final Properties getResolvedProperties(Properties properties) {
464                    return getResolvedProperties(properties, Constants.DEFAULT_PROPERTY_PLACEHOLDER_HELPER, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
465            }
466    
467            /**
468             * 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
469             * perform property resolution as indicated by <code>globalPropertiesMode</code>
470             */
471            public static final Properties getResolvedProperties(Properties properties, GlobalPropertiesMode globalPropertiesMode) {
472                    return getResolvedProperties(properties, Constants.DEFAULT_PROPERTY_PLACEHOLDER_HELPER, globalPropertiesMode);
473            }
474    
475            /**
476             * 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
477             * perform property resolution as indicated by <code>Constants.DEFAULT_GLOBAL_PROPERTIES_MODE</code>
478             */
479            public static final Properties getResolvedProperties(Properties properties, PropertyPlaceholderHelper helper) {
480                    return getResolvedProperties(properties, helper, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
481            }
482    
483            /**
484             * 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
485             * perform property resolution as indicated by <code>globalPropertiesMode</code>
486             */
487            public static final Properties getResolvedProperties(Properties properties, PropertyPlaceholderHelper helper, GlobalPropertiesMode globalPropertiesMode) {
488                    Properties global = PropertyUtils.getProperties(properties, globalPropertiesMode);
489                    List<String> keys = PropertyUtils.getSortedKeys(properties);
490                    Properties newProperties = new Properties();
491                    for (String key : keys) {
492                            String originalValue = properties.getProperty(key);
493                            String resolvedValue = helper.replacePlaceholders(originalValue, global);
494                            if (!resolvedValue.equals(originalValue)) {
495                                    logger.debug("Resolved property '" + key + "' [{}] -> [{}]", Str.flatten(originalValue), Str.flatten(resolvedValue));
496                                    newProperties.setProperty(key, resolvedValue);
497                            }
498                    }
499                    return newProperties;
500            }
501    
502            /**
503             * Return the property values from <code>keys</code>
504             */
505            public static final List<String> getValues(Properties properties, List<String> keys) {
506                    List<String> values = new ArrayList<String>();
507                    for (String key : keys) {
508                            values.add(properties.getProperty(key));
509                    }
510                    return values;
511            }
512    
513            /**
514             * Return a sorted <code>List</code> of keys from <code>properties</code> that end with <code>suffix</code>.
515             */
516            public static final List<String> getEndsWithKeys(Properties properties, String suffix) {
517                    List<String> keys = getSortedKeys(properties);
518                    List<String> matches = new ArrayList<String>();
519                    for (String key : keys) {
520                            if (StringUtils.endsWith(key, suffix)) {
521                                    matches.add(key);
522                            }
523                    }
524                    return matches;
525            }
526    
527            /**
528             * 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.
529             */
530            public static final void trim(Properties properties, String includesCSV, String excludesCSV) {
531                    List<String> includes = CollectionUtils.getTrimmedListFromCSV(includesCSV);
532                    List<String> excludes = CollectionUtils.getTrimmedListFromCSV(excludesCSV);
533                    trim(properties, includes, excludes);
534            }
535    
536            /**
537             * Alter the <code>properties</code> passed in to contain only the desired property values.
538             */
539            public static final void trim(Properties properties, List<String> includes, List<String> excludes) {
540                    List<String> keys = getSortedKeys(properties);
541                    for (String key : keys) {
542                            if (!include(key, includes, excludes)) {
543                                    logger.debug("Removing [{}]", key);
544                                    properties.remove(key);
545                            }
546                    }
547            }
548    
549            /**
550             * Return true if <code>value</code> should be included, false otherwise.<br>
551             * If <code>excludes</code> is not empty and matches <code>value</code> return false.<br>
552             * If <code>value</code> has not been explicitly excluded, check the <code>includes</code> list.<br>
553             * If <code>includes</code> is empty return true.<br>
554             * 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>
555             * A single wildcard <code>*</code> is supported for <code>includes</code> and <code>excludes</code>.<br>
556             */
557            public static final boolean include(String value, List<String> includes, List<String> excludes) {
558                    if (isSingleWildcardMatch(value, excludes)) {
559                            // No point incurring the overhead of matching an include pattern
560                            return false;
561                    } else {
562                            // If includes is empty always return true
563                            return CollectionUtils.isEmpty(includes) || isSingleWildcardMatch(value, includes);
564                    }
565            }
566    
567            public static final boolean isSingleWildcardMatch(String s, List<String> patterns) {
568                    for (String pattern : CollectionUtils.toEmptyList(patterns)) {
569                            if (isSingleWildcardMatch(s, pattern)) {
570                                    return true;
571                            }
572                    }
573                    return false;
574            }
575    
576            /**
577             * 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
578             * {@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
579             * {@code IllegalArgumentException}.
580             * 
581             * <pre>
582             * PropertyUtils.isSingleWildcardMatch(null, null)          = true
583             * PropertyUtils.isSingleWildcardMatch(null, *)             = false
584             * PropertyUtils.isSingleWildcardMatch(*, null)             = false
585             * PropertyUtils.isSingleWildcardMatch(*, "*")              = true
586             * PropertyUtils.isSingleWildcardMatch("abcdef", "bcd")     = false
587             * PropertyUtils.isSingleWildcardMatch("abcdef", "*def")    = true
588             * PropertyUtils.isSingleWildcardMatch("abcdef", "abc*")    = true
589             * PropertyUtils.isSingleWildcardMatch("abcdef", "ab*ef")   = true
590             * PropertyUtils.isSingleWildcardMatch("abcdef", "abc*def") = true
591             * PropertyUtils.isSingleWildcardMatch(*, "**")             = IllegalArgumentException
592             * </pre>
593             */
594            public static final boolean isSingleWildcardMatch(String value, String pattern) {
595                    if (value == null && pattern == null) {
596                            // both are null
597                            return true;
598                    } else if (value != null && pattern == null || value == null && pattern != null) {
599                            // One is null, but not the other
600                            return false;
601                    } else if (pattern.equals(Constants.WILDCARD)) {
602                            // neither one is null and pattern is the wildcard. Value is irrelevant
603                            return true;
604                    } else if (StringUtils.countMatches(pattern, Constants.WILDCARD) > 1) {
605                            // More than one wildcard in the pattern is not supported
606                            throw new IllegalArgumentException("Pattern [" + pattern + "] is not supported.  Only one wildcard is allowed in the pattern");
607                    } else if (!StringUtils.contains(pattern, Constants.WILDCARD)) {
608                            // Neither one is null and there is no wildcard in the pattern. They must match exactly
609                            return StringUtils.equals(value, pattern);
610                    } else {
611                            // The pattern contains 1 (and only 1) wildcard
612                            // Make sure value starts with the characters to the left of the wildcard
613                            // and ends with the characters to the right of the wildcard
614                            int pos = StringUtils.indexOf(pattern, Constants.WILDCARD);
615                            int suffixPos = pos + Constants.WILDCARD.length();
616                            boolean nullPrefix = pos == 0;
617                            boolean nullSuffix = suffixPos >= pattern.length();
618                            String prefix = nullPrefix ? null : StringUtils.substring(pattern, 0, pos);
619                            String suffix = nullSuffix ? null : StringUtils.substring(pattern, suffixPos);
620                            boolean prefixMatch = nullPrefix || StringUtils.startsWith(value, prefix);
621                            boolean suffixMatch = nullSuffix || StringUtils.endsWith(value, suffix);
622                            return prefixMatch && suffixMatch;
623                    }
624            }
625    
626            /**
627             * Return property keys that should be included as a sorted list.
628             */
629            public static final Properties getProperties(Properties properties, String include, String exclude) {
630                    List<String> keys = getSortedKeys(properties, include, exclude);
631                    Properties newProperties = new Properties();
632                    for (String key : keys) {
633                            String value = properties.getProperty(key);
634                            newProperties.setProperty(key, value);
635                    }
636                    return newProperties;
637            }
638    
639            /**
640             * Return property keys that should be included as a sorted list.
641             */
642            public static final List<String> getSortedKeys(Properties properties, String include, String exclude) {
643                    return getSortedKeys(properties, CollectionUtils.toEmptyList(include), CollectionUtils.toEmptyList(exclude));
644            }
645    
646            /**
647             * Return property keys that should be included as a sorted list.
648             */
649            public static final List<String> getSortedKeys(Properties properties, List<String> includes, List<String> excludes) {
650                    List<String> keys = getSortedKeys(properties);
651                    List<String> includedKeys = new ArrayList<String>();
652                    for (String key : keys) {
653                            if (include(key, includes, excludes)) {
654                                    includedKeys.add(key);
655                            }
656                    }
657                    return includedKeys;
658            }
659    
660            /**
661             * Return a sorted <code>List</code> of keys from <code>properties</code> that start with <code>prefix</code>
662             */
663            public static final List<String> getStartsWithKeys(Properties properties, String prefix) {
664                    List<String> keys = getSortedKeys(properties);
665                    List<String> matches = new ArrayList<String>();
666                    for (String key : keys) {
667                            if (StringUtils.startsWith(key, prefix)) {
668                                    matches.add(key);
669                            }
670                    }
671                    return matches;
672            }
673    
674            /**
675             * Return the property keys as a sorted list.
676             */
677            public static final List<String> getSortedKeys(Properties properties) {
678                    List<String> keys = new ArrayList<String>(properties.stringPropertyNames());
679                    Collections.sort(keys);
680                    return keys;
681            }
682    
683            public static final String toString(Properties properties) {
684                    List<String> keys = getSortedKeys(properties);
685                    StringBuilder sb = new StringBuilder();
686                    for (String key : keys) {
687                            String value = Str.flatten(properties.getProperty(key));
688                            sb.append(key + "=" + value + "\n");
689                    }
690                    return sb.toString();
691            }
692    
693            public static final void info(Properties properties) {
694                    properties = toEmpty(properties);
695                    logger.info("--- Displaying {} properties ---\n\n{}", properties.size(), toString(properties));
696            }
697    
698            public static final void debug(Properties properties) {
699                    properties = toEmpty(properties);
700                    logger.debug("--- Displaying {} properties ---\n\n{}", properties.size(), toString(properties));
701            }
702    
703            /**
704             * Store the properties to the indicated file using the platform default encoding.
705             */
706            public static final void store(Properties properties, File file) {
707                    store(properties, file, null);
708            }
709    
710            /**
711             * Store the properties to the indicated file using the indicated encoding.
712             */
713            public static final void store(Properties properties, File file, String encoding) {
714                    store(properties, file, encoding, null);
715            }
716    
717            /**
718             * Store the properties to the indicated file using the indicated encoding with the indicated comment appearing at the top of the file.
719             */
720            public static final void store(Properties properties, File file, String encoding, String comment) {
721                    store(properties, file, encoding, comment, false);
722            }
723    
724            /**
725             * Store the properties to the indicated file using the indicated encoding with the indicated comment appearing at the top of the file.
726             */
727            public static final void store(Properties properties, File file, String encoding, String comment, boolean silent) {
728                    OutputStream out = null;
729                    Writer writer = null;
730                    try {
731                            out = FileUtils.openOutputStream(file);
732                            String path = file.getCanonicalPath();
733                            boolean xml = isXml(path);
734                            Properties sorted = getSortedProperties(properties);
735                            comment = getComment(encoding, comment, xml);
736                            if (xml) {
737                                    if (!silent) {
738                                            logger.info("Storing XML properties - [{}] encoding={}", path, StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING));
739                                    }
740                                    if (encoding == null) {
741                                            sorted.storeToXML(out, comment);
742                                    } else {
743                                            sorted.storeToXML(out, comment, encoding);
744                                    }
745                            } else {
746                                    writer = LocationUtils.getWriter(out, encoding);
747                                    if (!silent) {
748                                            logger.info("Storing properties - [{}] encoding={}", path, StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING));
749                                    }
750                                    sorted.store(writer, comment);
751                            }
752                    } catch (IOException e) {
753                            throw new IllegalStateException("Unexpected IO error", e);
754                    } finally {
755                            IOUtils.closeQuietly(writer);
756                            IOUtils.closeQuietly(out);
757                    }
758            }
759    
760            /**
761             * Return a new properties object containing the properties from <code>getEnvAsProperties()</code> and <code>System.getProperties()</code>. Properties from
762             * <code>System.getProperties()</code> override properties from <code>getEnvAsProperties</code> if there are duplicates.
763             */
764            public static final Properties getGlobalProperties() {
765                    return getProperties(Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
766            }
767    
768            /**
769             * Return a new properties object containing the properties passed in, plus any properties returned by <code>getEnvAsProperties()</code> and <code>System.getProperties()</code>
770             * . Properties from <code>getEnvAsProperties()</code> override <code>properties</code> and properties from <code>System.getProperties()</code> override everything.
771             */
772            public static final Properties getGlobalProperties(Properties properties) {
773                    return getProperties(properties, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
774            }
775    
776            /**
777             * 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
778             * 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
779             * <code>getEnvProperties()</code>. If <code>mode</code> is <code>SYSTEM</code> the new properties contain the original properties plus <code>System.getProperties()</code>. If
780             * <code>mode</code> is <code>BOTH</code> the new properties contain the original properties plus <code>getEnvProperties()</code> and <code>System.getProperties()</code>.
781             */
782            public static final Properties getProperties(Properties properties, GlobalPropertiesMode mode) {
783                    Properties newProperties = duplicate(properties);
784                    List<PropertyProcessor> modifiers = getPropertyProcessors(mode);
785                    for (PropertyProcessor modifier : modifiers) {
786                            modifier.process(newProperties);
787                    }
788                    return newProperties;
789            }
790    
791            /**
792             * 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
793             * <code>ENVIRONMENT</code> the new properties contain the properties returned by <code>getEnvProperties()</code>. If <code>mode</code> is <code>SYSTEM</code> the new
794             * properties contain <code>System.getProperties()</code>. If <code>mode</code> is <code>BOTH</code> the new properties contain <code>getEnvProperties</code> plus
795             * <code>System.getProperties()</code> with system properties overriding environment variables if the same case sensitive property key is supplied in both places.
796             */
797            public static final Properties getProperties(GlobalPropertiesMode mode) {
798                    return getProperties(new Properties(), mode);
799            }
800    
801            /**
802             * Search global properties to find a value for <code>key</code> according to the mode passed in.
803             */
804            public static final String getProperty(String key, GlobalPropertiesMode mode) {
805                    return getProperty(key, new Properties(), mode);
806            }
807    
808            /**
809             * 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
810             * from the global properties is returned.
811             */
812            public static final String getProperty(String key, Properties properties, GlobalPropertiesMode mode) {
813                    return getProperties(properties, mode).getProperty(key);
814            }
815    
816            /**
817             * Return modifiers that add environment variables, system properties, or both, according to the mode passed in.
818             */
819            public static final List<PropertyProcessor> getPropertyProcessors(GlobalPropertiesMode mode) {
820                    List<PropertyProcessor> processors = new ArrayList<PropertyProcessor>();
821                    switch (mode) {
822                    case NONE:
823                            return processors;
824                    case ENVIRONMENT:
825                            processors.add(new AddPropertiesProcessor(getEnvAsProperties()));
826                            return processors;
827                    case SYSTEM:
828                            processors.add(new AddPropertiesProcessor(System.getProperties()));
829                            return processors;
830                    case BOTH:
831                            processors.add(new AddPropertiesProcessor(getEnvAsProperties()));
832                            processors.add(new AddPropertiesProcessor(System.getProperties()));
833                            return processors;
834                    default:
835                            throw new IllegalStateException(mode + " is unknown");
836                    }
837            }
838    
839            /**
840             * Convert the <code>Map</code> to a <code>Properties</code> object.
841             */
842            public static final Properties convert(Map<String, String> map) {
843                    Properties props = new Properties();
844                    for (String key : map.keySet()) {
845                            String value = map.get(key);
846                            props.setProperty(key, value);
847                    }
848                    return props;
849            }
850    
851            /**
852             * Return a new properties object that duplicates the properties passed in.
853             */
854            public static final Properties duplicate(Properties properties) {
855                    Properties newProperties = new Properties();
856                    newProperties.putAll(properties);
857                    return newProperties;
858            }
859    
860            /**
861             * Return a new properties object containing environment variables as properties prefixed with <code>env</code>
862             */
863            public static Properties getEnvAsProperties() {
864                    return getEnvAsProperties(ENV_PREFIX);
865            }
866    
867            /**
868             * Return a new properties object containing environment variables as properties prefixed with <code>prefix</code>
869             */
870            public static Properties getEnvAsProperties(String prefix) {
871                    Properties properties = convert(System.getenv());
872                    return getPrefixedProperties(properties, prefix);
873            }
874    
875            /**
876             * Return true if, and only if, location ends with <code>.xml</code> (case insensitive).
877             */
878            public static final boolean isXml(String location) {
879                    return StringUtils.endsWithIgnoreCase(location, XML_EXTENSION);
880            }
881    
882            /**
883             * Return true if, and only if, location ends with <code>rice-properties.xml</code> (case insensitive).
884             */
885            public static final boolean isRiceProperties(String location) {
886                    return StringUtils.endsWithIgnoreCase(location, Constants.RICE_PROPERTIES_SUFFIX);
887            }
888    
889            /**
890             * Return a new <code>Properties</code> object loaded from <code>file</code> where the properties are stored in Rice XML style syntax
891             */
892            public static final Properties loadRiceProperties(File file) {
893                    return loadRiceProperties(LocationUtils.getCanonicalPath(file));
894            }
895    
896            /**
897             * Return a new <code>Properties</code> object loaded from <code>location</code> where the properties are stored in Rice XML style syntax
898             */
899            public static final Properties loadRiceProperties(String location) {
900                    logger.info("Loading Rice properties [{}] encoding={}", location, DEFAULT_XML_ENCODING);
901                    String contents = LocationUtils.toString(location, DEFAULT_XML_ENCODING);
902                    String config = StringUtils.substringBetween(contents, "<config>", "</config>");
903                    String[] tokens = StringUtils.substringsBetween(config, "<param", "</param>");
904    
905                    Properties properties = new Properties();
906                    for (String token : tokens) {
907                            String key = StringUtils.substringBetween(token, "name=\"", "\">");
908                            validateRiceProperties(token, key);
909                            String value = StringUtils.substringBetween(token + "</param>", "\">", "</param>");
910                            properties.setProperty(key, value);
911                    }
912                    return properties;
913            }
914    
915            /**
916             * Make sure they are just loading simple properties and are not using any of the unsupported "features". Can't have a key named config.location, and can't use the system,
917             * override, or random attributes.
918             */
919            protected static final void validateRiceProperties(String token, String key) {
920                    if (StringUtils.equalsIgnoreCase("config.location", key)) {
921                            throw new IllegalArgumentException("config.location is not supported");
922                    }
923                    if (StringUtils.contains(token, "override=\"")) {
924                            throw new IllegalArgumentException("override attribute is not supported");
925                    }
926                    if (StringUtils.contains(token, "system=\"")) {
927                            throw new IllegalArgumentException("system attribute is not supported");
928                    }
929                    if (StringUtils.contains(token, "random=\"")) {
930                            throw new IllegalArgumentException("random attribute is not supported");
931                    }
932            }
933    
934            /**
935             * Return a new <code>Properties</code> object loaded from <code>file</code>.
936             */
937            public static final Properties load(File file) {
938                    return load(file, null);
939            }
940    
941            /**
942             * Return a new <code>Properties</code> object loaded from <code>file</code> using the given encoding.
943             */
944            public static final Properties load(File file, String encoding) {
945                    String location = LocationUtils.getCanonicalPath(file);
946                    return load(location, encoding);
947            }
948    
949            /**
950             * Return a new <code>Properties</code> object loaded from <code>location</code>.
951             */
952            public static final Properties load(String location) {
953                    return load(location, null);
954            }
955    
956            /**
957             * Return a new <code>Properties</code> object loaded from <code>locations</code> using <code>encoding</code>.
958             */
959            public static final Properties load(List<String> locations, String encoding) {
960                    Properties properties = new Properties();
961                    for (String location : locations) {
962                            properties.putAll(load(location, encoding));
963                    }
964                    return properties;
965            }
966    
967            /**
968             * Return a new <code>Properties</code> object loaded from <code>location</code> using <code>encoding</code>.
969             */
970            public static final Properties load(String location, String encoding) {
971                    return load(location, encoding, PropertyFormat.NORMAL);
972            }
973    
974            /**
975             * Return a new <code>Properties</code> object loaded from <code>location</code> using <code>encoding</code>.
976             */
977            public static final Properties load(String location, String encoding, PropertyFormat format) {
978                    return load(location, encoding, format, false);
979            }
980    
981            /**
982             * Return a new <code>Properties</code> object loaded from <code>location</code> using <code>encoding</code>.
983             */
984            public static final Properties load(String location, String encoding, PropertyFormat format, boolean silent) {
985                    InputStream in = null;
986                    Reader reader = null;
987                    try {
988                            Properties properties = new Properties();
989                            boolean xml = isXml(location);
990                            boolean riceProperties = isRiceProperties(location);
991                            location = getCanonicalLocation(location);
992                            if (PropertyFormat.RICE.equals(format) || riceProperties) {
993                                    properties = loadRiceProperties(location);
994                            } else if (xml) {
995                                    in = LocationUtils.getInputStream(location);
996                                    if (!silent) {
997                                            logger.info("Loading XML properties - [{}]", location);
998                                    }
999                                    properties.loadFromXML(in);
1000                            } else {
1001                                    if (!silent) {
1002                                            logger.info("Loading properties - [{}] encoding={}", location, StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING));
1003                                    }
1004                                    reader = LocationUtils.getBufferedReader(location, encoding);
1005                                    properties.load(reader);
1006                            }
1007                            return properties;
1008                    } catch (IOException e) {
1009                            throw new IllegalStateException("Unexpected IO error", e);
1010                    } finally {
1011                            IOUtils.closeQuietly(in);
1012                            IOUtils.closeQuietly(reader);
1013                    }
1014            }
1015    
1016            protected static String getCanonicalLocation(String location) {
1017                    if (LocationUtils.isExistingFile(location)) {
1018                            return LocationUtils.getCanonicalPath(new File(location));
1019                    } else {
1020                            return location;
1021                    }
1022            }
1023    
1024            /**
1025             * 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
1026             * the properties passed in.
1027             */
1028            public static final Properties getPrefixedProperties(Properties properties, String prefix) {
1029                    if (StringUtils.isBlank(prefix)) {
1030                            return duplicate(properties);
1031                    }
1032                    Properties newProperties = new Properties();
1033                    for (String key : properties.stringPropertyNames()) {
1034                            String value = properties.getProperty(key);
1035                            String newKey = StringUtils.startsWith(key, prefix + ".") ? key : prefix + "." + key;
1036                            newProperties.setProperty(newKey, value);
1037                    }
1038                    return newProperties;
1039            }
1040    
1041            /**
1042             * Return a new properties object where the keys have been converted to upper case and periods have been replaced with an underscore.
1043             */
1044            public static final Properties reformatKeysAsEnvVars(Properties properties) {
1045                    Properties newProperties = new Properties();
1046                    for (String key : properties.stringPropertyNames()) {
1047                            String value = properties.getProperty(key);
1048                            String newKey = StringUtils.upperCase(StringUtils.replace(key, ".", "-"));
1049                            newProperties.setProperty(newKey, value);
1050                    }
1051                    return newProperties;
1052            }
1053    
1054            /**
1055             * 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
1056             * <code>propertyOverwriteMode</code> to make sure we have permission to override the value.
1057             */
1058            public static final void addOrOverrideProperty(Properties properties, String key, String newValue, Mode propertyOverwriteMode) {
1059                    String oldValue = properties.getProperty(key);
1060                    if (StringUtils.equals(newValue, oldValue)) {
1061                            // Nothing to do! New value is the same as old value.
1062                            return;
1063                    }
1064                    boolean overwrite = !StringUtils.isBlank(oldValue);
1065    
1066                    // TODO Yuck! Do something smarter here
1067                    String logNewValue = newValue;
1068                    String logOldValue = oldValue;
1069                    if (obscure(key)) {
1070                            logNewValue = "PROTECTED";
1071                            logOldValue = "PROTECTED";
1072                    }
1073    
1074                    if (overwrite) {
1075                            // This property already has a value, and it is different from the new value
1076                            // Check to make sure we are allowed to override the old value before doing so
1077                            Object[] args = new Object[] { key, Str.flatten(logNewValue), Str.flatten(logOldValue) };
1078                            ModeUtils.validate(propertyOverwriteMode, "Overriding [{}={}] was [{}]", args, "Override of existing property [" + key + "] is not allowed.");
1079                    } else {
1080                            // There is no existing value for this key
1081                            logger.info("Adding [{}={}]", key, Str.flatten(logNewValue));
1082                    }
1083                    properties.setProperty(key, newValue);
1084            }
1085    
1086            protected static boolean obscure(String key) {
1087                    if (StringUtils.containsIgnoreCase(key, ".password")) {
1088                            return true;
1089                    }
1090                    if (StringUtils.containsIgnoreCase(key, ".secret")) {
1091                            return true;
1092                    }
1093                    if (StringUtils.containsIgnoreCase(key, ".private")) {
1094                            return true;
1095                    }
1096                    return false;
1097            }
1098    
1099            private static final String getDefaultComment(String encoding, boolean xml) {
1100                    if (encoding == null) {
1101                            if (xml) {
1102                                    // Java defaults XML properties files to UTF-8 if no encoding is provided
1103                                    return "encoding.default=" + DEFAULT_XML_ENCODING;
1104                            } else {
1105                                    // For normal properties files the platform default encoding is used
1106                                    return "encoding.default=" + DEFAULT_ENCODING;
1107                            }
1108                    } else {
1109                            return "encoding.specified=" + encoding;
1110                    }
1111            }
1112    
1113            private static final String getComment(String encoding, String comment, boolean xml) {
1114                    if (StringUtils.isBlank(comment)) {
1115                            return getDefaultComment(encoding, xml);
1116                    } else {
1117                            return comment + "\n#" + getDefaultComment(encoding, xml);
1118                    }
1119            }
1120    
1121            /**
1122             * This is private because <code>SortedProperties</code> does not fully honor the contract for <code>Properties</code>
1123             */
1124            private static final SortedProperties getSortedProperties(Properties properties) {
1125                    SortedProperties sp = new PropertyUtils().new SortedProperties();
1126                    sp.putAll(properties);
1127                    return sp;
1128            }
1129    
1130            /**
1131             * 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.
1132             */
1133            private class SortedProperties extends Properties {
1134    
1135                    private static final long serialVersionUID = 1330825236411537386L;
1136    
1137                    /**
1138                     * <code>Properties.storeToXML()</code> uses <code>keySet()</code>
1139                     */
1140                    @Override
1141                    public Set<Object> keySet() {
1142                            return Collections.unmodifiableSet(new TreeSet<Object>(super.keySet()));
1143                    }
1144    
1145                    /**
1146                     * <code>Properties.store()</code> uses <code>keys()</code>
1147                     */
1148                    @Override
1149                    public synchronized Enumeration<Object> keys() {
1150                            return Collections.enumeration(new TreeSet<Object>(super.keySet()));
1151                    }
1152            }
1153    
1154            /**
1155             * Set properties in the given Properties to CSV versions of the lists in the ComparisonResults
1156             * 
1157             * @param properties
1158             *            the Properties to populate
1159             * @param listComparison
1160             *            the ComparisonResults to use for data
1161             * @param propertyNames
1162             *            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:
1163             *            key for the DELETED list
1164             */
1165            public static final void addListComparisonProperties(Properties properties, ComparisonResults listComparison, List<String> propertyNames) {
1166                    // make sure that there are three names in the list of property names
1167                    Assert.isTrue(propertyNames.size() == 3);
1168    
1169                    properties.setProperty(propertyNames.get(0), CollectionUtils.getCSV(listComparison.getAdded()));
1170                    properties.setProperty(propertyNames.get(1), CollectionUtils.getCSV(listComparison.getSame()));
1171                    properties.setProperty(propertyNames.get(2), CollectionUtils.getCSV(listComparison.getDeleted()));
1172            }
1173    
1174    }