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