View Javadoc
1   /**
2    * Copyright 2005-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you cannot 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.properties;
17  
18  import static com.google.common.base.Preconditions.checkArgument;
19  import static com.google.common.base.Preconditions.checkNotNull;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Properties;
28  import java.util.SortedSet;
29  
30  import javax.xml.bind.JAXBContext;
31  import javax.xml.bind.JAXBException;
32  import javax.xml.bind.Unmarshaller;
33  import javax.xml.bind.UnmarshallerHandler;
34  import javax.xml.parsers.ParserConfigurationException;
35  import javax.xml.parsers.SAXParser;
36  import javax.xml.parsers.SAXParserFactory;
37  
38  import org.apache.commons.io.IOUtils;
39  import org.apache.commons.lang3.StringUtils;
40  import org.kuali.common.util.LocationUtils;
41  import org.kuali.common.util.PropertyUtils;
42  import org.kuali.common.util.Str;
43  import org.kuali.common.util.execute.Executable;
44  import org.kuali.common.util.execute.impl.NoOpExecutable;
45  import org.kuali.common.util.execute.impl.SetSystemPropertyExecutable;
46  import org.kuali.common.util.log.LoggerUtils;
47  import org.kuali.common.util.obscure.DefaultObscurer;
48  import org.kuali.common.util.obscure.Obscurer;
49  import org.kuali.common.util.properties.rice.Config;
50  import org.kuali.common.util.properties.rice.Param;
51  import org.slf4j.Logger;
52  import org.springframework.util.PropertyPlaceholderHelper;
53  import org.xml.sax.InputSource;
54  import org.xml.sax.SAXException;
55  import org.xml.sax.XMLReader;
56  
57  import com.google.common.base.Optional;
58  import com.google.common.collect.ImmutableList;
59  import com.google.common.collect.Lists;
60  import com.google.common.collect.Maps;
61  import com.google.common.collect.Sets;
62  
63  /**
64   * <p>
65   * Load Rice XML config files. This class supports the chaining of config files together via {@code config.location} param entries. It also honors the {@code override} attribute on
66   * param entries. It does not support the {@code system} attribute. If that attribute is present anywhere in any config file, an exception is thrown.
67   * </p>
68   * 
69   * <p>
70   * The purpose of this class is to decouple the loading of property files from everything else and produce a plain vanilla {@code java.util.Properties} object that accurately
71   * reflects the contents of the files as a simple object in memory.
72   * </p>
73   * 
74   * <p>
75   * No placeholder resolution is attempted on parameter entries with the exception of {@code config.location} entries. During the loading process, {@code config.location} entries
76   * that contain placeholders, are resolved using system and environment variables as well as any properties that have already been loaded.
77   * </p>
78   */
79  public class RicePropertiesLoader {
80  
81  	private static final Logger logger = LoggerUtils.make();
82  
83  	private final PropertyPlaceholderHelper propertyPlaceholderHelper;
84  	private final String chainedConfigLocationKey;
85  	private final ImmutableList<String> obscureTokens;
86  	private final Obscurer obscurer;
87  	private final Randomizer randomizer;
88  	private final boolean systemPropertiesWin;
89  
90  	public Properties load(String location) {
91  		checkArgument(!StringUtils.isBlank(location), "'location' cannot be blank");
92  		checkArgument(LocationUtils.exists(location), "[%s] does not exist", location);
93  		Unmarshaller unmarshaller = getUnmarshaller();
94  		Map<String, Param> params = Maps.newHashMap();
95  		load(location, unmarshaller, 0, params);
96  		if (systemPropertiesWin) {
97  			Map<String, Param> system = convert(PropertyUtils.getGlobalProperties(), true, false);
98  			for (Param param : system.values()) {
99  				update(params, param, "");
100 			}
101 		}
102 		handleRandomParams(params);
103 		handleSystemParams(params);
104 		return convert(params);
105 	}
106 
107 	protected void load(String location, Unmarshaller unmarshaller, int depth, Map<String, Param> params) {
108 
109 		// Setup an indentation prefix based on the recursive depth
110 		final String prefix = StringUtils.repeat(" ", depth);
111 
112 		// If the location does not exist, we are done
113 		if (location.equals("") || !LocationUtils.exists(location)) {
114 			logger.info("{}# skip non-existent location [{}]", prefix, location);
115 			return;
116 		}
117 
118 		InputStream in = null;
119 		try {
120 			in = LocationUtils.getInputStream(location);
121 			load(prefix, location, in, params, depth, unmarshaller);
122 		} catch (IOException e) {
123 			throw new IllegalStateException(e);
124 		} finally {
125 			IOUtils.closeQuietly(in);
126 		}
127 	}
128 
129 	protected void load(String prefix, String location, InputStream in, Map<String, Param> params, int depth, Unmarshaller unmarshaller) throws IOException {
130 		if (isPropertiesFile(location)) {
131 			loadJavaProperties(prefix, location, in, params, depth);
132 		} else {
133 			loadRiceProperties(prefix, location, in, params, depth, unmarshaller);
134 		}
135 	}
136 
137 	protected void loadRiceProperties(String prefix, String location, InputStream in, Map<String, Param> params, int depth, Unmarshaller unmarshaller) throws IOException {
138 		logger.info("{}+ loading - [{}]", prefix, location);
139 		Config config = unmarshal(unmarshaller, in);
140 		for (Param p : config.getParams()) {
141 			handleParam(p, depth, unmarshaller, params, prefix);
142 		}
143 		logger.info("{}- loaded  - [{}]", prefix, location);
144 	}
145 
146 	protected void handleParam(Param p, int depth, Unmarshaller unmarshaller, Map<String, Param> params, String prefix) {
147 
148 		// This is a reference to a nested config file
149 		if (p.getName().equalsIgnoreCase(chainedConfigLocationKey)) {
150 			String originalLocation = p.getValue();
151 			String resolvedLocation = getResolvedValue(prefix, originalLocation, params);
152 			load(resolvedLocation, unmarshaller, depth + 1, params);
153 		} else {
154 			// Update the map of parameter objects with this parameter
155 			update(params, p, prefix);
156 		}
157 
158 	}
159 
160 	protected void loadJavaProperties(String prefix, String location, InputStream in, Map<String, Param> params, int depth) throws IOException {
161 		logger.info("{}+ loading - [{}]", prefix, location);
162 		Properties loaded = new Properties();
163 		loaded.load(in);
164 
165 		// "override" defaults to true here because that is by far the most "normal" and widely accepted behavior
166 		// Both Spring and Maven adhere to the "last one in wins" strategy, so we follow that
167 		// Normal .properties files don't have a way to toggle an "override" attribute at the individual property level (nor should they)
168 		// Thus, the default value of override=true
169 		Map<String, Param> newMap = convert(loaded, true, false);
170 		for (Param p : newMap.values()) {
171 			update(params, p, prefix);
172 		}
173 		logger.info("{}- loaded  - [{}]", prefix, location);
174 	}
175 
176 	protected void update(Map<String, Param> params, Param p, String prefix) {
177 		checkNotNull(p.getValue(), "parameter value cannot be null");
178 
179 		// Extract the old value (if it's present)
180 		Optional<Param> oldParam = Optional.fromNullable(params.get(p.getName()));
181 
182 		// Get a log friendly value
183 		String newLogValue = getLogValue(p);
184 
185 		// If there is no previous value, just add it
186 		if (!oldParam.isPresent()) {
187 			Object[] args = { prefix, p.getName(), newLogValue };
188 			logger.debug("{}~ add - [{}]=[{}]", args);
189 			params.put(p.getName(), p);
190 			return;
191 		}
192 
193 		// The new value is the same as the old value. Nothing more to do
194 		if (oldParam.get().getValue().equals(p.getValue())) {
195 			Object[] args = { prefix, p.getName(), newLogValue };
196 			logger.debug("{}~ duplicate - [{}]=[{}]", args);
197 			return;
198 		}
199 
200 		// Get a log friendly value
201 		String oldLogValue = getLogValue(oldParam.get());
202 
203 		// There is a new value for this property and it's different than the old value
204 		if (p.isOverride()) {
205 			// Change it, and log the fact that we are changing it
206 			Object[] args = { prefix, p.getName(), oldLogValue, newLogValue };
207 			logger.info("{}* override - [{}]=[{}] -> [{}]", args);
208 			params.put(p.getName(), p);
209 		} else {
210 			// Ignore it, and log the fact that we are ignoring it
211 			Object[] args = { prefix, p.getName(), newLogValue };
212 			logger.info("{}~ ignore - [{}]=[{}]", args);
213 		}
214 	}
215 
216 	protected String getLogValue(Param param) {
217 		String lcase = param.getName().toLowerCase();
218 		for (String obscurePattern : obscureTokens) {
219 			if (lcase.contains(obscurePattern)) {
220 				return Str.flatten(obscurer.obscure(param.getValue()));
221 			}
222 		}
223 		return Str.flatten(param.getValue());
224 	}
225 
226 	protected Unmarshaller getUnmarshaller() {
227 		try {
228 			JAXBContext context = JAXBContext.newInstance(Config.class);
229 			return context.createUnmarshaller();
230 		} catch (JAXBException e) {
231 			throw new IllegalStateException("Error initializing JAXB for config", e);
232 		}
233 	}
234 
235 	protected Config unmarshal(Unmarshaller unmarshaller, InputStream in) throws IOException {
236 		try {
237 			UnmarshallerHandler unmarshallerHandler = unmarshaller.getUnmarshallerHandler();
238 			SAXParserFactory spf = SAXParserFactory.newInstance();
239 			SAXParser sp = spf.newSAXParser();
240 			XMLReader xr = sp.getXMLReader();
241 			xr.setContentHandler(unmarshallerHandler);
242 			InputSource xmlSource = new InputSource(in);
243 			xr.parse(xmlSource);
244 			return (Config) unmarshallerHandler.getResult();
245 		} catch (SAXException e) {
246 			throw new IllegalStateException("Unexpected SAX error", e);
247 		} catch (ParserConfigurationException e) {
248 			throw new IllegalStateException("Unexpected parser configuration error", e);
249 		} catch (JAXBException e) {
250 			throw new IllegalStateException("Unexpected JAXB error", e);
251 		}
252 	}
253 
254 	protected boolean isPropertiesFile(String location) {
255 		return location.toLowerCase().endsWith(".properties");
256 	}
257 
258 	protected Map<String, Param> convert(Properties properties, boolean override, boolean system) {
259 		Map<String, Param> params = Maps.newHashMap();
260 		SortedSet<String> keys = Sets.newTreeSet(properties.stringPropertyNames());
261 		for (String key : keys) {
262 			String value = properties.getProperty(key);
263 			Param param = Param.builder(key, value).override(override).system(system).build();
264 			params.put(param.getName(), param);
265 		}
266 		return params;
267 	}
268 
269 	protected Properties convert(Map<String, Param> params) {
270 		Properties properties = new Properties();
271 		for (Param p : params.values()) {
272 			properties.setProperty(p.getName(), p.getValue());
273 		}
274 		return properties;
275 	}
276 
277 	protected String getResolvedValue(String prefix, String value, Map<String, Param> params) {
278 		Properties properties = convert(params);
279 		Properties global = PropertyUtils.getGlobalProperties(properties);
280 		String resolvedValue = propertyPlaceholderHelper.replacePlaceholders(value, global);
281 		return resolvedValue;
282 	}
283 
284 	public static Builder builder() {
285 		return new Builder();
286 	}
287 
288 	private RicePropertiesLoader(Builder builder) {
289 		this.propertyPlaceholderHelper = builder.propertyPlaceholderHelper;
290 		this.chainedConfigLocationKey = builder.chainedConfigLocationKey;
291 		this.obscureTokens = ImmutableList.copyOf(builder.obscureTokens);
292 		this.obscurer = builder.obscurer;
293 		this.randomizer = builder.randomizer;
294 		this.systemPropertiesWin = builder.systemPropertiesWin;
295 	}
296 
297 	public static class Builder {
298 
299 		private String chainedConfigLocationKey = "config.location";
300 		private Obscurer obscurer = new DefaultObscurer();
301 		private Randomizer randomizer = DefaultRandomizer.create();
302 		private PropertyPlaceholderHelper propertyPlaceholderHelper = RicePropertyPlaceholderHelper.create();
303 		private boolean systemPropertiesWin = false;
304 		private List<String> obscureTokens = ImmutableList.of("private", "password", "secret", "encryption.key", "accountAccessKey");
305 
306 		public Builder systemPropertiesWin(boolean systemPropertiesWin) {
307 			this.systemPropertiesWin = systemPropertiesWin;
308 			return this;
309 		}
310 
311 		public Builder propertyPlaceholderHelper(PropertyPlaceholderHelper propertyPlaceholderHelper) {
312 			this.propertyPlaceholderHelper = propertyPlaceholderHelper;
313 			return this;
314 		}
315 
316 		public Builder chainedConfigLocationKey(String chainedConfigLocationKey) {
317 			this.chainedConfigLocationKey = chainedConfigLocationKey;
318 			return this;
319 		}
320 
321 		public Builder obscurePatterns(List<String> obscurePatterns) {
322 			this.obscureTokens = obscurePatterns;
323 			return this;
324 		}
325 
326 		public Builder randomizer(Randomizer randomizer) {
327 			this.randomizer = randomizer;
328 			return this;
329 		}
330 
331 		public RicePropertiesLoader build() {
332 			RicePropertiesLoader instance = new RicePropertiesLoader(this);
333 			validate(instance);
334 			return instance;
335 		}
336 
337 		private static void validate(RicePropertiesLoader instance) {
338 			checkNotNull(instance.propertyPlaceholderHelper, "propertyPlaceholderHelper cannot be null");
339 			checkNotNull(instance.obscureTokens, "obscureTokens cannot be null");
340 			checkNotNull(instance.obscurer, "obscurer cannot be null");
341 			checkNotNull(instance.randomizer, "randomizer cannot be null");
342 			checkArgument(!StringUtils.isBlank(instance.chainedConfigLocationKey), "chainedConfigLocationKey cannot be blank");
343 		}
344 
345 		public String getChainedConfigLocationKey() {
346 			return chainedConfigLocationKey;
347 		}
348 
349 		public void setChainedConfigLocationKey(String chainedConfigLocationKey) {
350 			this.chainedConfigLocationKey = chainedConfigLocationKey;
351 		}
352 
353 		public List<String> getObscureTokens() {
354 			return obscureTokens;
355 		}
356 
357 		public void setObscureTokens(List<String> obscureTokens) {
358 			this.obscureTokens = obscureTokens;
359 		}
360 
361 		public Obscurer getObscurer() {
362 			return obscurer;
363 		}
364 
365 		public void setObscurer(Obscurer obscurer) {
366 			this.obscurer = obscurer;
367 		}
368 
369 		public Randomizer getRandomizer() {
370 			return randomizer;
371 		}
372 
373 		public void setRandomizer(Randomizer randomizer) {
374 			this.randomizer = randomizer;
375 		}
376 
377 		public boolean isSystemPropertiesWin() {
378 			return systemPropertiesWin;
379 		}
380 
381 		public void setSystemPropertiesWin(boolean systemPropertiesWin) {
382 			this.systemPropertiesWin = systemPropertiesWin;
383 		}
384 
385 	}
386 
387 	protected void handleRandomParams(Map<String, Param> params) {
388 		List<Param> randoms = getRandomParams(params.values());
389 		for (Param param : randoms) {
390 			String rangeSpec = param.getValue();
391 			int random = randomizer.getInteger(rangeSpec);
392 			String value = Integer.toString(random);
393 			Param newParam = Param.builder(param.getName(), value).build();
394 			params.put(newParam.getName(), newParam);
395 		}
396 	}
397 
398 	protected void handleSystemParams(Map<String, Param> params) {
399 		Properties properties = convert(params);
400 		List<Param> system = getSystemParams(params.values());
401 		for (Param param : system) {
402 			if (isOverrideSystemProperty(param)) {
403 				overrideSystemProperty(param, params, properties);
404 			}
405 		}
406 	}
407 
408 	protected boolean isOverrideSystemProperty(Param param) {
409 
410 		// If the system flag is not set on this param, always return false
411 		if (!param.isSystem()) {
412 			return false;
413 		}
414 
415 		// If we get here, we know the system flag on this parameter is set to true
416 		// We now need to check and see if there is an existing system property
417 		Optional<String> system = Optional.fromNullable(System.getProperty(param.getName()));
418 
419 		if (system.isPresent()) {
420 			// If there is an existing system property, only return true if the override flag is set
421 			return param.isOverride();
422 		} else {
423 			// If there is no existing system property, always return true
424 			return true;
425 		}
426 	}
427 
428 	protected void overrideSystemProperty(Param param, Map<String, Param> params, Properties properties) {
429 		Param resolved = getResolvedParam(param, properties);
430 		if (!resolved.getValue().equals(param.getValue())) {
431 			params.put(resolved.getName(), resolved);
432 			properties.setProperty(resolved.getName(), resolved.getValue());
433 		}
434 		getSystemPropertySetter(resolved).execute();
435 	}
436 
437 	protected Param getResolvedParam(Param param, Properties properties) {
438 		String originalValue = param.getValue();
439 		String resolvedValue = propertyPlaceholderHelper.replacePlaceholders(originalValue, properties);
440 		if (resolvedValue.equals(originalValue)) {
441 			return param;
442 		} else {
443 			return Param.create(param.getName(), resolvedValue);
444 		}
445 	}
446 
447 	protected List<Param> getRandomParams(Collection<Param> params) {
448 		List<Param> list = Lists.newArrayList();
449 		for (Param param : params) {
450 			if (param.isRandom()) {
451 				list.add(param);
452 			}
453 		}
454 		Collections.sort(list);
455 		return list;
456 	}
457 
458 	protected List<Param> getSystemParams(Collection<Param> params) {
459 		List<Param> list = Lists.newArrayList();
460 		for (Param param : params) {
461 			if (param.isSystem()) {
462 				list.add(param);
463 			}
464 		}
465 		Collections.sort(list);
466 		return list;
467 	}
468 
469 	protected Executable getSystemPropertySetter(final Param param) {
470 		Optional<String> system = Optional.fromNullable(System.getProperty(param.getName()));
471 
472 		List<Object> args = ImmutableList.<Object> of(param.getName(), getLogValue(param));
473 		// There is no existing system property - add it and log a message indicating we added it
474 		if (!system.isPresent()) {
475 			String msg = "~ add system property [{}]=[{}]";
476 			return SetSystemPropertyExecutable.builder(param.getName(), param.getValue()).log(msg, args).build();
477 		}
478 
479 		// There is existing system property that is different from the value we want it to be
480 		// Override it, and log a message indicating that it has been overridden
481 		if (system.isPresent() && !system.get().equals(param.getValue())) {
482 			String msg = "* override system property [{}]=[{}]";
483 			return SetSystemPropertyExecutable.builder(param.getName(), param.getValue()).log(msg, args).build();
484 		}
485 
486 		// Noop - existing system property which is exactly the same as the parameter value
487 		return NoOpExecutable.INSTANCE;
488 	}
489 
490 	public PropertyPlaceholderHelper getPropertyPlaceholderHelper() {
491 		return propertyPlaceholderHelper;
492 	}
493 
494 	public String getChainedConfigLocationKey() {
495 		return chainedConfigLocationKey;
496 	}
497 
498 	public ImmutableList<String> getObscureTokens() {
499 		return obscureTokens;
500 	}
501 
502 	public Obscurer getObscurer() {
503 		return obscurer;
504 	}
505 
506 	public Randomizer getRandomizer() {
507 		return randomizer;
508 	}
509 
510 	public boolean isSystemPropertiesWin() {
511 		return systemPropertiesWin;
512 	}
513 
514 }