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