View Javadoc

1   /**
2    * Copyright 2010-2012 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.common.util;
17  
18  import java.io.File;
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.io.OutputStream;
22  import java.io.Reader;
23  import java.io.Writer;
24  import java.nio.charset.Charset;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.Enumeration;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Properties;
31  import java.util.Set;
32  import java.util.TreeSet;
33  
34  import org.apache.commons.io.FileUtils;
35  import org.apache.commons.io.IOUtils;
36  import org.apache.commons.lang3.StringUtils;
37  import org.kuali.common.util.property.Constants;
38  import org.kuali.common.util.property.GlobalPropertiesMode;
39  import org.kuali.common.util.property.processor.AddPropertiesProcessor;
40  import org.kuali.common.util.property.processor.PropertyProcessor;
41  import org.slf4j.Logger;
42  import org.slf4j.LoggerFactory;
43  import org.springframework.util.PropertyPlaceholderHelper;
44  
45  /**
46   * Simplify handling of <code>Properties</code> especially as it relates to storing and loading. <code>Properties</code> can be loaded from
47   * any url Spring resource loading can understand. When storing and loading, locations ending in <code>.xml</code> are automatically handled
48   * using <code>storeToXML()</code> and <code>loadFromXML()</code>, respectively. <code>Properties</code> are always stored in sorted order
49   * with the <code>encoding</code> indicated via a comment.
50   */
51  public class PropertyUtils {
52  
53  	private static final Logger logger = LoggerFactory.getLogger(PropertyUtils.class);
54  
55  	private static final String XML_EXTENSION = ".xml";
56  	private static final String ENV_PREFIX = "env";
57  	private static final String DEFAULT_ENCODING = Charset.defaultCharset().name();
58  	private static final String DEFAULT_XML_ENCODING = "UTF-8";
59  
60  	public static final Properties toEmpty(Properties properties) {
61  		return properties == null ? new Properties() : properties;
62  	}
63  
64  	public static final boolean isSingleUnresolvedPlaceholder(String string) {
65  		return isSingleUnresolvedPlaceholder(string, Constants.DEFAULT_PLACEHOLDER_PREFIX, Constants.DEFAULT_PLACEHOLDER_SUFFIX);
66  	}
67  
68  	public static final boolean isSingleUnresolvedPlaceholder(String string, String prefix, String suffix) {
69  		int prefixMatches = StringUtils.countMatches(string, prefix);
70  		int suffixMatches = StringUtils.countMatches(string, suffix);
71  		boolean startsWith = StringUtils.startsWith(string, prefix);
72  		boolean endsWith = StringUtils.endsWith(string, suffix);
73  		return prefixMatches == 1 && suffixMatches == 1 && startsWith && endsWith;
74  	}
75  
76  	public static final boolean containsUnresolvedPlaceholder2(String string) {
77  		int beginIndex = StringUtils.indexOf(string, Constants.DEFAULT_PLACEHOLDER_PREFIX);
78  		if (beginIndex == -1) {
79  			return false;
80  		}
81  		int endIndex = StringUtils.indexOf(string, Constants.DEFAULT_PLACEHOLDER_SUFFIX, beginIndex);
82  		if (endIndex == -1) {
83  			return false;
84  		}
85  		String substring = StringUtils.substring(string, beginIndex, endIndex);
86  		return StringUtils.indexOf(substring, Constants.DEFAULT_VALUE_SEPARATOR) == -1;
87  	}
88  
89  	public static final boolean containsUnresolvedPlaceholder(String string) {
90  		return containsUnresolvedPlaceholder(string, Constants.DEFAULT_PLACEHOLDER_PREFIX, Constants.DEFAULT_PLACEHOLDER_SUFFIX);
91  	}
92  
93  	public static final boolean containsUnresolvedPlaceholder(String string, String prefix, String suffix) {
94  		int beginIndex = StringUtils.indexOf(string, prefix);
95  		if (beginIndex == -1) {
96  			return false;
97  		}
98  		return StringUtils.indexOf(string, suffix) != -1;
99  	}
100 
101 	/**
102 	 * Return a new <code>Properties</code> object containing only those properties where the resolved value is different from the original
103 	 * value. Using global properties to perform property resolution as indicated by <code>Constants.DEFAULT_GLOBAL_PROPERTIES_MODE</code>
104 	 */
105 	public static final Properties getResolvedProperties(Properties properties) {
106 		return getResolvedProperties(properties, Constants.DEFAULT_PROPERTY_PLACEHOLDER_HELPER, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
107 	}
108 
109 	/**
110 	 * Return a new <code>Properties</code> object containing only those properties where the resolved value is different from the original
111 	 * value. Using global properties to perform property resolution as indicated by <code>globalPropertiesMode</code>
112 	 */
113 	public static final Properties getResolvedProperties(Properties properties, GlobalPropertiesMode globalPropertiesMode) {
114 		return getResolvedProperties(properties, Constants.DEFAULT_PROPERTY_PLACEHOLDER_HELPER, globalPropertiesMode);
115 	}
116 
117 	/**
118 	 * Return a new <code>Properties</code> object containing only those properties where the resolved value is different from the original
119 	 * value. Using global properties to perform property resolution as indicated by <code>Constants.DEFAULT_GLOBAL_PROPERTIES_MODE</code>
120 	 */
121 	public static final Properties getResolvedProperties(Properties properties, PropertyPlaceholderHelper helper) {
122 		return getResolvedProperties(properties, helper, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
123 	}
124 
125 	/**
126 	 * Return a new <code>Properties</code> object containing only those properties where the resolved value is different from the original
127 	 * value. Using global properties to perform property resolution as indicated by <code>globalPropertiesMode</code>
128 	 */
129 	public static final Properties getResolvedProperties(Properties properties, PropertyPlaceholderHelper helper, GlobalPropertiesMode globalPropertiesMode) {
130 		Properties global = PropertyUtils.getProperties(properties, globalPropertiesMode);
131 		List<String> keys = PropertyUtils.getSortedKeys(properties);
132 		Properties newProperties = new Properties();
133 		for (String key : keys) {
134 			String originalValue = properties.getProperty(key);
135 			String resolvedValue = helper.replacePlaceholders(originalValue, global);
136 			if (!resolvedValue.equals(originalValue)) {
137 				logger.debug("Resolved property '" + key + "' [{}] -> [{}]", Str.flatten(originalValue), Str.flatten(resolvedValue));
138 				newProperties.setProperty(key, resolvedValue);
139 			}
140 		}
141 		return newProperties;
142 	}
143 
144 	/**
145 	 * Return the property values from <code>keys</code>
146 	 */
147 	public static final List<String> getValues(Properties properties, List<String> keys) {
148 		List<String> values = new ArrayList<String>();
149 		for (String key : keys) {
150 			values.add(properties.getProperty(key));
151 		}
152 		return values;
153 	}
154 
155 	/**
156 	 * Return a sorted <code>List</code> of keys from <code>properties</code> that end with <code>suffix</code>.
157 	 */
158 	public static final List<String> getEndsWithKeys(Properties properties, String suffix) {
159 		List<String> keys = getSortedKeys(properties);
160 		List<String> matches = new ArrayList<String>();
161 		for (String key : keys) {
162 			if (StringUtils.endsWith(key, suffix)) {
163 				matches.add(key);
164 			}
165 		}
166 		return matches;
167 	}
168 
169 	/**
170 	 * Alter the <code>properties</code> passed in to contain only the desired property values. <code>includes</code> and
171 	 * <code>excludes</code> are comma separated values.
172 	 */
173 	public static final void trim(Properties properties, String includesCSV, String excludesCSV) {
174 		List<String> includes = CollectionUtils.getTrimmedListFromCSV(includesCSV);
175 		List<String> excludes = CollectionUtils.getTrimmedListFromCSV(excludesCSV);
176 		trim(properties, includes, excludes);
177 	}
178 
179 	/**
180 	 * Alter the <code>properties</code> passed in to contain only the desired property values.
181 	 */
182 	public static final void trim(Properties properties, List<String> includes, List<String> excludes) {
183 		List<String> keys = getSortedKeys(properties);
184 		for (String key : keys) {
185 			boolean include = include(key, includes, excludes);
186 			if (!include) {
187 				logger.debug("Removing [{}]", key);
188 				properties.remove(key);
189 			}
190 		}
191 	}
192 
193 	/**
194 	 * Return true if <code>value</code> should be included, false otherwise.<br>
195 	 * If <code>excludes</codes> is not empty and contains <code>value</code> return false.<br>
196 	 * If <code>value</code> has not been explicitly excluded, proceed with checking the <code>includes</code> list.<br>
197 	 * If <code>includes</code> is empty return true.<br>
198 	 * If <code>includes</code> is not empty, return true if, and only if, <code>value</code> appears in the list.
199 	 */
200 	public static final boolean include(String value, List<String> includes, List<String> excludes) {
201 		if (!CollectionUtils.isEmpty(excludes) && excludes.contains(value)) {
202 			return false;
203 		} else {
204 			return CollectionUtils.isEmpty(includes) || includes.contains(value);
205 		}
206 	}
207 
208 	/**
209 	 * Return property keys that should be included as a sorted list.
210 	 */
211 	public static final List<String> getSortedKeys(Properties properties, List<String> includes, List<String> excludes) {
212 		List<String> keys = getSortedKeys(properties);
213 		List<String> includedKeys = new ArrayList<String>();
214 		for (String key : keys) {
215 			if (include(key, includes, excludes)) {
216 				includedKeys.add(key);
217 			}
218 		}
219 		return includedKeys;
220 	}
221 
222 	/**
223 	 * Return a sorted <code>List</code> of keys from <code>properties</code> that start with <code>prefix</code>
224 	 */
225 	public static final List<String> getStartsWithKeys(Properties properties, String prefix) {
226 		List<String> keys = getSortedKeys(properties);
227 		List<String> matches = new ArrayList<String>();
228 		for (String key : keys) {
229 			if (StringUtils.startsWith(key, prefix)) {
230 				matches.add(key);
231 			}
232 		}
233 		return matches;
234 	}
235 
236 	/**
237 	 * Return the property keys as a sorted list.
238 	 */
239 	public static final List<String> getSortedKeys(Properties properties) {
240 		List<String> keys = new ArrayList<String>(properties.stringPropertyNames());
241 		Collections.sort(keys);
242 		return keys;
243 	}
244 
245 	public static final void show(Properties properties) {
246 		List<String> keys = getSortedKeys(properties);
247 		for (String key : keys) {
248 			String value = Str.flatten(properties.getProperty(key));
249 			logger.info(key + "=" + value);
250 		}
251 	}
252 
253 	/**
254 	 * Store the properties to the indicated file using the platform default encoding.
255 	 */
256 	public static final void store(Properties properties, File file) {
257 		store(properties, file, null);
258 	}
259 
260 	/**
261 	 * Store the properties to the indicated file using the indicated encoding.
262 	 */
263 	public static final void store(Properties properties, File file, String encoding) {
264 		store(properties, file, encoding, null);
265 	}
266 
267 	/**
268 	 * Store the properties to the indicated file using the indicated encoding with the indicated comment appearing at the top of the file.
269 	 */
270 	public static final void store(Properties properties, File file, String encoding, String comment) {
271 		OutputStream out = null;
272 		Writer writer = null;
273 		try {
274 			out = FileUtils.openOutputStream(file);
275 			String path = file.getCanonicalPath();
276 			boolean xml = isXml(path);
277 			Properties sorted = getSortedProperties(properties);
278 			comment = getComment(encoding, comment, xml);
279 			if (xml) {
280 				logger.info("Storing XML properties - [{}] encoding={}", path, StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING));
281 				if (encoding == null) {
282 					sorted.storeToXML(out, comment);
283 				} else {
284 					sorted.storeToXML(out, comment, encoding);
285 				}
286 			} else {
287 				writer = LocationUtils.getWriter(out, encoding);
288 				logger.info("Storing properties - [{}] encoding={}", path, StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING));
289 				sorted.store(writer, comment);
290 			}
291 		} catch (IOException e) {
292 			throw new IllegalStateException("Unexpected IO error", e);
293 		} finally {
294 			IOUtils.closeQuietly(writer);
295 			IOUtils.closeQuietly(out);
296 		}
297 	}
298 
299 	/**
300 	 * Return a new properties object containing the properties from <code>getEnvAsProperties()</code> and
301 	 * <code>System.getProperties()</code>. Properties from <code>System.getProperties()</code> override properties from
302 	 * <code>getEnvAsProperties</code> if there are duplicates.
303 	 */
304 	public static final Properties getGlobalProperties() {
305 		return getProperties(Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
306 	}
307 
308 	/**
309 	 * Return a new properties object containing the properties passed in, plus any properties returned by <code>getEnvAsProperties()</code>
310 	 * and <code>System.getProperties()</code>. Properties from <code>getEnvAsProperties()</code> override <code>properties</code> and
311 	 * properties from <code>System.getProperties()</code> override everything.
312 	 */
313 	public static final Properties getGlobalProperties(Properties properties) {
314 		return getProperties(properties, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
315 	}
316 
317 	/**
318 	 * Return a new properties object containing the properties passed in, plus any global properties as requested. If <code>mode</code> is
319 	 * <code>NONE</code> the new properties are a duplicate of the properties passed in. If <code>mode</code> is <code>ENVIRONMENT</code>
320 	 * the new properties contain the original properties plus any properties returned by <code>getEnvProperties()</code>. If
321 	 * <code>mode</code> is <code>SYSTEM</code> the new properties contain the original properties plus <code>System.getProperties()</code>.
322 	 * If <code>mode</code> is <code>BOTH</code> the new properties contain the original properties plus <code>getEnvProperties()</code> and
323 	 * <code>System.getProperties()</code>.
324 	 */
325 	public static final Properties getProperties(Properties properties, GlobalPropertiesMode mode) {
326 		Properties newProperties = duplicate(properties);
327 		List<PropertyProcessor> modifiers = getPropertyProcessors(mode);
328 		for (PropertyProcessor modifier : modifiers) {
329 			modifier.process(newProperties);
330 		}
331 		return newProperties;
332 	}
333 
334 	/**
335 	 * Return a new properties object containing global properties as requested. If <code>mode</code> is <code>NONE</code> the new
336 	 * properties are empty. If <code>mode</code> is <code>ENVIRONMENT</code> the new properties contain the properties returned by
337 	 * <code>getEnvProperties()</code>. If <code>mode</code> is <code>SYSTEM</code> the new properties contain
338 	 * <code>System.getProperties()</code>. If <code>mode</code> is <code>BOTH</code> the new properties contain
339 	 * <code>getEnvProperties</code> plus <code>System.getProperties()</code> with system properties overriding environment variables if the
340 	 * same case sensitive property key is supplied in both places.
341 	 */
342 	public static final Properties getProperties(GlobalPropertiesMode mode) {
343 		return getProperties(new Properties(), mode);
344 	}
345 
346 	/**
347 	 * Search global properties to find a value for <code>key</code> according to the mode passed in.
348 	 */
349 	public static final String getProperty(String key, GlobalPropertiesMode mode) {
350 		return getProperty(key, new Properties(), mode);
351 	}
352 
353 	/**
354 	 * Search <code>properties</code> plus global properties to find a value for <code>key</code> according to the mode passed in. If the
355 	 * property is present in both, the value from the global properties is returned.
356 	 */
357 	public static final String getProperty(String key, Properties properties, GlobalPropertiesMode mode) {
358 		return getProperties(properties, mode).getProperty(key);
359 	}
360 
361 	/**
362 	 * Return modifiers that add environment variables, system properties, or both, according to the mode passed in.
363 	 */
364 	public static final List<PropertyProcessor> getPropertyProcessors(GlobalPropertiesMode mode) {
365 		List<PropertyProcessor> processors = new ArrayList<PropertyProcessor>();
366 		switch (mode) {
367 		case NONE:
368 			return processors;
369 		case ENVIRONMENT:
370 			processors.add(new AddPropertiesProcessor(getEnvAsProperties()));
371 			return processors;
372 		case SYSTEM:
373 			processors.add(new AddPropertiesProcessor(System.getProperties()));
374 			return processors;
375 		case BOTH:
376 			processors.add(new AddPropertiesProcessor(getEnvAsProperties()));
377 			processors.add(new AddPropertiesProcessor(System.getProperties()));
378 			return processors;
379 		default:
380 			throw new IllegalStateException(mode + " is unknown");
381 		}
382 	}
383 
384 	/**
385 	 * Convert the <code>Map</code> to a <code>Properties</code> object.
386 	 */
387 	public static final Properties convert(Map<String, String> map) {
388 		Properties props = new Properties();
389 		for (String key : map.keySet()) {
390 			String value = map.get(key);
391 			props.setProperty(key, value);
392 		}
393 		return props;
394 	}
395 
396 	/**
397 	 * Return a new properties object that duplicates the properties passed in.
398 	 */
399 	public static final Properties duplicate(Properties properties) {
400 		Properties newProperties = new Properties();
401 		newProperties.putAll(properties);
402 		return newProperties;
403 	}
404 
405 	/**
406 	 * Return a new properties object containing environment variables as properties prefixed with <code>env</code>
407 	 */
408 	public static Properties getEnvAsProperties() {
409 		return getEnvAsProperties(ENV_PREFIX);
410 	}
411 
412 	/**
413 	 * Return a new properties object containing environment variables as properties prefixed with <code>prefix</code>
414 	 */
415 	public static Properties getEnvAsProperties(String prefix) {
416 		Properties properties = convert(System.getenv());
417 		return getPrefixedProperties(properties, prefix);
418 	}
419 
420 	/**
421 	 * Return true if, and only if, location ends with <code>.xml</code> (case insensitive).
422 	 */
423 	public static final boolean isXml(String location) {
424 		return StringUtils.endsWithIgnoreCase(location, XML_EXTENSION);
425 	}
426 
427 	/**
428 	 * Return a new <code>Properties</code> object loaded from <code>location</code>.
429 	 */
430 	public static final Properties load(String location) {
431 		return load(location, null);
432 	}
433 
434 	/**
435 	 * Return a new <code>Properties</code> object loaded from <code>location</code> using <code>encoding</code>.
436 	 */
437 	public static final Properties load(String location, String encoding) {
438 		InputStream in = null;
439 		Reader reader = null;
440 		try {
441 			Properties properties = new Properties();
442 			boolean xml = isXml(location);
443 			if (xml) {
444 				in = LocationUtils.getInputStream(location);
445 				logger.info("Loading XML properties - [{}]", location);
446 				properties.loadFromXML(in);
447 			} else {
448 				logger.info("Loading properties - [{}] encoding={}", location, StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING));
449 				reader = LocationUtils.getBufferedReader(location, encoding);
450 				properties.load(reader);
451 			}
452 			return properties;
453 		} catch (IOException e) {
454 			throw new IllegalStateException("Unexpected IO error", e);
455 		} finally {
456 			IOUtils.closeQuietly(in);
457 			IOUtils.closeQuietly(reader);
458 		}
459 	}
460 
461 	/**
462 	 * Return a new <code>Properties</code> object containing properties prefixed with <code>prefix</code>. If <code>prefix</code> is blank,
463 	 * the new properties object duplicates the properties passed in.
464 	 */
465 	public static final Properties getPrefixedProperties(Properties properties, String prefix) {
466 		if (StringUtils.isBlank(prefix)) {
467 			return duplicate(properties);
468 		}
469 		Properties newProperties = new Properties();
470 		for (String key : properties.stringPropertyNames()) {
471 			String value = properties.getProperty(key);
472 			String newKey = StringUtils.startsWith(key, prefix + ".") ? key : prefix + "." + key;
473 			newProperties.setProperty(newKey, value);
474 		}
475 		return newProperties;
476 	}
477 
478 	/**
479 	 * Return a new properties object where the keys have been converted to upper case and periods have been replaced with an underscore.
480 	 */
481 	public static final Properties reformatKeysAsEnvVars(Properties properties) {
482 		Properties newProperties = new Properties();
483 		for (String key : properties.stringPropertyNames()) {
484 			String value = properties.getProperty(key);
485 			String newKey = StringUtils.upperCase(StringUtils.replace(key, ".", "-"));
486 			newProperties.setProperty(newKey, value);
487 		}
488 		return newProperties;
489 	}
490 
491 	/**
492 	 * Before setting the newValue, check to see if there is a conflict with an existing value. If there is no existing value, add the
493 	 * property. If there is a conflict, check <code>propertyOverwriteMode</code> to make sure we have permission to override the value.
494 	 */
495 	public static final void addOrOverrideProperty(Properties properties, String key, String newValue, Mode propertyOverwriteMode) {
496 		String oldValue = properties.getProperty(key);
497 		boolean newEqualsOld = StringUtils.equals(newValue, oldValue);
498 		if (newEqualsOld) {
499 			// Nothing to do! New value is the same as old value.
500 			return;
501 		}
502 		boolean overwrite = !StringUtils.isBlank(oldValue);
503 		if (overwrite) {
504 			// This property already has a value, and it is different from the new value
505 			// Check to make sure we are allowed to override the old value before doing so
506 			ModeUtils.validate(propertyOverwriteMode, "Overriding [{}]", key, "Override of existing property [" + key + "] is not allowed.");
507 		} else {
508 			// There is no existing value for this key
509 			logger.debug("Adding property {}=[{}]", key, Str.flatten(newValue));
510 		}
511 		properties.setProperty(key, newValue);
512 	}
513 
514 	private static final String getDefaultComment(String encoding, boolean xml) {
515 		if (encoding == null) {
516 			if (xml) {
517 				// Java defaults XML properties files to UTF-8 if no encoding is provided
518 				return "encoding.default=" + DEFAULT_XML_ENCODING;
519 			} else {
520 				// For normal properties files the platform default encoding is used
521 				return "encoding.default=" + DEFAULT_ENCODING;
522 			}
523 		} else {
524 			return "encoding.specified=" + encoding;
525 		}
526 	}
527 
528 	private static final String getComment(String comment, String encoding, boolean xml) {
529 		if (StringUtils.isBlank(comment)) {
530 			return getDefaultComment(encoding, xml);
531 		} else {
532 			return comment + "\n#" + getDefaultComment(encoding, xml);
533 		}
534 	}
535 
536 	/**
537 	 * This is private because <code>SortedProperties</code> does not fully honor the contract for <code>Properties</code>
538 	 */
539 	private static final SortedProperties getSortedProperties(Properties properties) {
540 		SortedProperties sp = new PropertyUtils().new SortedProperties();
541 		sp.putAll(properties);
542 		return sp;
543 	}
544 
545 	/**
546 	 * This is private since it does not honor the full contract for <code>Properties</code>. <code>PropertyUtils</code> uses it internally
547 	 * to store properties in sorted order.
548 	 */
549 	private class SortedProperties extends Properties {
550 
551 		private static final long serialVersionUID = 1330825236411537386L;
552 
553 		/**
554 		 * <code>Properties.storeToXML()</code> uses <code>keySet()</code>
555 		 */
556 		@Override
557 		public Set<Object> keySet() {
558 			return Collections.unmodifiableSet(new TreeSet<Object>(super.keySet()));
559 		}
560 
561 		/**
562 		 * <code>Properties.store()</code> uses <code>keys()</code>
563 		 */
564 		@Override
565 		public synchronized Enumeration<Object> keys() {
566 			return Collections.enumeration(new TreeSet<Object>(super.keySet()));
567 		}
568 	}
569 
570 }