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