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