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