View Javadoc

1   /*
2    * Copyright 2007-2010 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.rice.core.config;
17  
18  import org.apache.commons.collections.CollectionUtils;
19  import org.apache.commons.lang.StringUtils;
20  import org.apache.log4j.Logger;
21  import org.kuali.rice.core.config.xsd.Param;
22  import org.kuali.rice.core.util.ImmutableProperties;
23  import org.kuali.rice.core.util.RiceUtilities;
24  import org.xml.sax.Attributes;
25  import org.xml.sax.InputSource;
26  import org.xml.sax.SAXException;
27  import org.xml.sax.XMLFilter;
28  import org.xml.sax.helpers.XMLFilterImpl;
29  
30  import javax.xml.bind.JAXBContext;
31  import javax.xml.bind.Unmarshaller;
32  import javax.xml.bind.UnmarshallerHandler;
33  import javax.xml.parsers.SAXParserFactory;
34  import java.io.IOException;
35  import java.io.InputStream;
36  import java.util.*;
37  import java.util.regex.Matcher;
38  import java.util.regex.Pattern;
39  
40  /**
41   * This implementation of the Config interface uses JAXB to parse the config file and
42   * maintains an internal copy of all properties in their "raw" form (without any nested
43   * properties resolved).  This allows properties to be added in stages and still alter
44   * values of properties previously read in.
45   * It also has settings for whether system properties should override all properties or
46   * only serve as default when the property has not been defined.
47   * 
48   * @author Kuali Rice Team (rice.collab@kuali.org)
49   *
50   */
51  public class JAXBConfigImpl extends AbstractBaseConfig {
52  
53      private static final Logger LOG = Logger.getLogger(JAXBConfigImpl.class);
54  
55      private static final String CONFIG_CODED_DEFAULTS = "classpath:org/kuali/rice/core/config-coded-defaults.xml";
56      private static final String IMPORT_NAME = "config.location";
57      private static final String INDENT = "  ";
58      private static final String PLACEHOLDER_REGEX = "\\$\\{([^{}]+)\\}";
59  
60      // keep the same random
61      private static final Random RANDOM = new Random();
62  
63      private final List<String> fileLocs = new ArrayList<String>();
64  
65      private final Map<String, Object> objects = new LinkedHashMap<String, Object>();
66      private final Properties rawProperties = new Properties();
67      private final Properties resolvedProperties = new Properties();
68  
69      // compile pattern for regex once
70      private final Pattern pattern = Pattern.compile(PLACEHOLDER_REGEX);
71  
72      private boolean systemOverride = false;
73      
74      public JAXBConfigImpl(){}
75      
76      public JAXBConfigImpl(org.kuali.rice.core.config.Config config) {
77      	this.copyConfig(config);
78      }
79      
80      public JAXBConfigImpl(String fileLoc, org.kuali.rice.core.config.Config config) {
81      	this.copyConfig(config);
82      	this.fileLocs.add(fileLoc);
83      }
84      
85  	public JAXBConfigImpl(List<String> fileLocs, org.kuali.rice.core.config.Config config) {
86      	this.copyConfig(config);
87      	this.fileLocs.addAll(fileLocs);
88      	
89      }
90      
91      public JAXBConfigImpl(String fileLoc) {
92          this.fileLocs.add(fileLoc);
93      }
94  
95      public JAXBConfigImpl(List<String> fileLocs) {
96          this.fileLocs.addAll(fileLocs);
97      }
98  
99      public JAXBConfigImpl(Properties properties) {    	   
100     	this.putProperties(properties);
101     }
102     
103     public JAXBConfigImpl(String fileLoc, Properties properties) {
104         this.fileLocs.add(fileLoc);    
105         this.putProperties(properties);
106     }
107 
108     public JAXBConfigImpl(List<String> fileLocs, Properties properties) {
109         this.fileLocs.addAll(fileLocs);        
110         this.putProperties(properties);
111     }
112     
113     /*****************************************************/
114 
115     /*
116      * We need the ability to take a config object and copy the raw + cached data into
117      * this config object. 
118      */
119     private void copyConfig(Config config){
120     	if(config == null) return;
121     	
122     	if(config instanceof JAXBConfigImpl) {
123     		this.rawProperties.putAll(((JAXBConfigImpl) config).rawProperties);
124     		this.resolvedProperties.putAll(((JAXBConfigImpl) config).resolvedProperties);
125     	}else{    		
126     		
127     		this.putProperties(config.getProperties());
128     		
129     	}
130     	if(config.getObjects() != null)
131     		this.objects.putAll(config.getObjects());
132     }
133     
134     public Object getObject(String key) {
135         return objects.get(key);
136     }
137 
138     public Map<String, Object> getObjects() {
139     	return Collections.unmodifiableMap(objects);
140     }
141 
142     public Properties getProperties() {
143     	return new ImmutableProperties(resolvedProperties);
144     }
145 
146     public String getProperty(String key) {
147         return resolvedProperties.getProperty(key);
148     }
149 
150     public void overrideProperty(String name, String value) {
151     	this.putProperty(name, value);
152     }
153    
154     /**
155      * 
156      * This overrided the property. Takes the place of the now depricated overrideProperty
157      * 
158      * @see org.kuali.rice.core.config.Config#putProperty(java.lang.String, java.lang.Object)
159      */
160 	public void putProperty(String key, String value) {
161         this.setProperty(key, replaceVariable(key, value));
162         resolveRawToCache();
163 	}
164 
165 	public void putProperties(Properties properties) {
166         if (properties != null) {
167             for(Object o : properties.keySet()) {
168         	    this.setProperty((String)o, replaceVariable((String)o, properties.getProperty((String)o)));
169             }
170             
171     	    resolveRawToCache();
172         }
173     }
174 
175     public void parseConfig() throws IOException {
176 
177         if (fileLocs.size() > 0) {
178 
179             if (LOG.isInfoEnabled()) {
180                 LOG.info("Loading Rice configs: " + StringUtils.join(fileLocs, ", "));
181             }
182 
183             JAXBContext jaxbContext;
184             Unmarshaller unmarshaller;
185 
186             try {
187                 jaxbContext = JAXBContext.newInstance(org.kuali.rice.core.config.xsd.Config.class);
188                 unmarshaller = jaxbContext.createUnmarshaller();
189             } catch (Exception ex) {
190                 throw new ConfigurationException("Error initializing JAXB for config", ex);
191             }
192 
193             // add these first so they can be overridden
194             configureBuiltIns();
195 
196             // parse all config files, but do not resolve any right hand side variables
197             for (String s : fileLocs) {
198                 parseConfig(s, unmarshaller, 0);
199             }
200 
201             // now that all properties have been loaded, resolve the right hand side from
202             // the raw properties into the resolved properties.  This will also replace properties
203             // defined in the files with system properties if systemOverride==true.
204             resolveRawToCache();
205 
206             if (LOG.isInfoEnabled()) {
207             	final StringBuilder log = new StringBuilder();
208             	log.append("\n");
209             	log.append("####################################\n");
210             	log.append("#\n");
211             	log.append("# Properties used after config override/replacement\n");
212             	log.append("# " + StringUtils.join(fileLocs, ", ") + "\n");
213             	log.append("#\n");
214             	log.append("####################################\n");
215                 //commented out to backport to java 5
216                 //SortedSet<String> sorted = new TreeSet<String>(properties.stringPropertyNames());
217                 
218                 SortedSet<String> sorted = new TreeSet<String>();
219                 CollectionUtils.addAll(sorted, rawProperties.propertyNames());
220                 
221                 for (String s : sorted) {
222                 	log.append("Using config Prop " + s + "=[" + ConfigLogger.getDisplaySafeValue(s, this.getProperty(s)) + "]\n");
223                 }
224                 LOG.info(log);
225             }
226 
227         } else {
228             LOG.info("Loading Rice configs: No config files specified");
229         }
230     }
231 
232     protected void parseConfig(String filename, Unmarshaller unmarshaller, int depth) throws IOException {
233 
234         InputStream in = null;
235 
236         // have to check for empty filename because getResource will
237         // return non-null if passed ""
238         if (StringUtils.isNotEmpty(filename)) {
239             in = RiceUtilities.getResourceAsStream(filename);
240         }
241 
242         if (in == null) {
243         	final StringBuilder log = new StringBuilder();
244         	log.append("\n");
245         	log.append("####################################\n");
246         	log.append("#\n");
247         	log.append("# Configuration file '" + filename + "' not found!\n");
248         	log.append("#\n");
249         	log.append("####################################\n");
250         	LOG.warn(log);
251         } else {
252 
253             final String prefix = StringUtils.repeat(INDENT, depth);            
254             LOG.info(prefix + "+ Parsing config: " + filename);            
255             org.kuali.rice.core.config.xsd.Config config;
256 
257             try {
258                 config = unmarshal(unmarshaller, in);
259             } catch (Exception ex) {
260                 throw new ConfigurationException("Error parsing config file: " + filename, ex);
261             }
262 
263             for (Param p : config.getParamList()) {
264 
265                 String name = p.getName();
266                                 
267                 if (name.equals(IMPORT_NAME)) {
268                     String configLocation = parseValue(p.getValue(), new HashSet<String>());
269                     // Remove new lines and white space.
270                     if(configLocation != null){
271                     	configLocation = configLocation.trim();
272                     }
273                     parseConfig(configLocation, unmarshaller, depth + 1);
274                 } else if(p.isSystem()){
275                 	if (p.isOverride() || !(System.getProperty(name) != null)){
276                 		if(p.isRandom()){
277                 			String randStr = String.valueOf(generateRandomInteger(p.getValue()));
278                 			System.setProperty(name, randStr);
279                             this.setProperty(p.getName(), randStr); 
280                         	if(LOG.isInfoEnabled())
281                         	{	
282                         		LOG.info("generating random string " + randStr + " for system property " + p.getName());
283                         	}    
284                 		}else{
285                 			// resolve and set system params immediately so they can override
286                             // existing system params. Add to rawProperties resolved as well to
287                             // prevent possible mismatch
288                             HashSet<String> set = new HashSet<String>();
289                             set.add(p.getName());
290                             String value = parseValue(p.getValue(), set);
291                             System.setProperty(name, value);
292                             this.setProperty(name, value);
293                 		}
294                 	}
295                 }
296                 else if (p.isOverride() || !rawProperties.containsKey(name)) {
297 
298                 	if (p.isRandom()) {
299                     
300                     	String randStr = String.valueOf(generateRandomInteger(p.getValue()));
301                         this.setProperty(p.getName(), randStr); 
302                     	if(LOG.isInfoEnabled())
303                     	{	
304                     		LOG.info("generating random string " + randStr + " for property " + p.getName());
305                     	}
306                     } else {
307                     	
308                     	/*
309                     	 * myProp = dog
310                     	 * We have a case where you might want myProp = ${myProp}:someOtherStuff:${foo}
311                     	 * This would normally overwrite the existing myProp with ${myProp}:someOtherStuff:${foo}
312                     	 * but what we want is:
313                     	 * myProp = dog:someOtherStuff:${foo}
314                     	 * so that we put the existing value of myProp into the new value. Basically how path works.
315                     	 */
316                     	String value = replaceVariable(name, p.getValue());                       
317                     	
318                     	this.setProperty(name, value);                    	
319                     }
320                 }
321             }
322 
323             LOG.info(prefix + "- Parsed config: " + filename);
324         }
325     }
326     
327     /*
328      * This will set the property. No logic checking so what you pass in gets set.
329      * We use this as a focal point for debugging the raw config changes.
330      */
331     protected void setProperty(String name, String value){
332     	if(LOG.isInfoEnabled()){
333     		String oldProp = rawProperties.getProperty(name);
334     		if(oldProp != null && !oldProp.equals(value)){
335     			LOG.info("Raw Config Override: " + name + "=[" + ConfigLogger.getDisplaySafeValue(name,oldProp) +"]->[" + ConfigLogger.getDisplaySafeValue(name,value) +"]");
336     		}
337     	}
338     	rawProperties.setProperty(name, value);
339     }    
340 
341     protected String resolve(String key) {
342     	return resolve(key, null);
343     }
344     
345     /**
346      * This method will determine the value for a property by looking it up in the raw properties.  If the
347      * property value contains a nested property (foo=${nested}) it will start the recursion by
348      * calling parseValue().
349      * It will also check for a system property of the same name and, based on the value of systemOverride,
350      * 'override with' the system property or 'default to' the system property if not found in the raw properties.
351      * This method only determines the resolved value, it does not modify the properties in the resolved or raw
352      * properties objects.
353      * 
354      * @param key they key of the property for which to determine the value
355      * @param keySet contains all keys used so far in this recursion.  used to check for circular references.
356      * @return
357      */
358     protected String resolve(String key, Set keySet) {
359     	
360         // check if we have already resolved this key and have circular reference
361         if (keySet != null && keySet.contains(key)) {
362             throw new ConfigurationException("Circular reference in config: " + key);
363         }
364         
365         String value = this.rawProperties.getProperty(key);
366         
367         if ((value == null || systemOverride) && System.getProperties().containsKey(key)) {
368             value = System.getProperty(key);
369         }
370         
371         if (value != null && value.contains("${")) {
372         	if(keySet == null) {
373         		keySet = new HashSet<String>();
374         	}
375             keySet.add(key);
376 
377             value = parseValue(value, keySet);
378             
379             keySet.remove(key);
380         }
381         
382         if(value == null) {
383         	value = "";
384         	LOG.warn("Property key: '" + key + "' is not available and hence set to empty");
385         }
386 
387         return value;
388     }
389  
390     /**
391      * This method parses the value string to find all nested properties (foo=${nested}) and
392      * replaces them with the value returned from calling resolve().  It does this in a new
393      * string and does not modify the raw or resolved properties objects.
394      * 
395      * @param value the string to search for nest properties
396      * @param keySet contains all keys used so far in this recursion.  used to check for circular references.
397      * @return
398      */
399     protected String parseValue(String value, Set<String> keySet) {
400         String result = value;
401 
402         Matcher matcher = pattern.matcher(value);
403 
404         while (matcher.find()) {
405 
406             // get the first, outermost ${} in the string.  removes the ${} as well.
407             String key = matcher.group(1);
408 
409             String resolved = resolve(key, keySet);
410 
411             result = matcher.replaceFirst(Matcher.quoteReplacement(resolved));
412             matcher = matcher.reset(result);
413         }
414 
415         return result;
416     }
417     
418     
419     /**
420      * This method is used when reading in new properties to check if there is a direct reference to the
421      * key in the value.  This emulates operating system environment variable setting behavior 
422      * and replaces the reference in the value with the current value of the property from the rawProperties.
423      * <pre>
424      * ex:
425      * path=/usr/bin;${someVar}
426      * path=${path};/some/other/path
427      * 
428      * resolves to:
429      * path=/usr/bin;${someVar};/some/other/path
430      * </pre>
431      * 
432      * It does not resolve the the value from rawProperties as it could contain nested properties that might change later.
433      * If the property does not exist in the rawProperties it will check for a default system property now to
434      * prevent a circular reference error.
435      * 
436      * @param name the property name
437      * @param value the value to check for nested property of the same name
438      * @return
439      */
440     protected String replaceVariable(String name, String value){
441     	String regex = "(?:\\$\\{"+ name +"\\})";
442     	String temporary = null;
443     	
444     	// Look for a property in the map first and use that.  If system override is true
445     	// then it will get overridden during the resolve phase.  If the value is null
446     	// we need to check the system now so we don't throw an error.
447     	if(value.contains("${" + name + "}")) {
448     		if( (temporary = rawProperties.getProperty(name)) == null ) {
449     			temporary = System.getProperty(name);
450     		}
451     		
452     		if(temporary != null) {
453     			return value.replaceAll(regex,  Matcher.quoteReplacement(temporary));
454     		}
455     	}   
456     	
457     	return value;
458     }
459     
460     /**
461      * This method iterates through the raw properties and stores their resolved values in the
462      * resolved properties map, which acts as a cache so we don't have to run the recursion every
463      * time getProperty() is called.
464      */
465     protected void resolveRawToCache() {
466     	if(rawProperties.size() > 0) {
467     		Properties oldProps = new Properties(new ImmutableProperties(resolvedProperties));  
468     		//oldProps.putAll(new ImmutableProperties(resolvedProperties));
469     		resolvedProperties.clear();
470     		
471     		for(Object o : rawProperties.keySet()) {    			
472     			String resolved = resolve((String)o);
473     			
474     			if(LOG.isInfoEnabled()){
475     				String oldResolved = oldProps.getProperty((String)o);
476     				if(oldResolved != null && !oldResolved.equals(resolved)){
477     					String key = (String)o;
478     					String unResolved = rawProperties.getProperty(key);
479     					
480     					if(unResolved.contains("$")){
481     						LOG.info("Resolved Config Override: " + key + "(" + unResolved +")=[" + ConfigLogger.getDisplaySafeValue(key,oldResolved) +"]->[" + ConfigLogger.getDisplaySafeValue(key,resolved) +"]");     					
482     					}else{
483     						LOG.info("Resolved Config Override: " + key + "=[" + ConfigLogger.getDisplaySafeValue(key,oldResolved) +"]->[" + ConfigLogger.getDisplaySafeValue(key,resolved) +"]"); 
484     					}    					
485     				}
486     			}    			
487     			resolvedProperties.setProperty((String)o, resolved);
488     		}
489     	}
490     }
491 
492     /**
493      * Configures built-in properties.
494      */
495     protected void configureBuiltIns() {
496     	this.setProperty("host.ip", RiceUtilities.getIpNumber());
497     	this.setProperty("host.name", RiceUtilities.getHostName());
498     }
499 
500     /**
501      * Generates a random integer in the range specified by the specifier, in the format: min-max
502      * 
503      * @param rangeSpec
504      *            a range specification, 'min-max'
505      * @return a random integer in the range specified by the specifier, in the format: min-max
506      */
507     protected int generateRandomInteger(String rangeSpec) {
508         String[] range = rangeSpec.split("-");
509         if (range.length != 2) {
510             throw new RuntimeException("Invalid range specifier: " + rangeSpec);
511         }
512         int from = Integer.parseInt(range[0].trim());
513         int to = Integer.parseInt(range[1].trim());
514         if (from > to) {
515             int tmp = from;
516             from = to;
517             to = tmp;
518         }
519         int num;
520         // not very random huh...
521         if (from == to) {
522             num = from;
523             if(LOG.isInfoEnabled())
524             {
525             	LOG.info("from==to, so not generating random value for property.");
526             }
527         } else {
528             num = from + RANDOM.nextInt((to - from) + 1);
529         }
530         return num;
531     }
532     
533     public boolean isSystemOverride() {
534         return systemOverride;
535     }
536     
537     /**
538      * If set to true then system properties will always be checked first, disregarding
539      * any values in the config.
540      * 
541      * The default is false.
542      * 
543      * @param systemOverride
544      */
545     public void setSystemOverride(boolean systemOverride) {
546         this.systemOverride = systemOverride;
547     }
548 
549     protected org.kuali.rice.core.config.xsd.Config unmarshal(Unmarshaller unmarshaller, InputStream in) throws Exception {
550         SAXParserFactory spf = SAXParserFactory.newInstance();
551         spf.setNamespaceAware(true);
552 
553         XMLFilter filter = new ConfigNamespaceURIFilter();
554         filter.setParent(spf.newSAXParser().getXMLReader());
555 
556         UnmarshallerHandler handler = unmarshaller.getUnmarshallerHandler();
557         filter.setContentHandler(handler);
558 
559         filter.parse(new InputSource(in));
560 
561         return (org.kuali.rice.core.config.xsd.Config)handler.getResult();
562     }
563 
564     /**
565      *  This is a SAX filter that adds the config xml namespace to the document if the document
566      *  does not have a namespace (for backwards compatibility).  This filter assumes unqualified
567      *  attributes and does not modify their namespace (if any).
568      *  
569      *   This could be broken out into a more generic class if Rice makes more use of JAXB.
570      * 
571      * @author Kuali Rice Team (kuali-rice@googlegroups.com)
572      *
573      */
574     public class ConfigNamespaceURIFilter extends XMLFilterImpl {
575 
576         public static final String CONFIG_URI="http://rice.kuali.org/xsd/core/config";
577         
578         public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
579             if(StringUtils.isBlank(uri)) {
580                 uri = CONFIG_URI;
581             }
582             
583             super.startElement(uri, localName, qName, atts);
584         }
585 
586         public void endElement(String uri, String localName, String qName) throws SAXException {
587             if(StringUtils.isBlank(uri)) {
588                 uri = CONFIG_URI;
589             }
590             
591             super.endElement(uri, localName, qName);
592         }
593     }
594 
595 	
596 	public void putObject(String key, Object value) {
597 		this.objects.put(key, value);		
598 	}
599 	
600 	public void putObjects(Map<String, Object> objects) {
601 		this.objects.putAll(objects);	
602 	}
603 	
604 	public void removeObject(String key){
605 		this.objects.remove(key);
606 	}
607 	
608 	public void removeProperty(String key){
609 		this.rawProperties.remove(key);
610     	    	
611     	resolveRawToCache();
612 	}
613 
614 	public void putConfig(Config config) {
615 		this.copyConfig(config);
616 	}
617 	
618     
619 }