View Javadoc

1   /**
2    * Copyright (C) 2005-2009 Alfresco Software Limited.
3    *
4    * This file is part of the Spring Surf Extension project.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  package org.springframework.extensions.config.xml;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.util.ArrayList;
24  import java.util.HashMap;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Properties;
29  
30  import org.apache.commons.logging.Log;
31  import org.apache.commons.logging.LogFactory;
32  import org.dom4j.Document;
33  import org.dom4j.Element;
34  import org.dom4j.io.SAXReader;
35  import org.springframework.beans.BeansException;
36  import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
37  import org.springframework.core.Constants;
38  import org.springframework.core.io.Resource;
39  import org.springframework.extensions.config.BaseConfigService;
40  import org.springframework.extensions.config.ConfigDeployer;
41  import org.springframework.extensions.config.ConfigDeployment;
42  import org.springframework.extensions.config.ConfigElement;
43  import org.springframework.extensions.config.ConfigException;
44  import org.springframework.extensions.config.ConfigSection;
45  import org.springframework.extensions.config.ConfigSectionImpl;
46  import org.springframework.extensions.config.ConfigSource;
47  import org.springframework.extensions.config.evaluator.Evaluator;
48  import org.springframework.extensions.config.xml.elementreader.ConfigElementReader;
49  import org.springframework.extensions.config.xml.elementreader.GenericElementReader;
50  import org.springframework.util.PropertyPlaceholderHelper;
51  import org.springframework.util.StringValueResolver;
52  
53  /**
54   * XML based configuration service.
55   * <p/>
56   * The sytem properties can be used; to override entries in the properties files, act as fallback values or be ignored.
57   * <ul>
58   *   <li><b>SYSTEM_PROPERTIES_MODE_NEVER:             </b>Don't use system properties at all.</li>
59   *   <li><b>SYSTEM_PROPERTIES_MODE_FALLBACK:          </b>Fallback to a system property only for undefined properties.</li>
60   *   <li><b>SYSTEM_PROPERTIES_MODE_OVERRIDE: (DEFAULT)</b>Use a system property if it is available.</li>
61   * </ul>
62   * 
63   * @author gavinc
64   */
65  public class XMLConfigService extends BaseConfigService implements XMLConfigConstants
66  {
67      private static final Log logger = LogFactory.getLog(XMLConfigService.class);
68  
69      private static final Constants constants = new Constants(PropertyPlaceholderConfigurer.class);
70  
71      private Resource[] propertyLocations;
72      private int systemPropertiesMode = PropertyPlaceholderConfigurer.SYSTEM_PROPERTIES_MODE_OVERRIDE;
73      private PropertyConfigurer propertyConfigurer;
74      private Map<String, ConfigElementReader> elementReaders;
75      
76      /**
77       * Constructs an XMLConfigService using the given config source
78       * 
79       * @param configSource
80       *            A ConfigSource
81       */
82      public XMLConfigService(ConfigSource configSource)
83      {
84          super(configSource);
85      }
86  
87  	/**
88  	 * Set locations of properties files to be loaded.
89  	 * <p>Can point to classic properties files or to XML files
90  	 * that follow JDK 1.5's properties XML format.
91  	 */
92  	public void setProperties(Resource[] locations)
93  	{
94  	    this.propertyLocations = locations;
95  	}
96  
97      /**
98       * Set the system property mode by the name of the corresponding constant,
99       * e.g. "SYSTEM_PROPERTIES_MODE_OVERRIDE".
100      * @param constantName name of the constant
101      * @throws java.lang.IllegalArgumentException if an invalid constant was specified
102      * @see #setSystemPropertiesMode
103      */
104     public void setSystemPropertiesModeName(String constantName) throws IllegalArgumentException
105     {
106         this.systemPropertiesMode = constants.asNumber(constantName).intValue();
107     }
108 
109 	public List<ConfigDeployment> initConfig()
110     {
111         if (logger.isDebugEnabled())
112             logger.debug("Commencing initialisation");
113 
114         List<ConfigDeployment> configDeployments = super.initConfig();
115 
116         // initialise property configurer
117         propertyConfigurer = null;
118         if (propertyLocations != null)
119         {
120            PropertyConfigurer configurer = new PropertyConfigurer();
121            configurer.setLocations(propertyLocations);
122            configurer.setIgnoreUnresolvablePlaceholders(true);
123            configurer.setSystemPropertiesMode(systemPropertiesMode);
124            configurer.init();
125            propertyConfigurer = configurer;
126         }
127 
128         // initialise the element readers map with built-in readers
129         putElementReaders(new HashMap<String, ConfigElementReader>());
130 
131         List<ConfigDeployment> deployments = parse();
132         configDeployments.addAll(deployments);
133                 
134     	// append additional config, if any
135         for (ConfigDeployer configDeployer : configDeployers)
136         {
137         	deployments = configDeployer.initConfig();
138         	configDeployments.addAll(deployments);
139         }
140         
141         if (logger.isDebugEnabled())
142             logger.debug("Completed initialisation");
143         
144         return configDeployments;
145     }
146     
147     public void destroy()
148     {
149         removeElementReaders();
150         super.destroy();
151     }
152 
153     protected void parse(InputStream stream)
154     {
155     	Map<String, ConfigElementReader> parsedElementReaders = null;
156     	Map<String, Evaluator> parsedEvaluators = null;
157     	List<ConfigSection> parsedConfigSections = new ArrayList<ConfigSection>();
158     	
159     	String currentArea = null;
160         try
161         {
162             // get the root element
163             SAXReader reader = new SAXReader();
164             Document document = reader.read(stream);
165             Element rootElement = document.getRootElement();
166 
167             // see if there is an area defined
168             currentArea = rootElement.attributeValue("area");
169 
170             // parse the plug-ins section of a config file
171             Element pluginsElement = rootElement.element(ELEMENT_PLUG_INS);
172             if (pluginsElement != null)
173             {
174                 // parse the evaluators section
175             	parsedEvaluators = parseEvaluatorsElement(pluginsElement.element(ELEMENT_EVALUATORS));
176 
177                 // parse the element readers section
178             	parsedElementReaders = parseElementReadersElement(pluginsElement.element(ELEMENT_ELEMENT_READERS));
179             }
180 
181             // parse each config section in turn
182             @SuppressWarnings("unchecked")
183             Iterator<Element> configElements = rootElement.elementIterator(ELEMENT_CONFIG);
184             while (configElements.hasNext())
185             {
186                 Element configElement = configElements.next();
187                 parsedConfigSections.add(parseConfigElement(parsedElementReaders, configElement, currentArea));
188             }
189         }
190         catch (Throwable e)
191         {
192             if (e instanceof ConfigException)
193             {
194                throw (ConfigException)e;
195             }
196             else
197             {
198                throw new ConfigException("Failed to parse config stream", e);
199             }
200         }
201         
202         try 
203         {
204 	        // valid for this stream, now add to config service ...
205 	        
206 	        if (parsedEvaluators != null)
207 	        {
208 		        for (Map.Entry<String, Evaluator> entry : parsedEvaluators.entrySet())
209 		        {
210 		        	// add the evaluators to the config service
211 		        	addEvaluator(entry.getKey(), entry.getValue());
212 		        }
213 	        }
214 	        
215 	        if (parsedElementReaders != null)
216 	        {
217 		        for (Map.Entry<String, ConfigElementReader> entry : parsedElementReaders.entrySet())
218 		        {
219 		        	// add the element readers to the config service
220 		        	addConfigElementReader(entry.getKey(), entry.getValue());
221 		        }
222 	        }
223 	        
224 	        if (parsedConfigSections != null)
225 	        {
226 	        	for (ConfigSection section : parsedConfigSections)
227 		        {
228 	        		// add the config sections to the config service
229 	        		addConfigSection(section, currentArea);
230 		        }
231 	        }
232 	    }
233 	    catch (Throwable e)
234 	    {
235 	        throw new ConfigException("Failed to add config to config service", e);
236 	    }
237     }
238 
239 
240     /**
241      * Parses the evaluators element
242      * 
243      * @param evaluatorsElement
244      */
245     private Map<String, Evaluator> parseEvaluatorsElement(Element evaluatorsElement)
246     {
247         if (evaluatorsElement != null)
248         {
249             Map<String, Evaluator> parsedEvaluators = new HashMap<String, Evaluator>();
250             @SuppressWarnings("unchecked")
251             Iterator<Element> evaluators = evaluatorsElement.elementIterator();
252             while (evaluators.hasNext())
253             {
254                 Element evaluatorElement = evaluators.next();
255                 String evaluatorName = evaluatorElement.attributeValue(ATTR_ID);
256                 String evaluatorClass = evaluatorElement.attributeValue(ATTR_CLASS);
257 
258                 // TODO: Can these checks be removed if we use a DTD and/or
259                 // schema??
260                 if (evaluatorName == null || evaluatorName.length() == 0)
261                 {
262                     throw new ConfigException("All evaluator elements must define an id attribute");
263                 }
264 
265                 if (evaluatorClass == null || evaluatorClass.length() == 0)
266                 {
267                     throw new ConfigException("Evaluator '" + evaluatorName + "' must define a class attribute");
268                 }
269 
270                 // add the evaluator
271                 parsedEvaluators.put(evaluatorName, createEvaluator(evaluatorName, evaluatorClass));
272             }
273             
274             return parsedEvaluators;
275         }
276         
277         return null;
278     }
279 
280     /**
281      * Parses the element-readers element
282      * 
283      * @param readersElement
284      */
285     private Map<String, ConfigElementReader> parseElementReadersElement(Element readersElement)
286     {
287         if (readersElement != null)
288         {
289             Map<String, ConfigElementReader> parsedElementReaders = new HashMap<String, ConfigElementReader>();
290             @SuppressWarnings("unchecked")
291             Iterator<Element> readers = readersElement.elementIterator();
292             while (readers.hasNext())
293             {
294                 Element readerElement = readers.next();
295                 String readerElementName = readerElement.attributeValue(ATTR_ELEMENT_NAME);
296                 String readerElementClass = readerElement.attributeValue(ATTR_CLASS);
297 
298                 if (readerElementName == null || readerElementName.length() == 0)
299                 {
300                     throw new ConfigException("All element-reader elements must define an element-name attribute");
301                 }
302 
303                 if (readerElementClass == null || readerElementClass.length() == 0)
304                 {
305                     throw new ConfigException("Element-reader '" + readerElementName
306                             + "' must define a class attribute");
307                 }
308 
309                 // add the element reader
310                 parsedElementReaders.put(readerElementName, createConfigElementReader(readerElementName, readerElementClass));
311             }
312             
313             return parsedElementReaders;
314         }
315         
316         return null;
317     }
318 
319     /**
320      * Parses a config element of a config file
321      * 
322      * @param configElement The config element
323      * @param currentArea The current area
324      */
325     private ConfigSection parseConfigElement(Map<String, ConfigElementReader> parsedElementReaders, Element configElement, String currentArea)
326     {
327         if (configElement != null)
328         {
329             boolean replace = false;
330             String evaluatorName = configElement.attributeValue(ATTR_EVALUATOR);
331             String condition = configElement.attributeValue(ATTR_CONDITION);
332             String replaceValue = configElement.attributeValue(ATTR_REPLACE);
333             if (replaceValue != null && replaceValue.equalsIgnoreCase("true"))
334             {
335                replace = true;
336             }
337 
338             // create the section object
339             ConfigSectionImpl section = new ConfigSectionImpl(evaluatorName, condition, replace);
340 
341             // retrieve the config elements for the section
342             @SuppressWarnings("unchecked")
343             Iterator<Element> children = configElement.elementIterator();
344             while (children.hasNext())
345             {
346                 Element child = children.next();
347                 String elementName = child.getName();
348 
349                 // get the element reader for the child
350                 ConfigElementReader elementReader = null;
351                 if (parsedElementReaders != null)
352                 {
353                 	elementReader = parsedElementReaders.get(elementName);
354                 }
355                 
356                 if (elementReader == null)
357                 {
358                 	elementReader = getConfigElementReader(elementName);
359                 }
360                 
361                 if (logger.isDebugEnabled())
362                     logger.debug("Retrieved element reader " + elementReader + " for element named '" + elementName
363                             + "'");
364 
365                 if (elementReader == null)
366                 {
367                     elementReader = new GenericElementReader(propertyConfigurer);
368 
369                     if (logger.isDebugEnabled())
370                         logger.debug("Defaulting to " + elementReader + " as there wasn't an element "
371                                 + "reader registered for element '" + elementName + "'");
372                 }
373 
374                 ConfigElement cfgElement = elementReader.parse(child);
375                 section.addConfigElement(cfgElement);
376 
377                 if (logger.isDebugEnabled())
378                     logger.debug("Added " + cfgElement + " to " + section);
379             }
380             
381             return section;
382         }
383         
384         return null;
385     }
386 
387     /**
388      * Adds the config element reader to the config service
389      * 
390      * @param name
391      *            Name of the element
392      * @param elementReader
393      *            The element reader
394      */
395     private void addConfigElementReader(String elementName, ConfigElementReader elementReader)
396     {
397         putConfigElementReader(elementName, elementReader);
398 
399         if (logger.isDebugEnabled())
400             logger.debug("Added element reader '" + elementName + "': " + elementReader.getClass().getName());
401     }
402     
403     /**
404      * Instantiate the config element reader with the given name and class
405      * 
406      * @param name
407      *            Name of the element
408      * @param className
409      *            Class name of the element reader
410      */
411     private ConfigElementReader createConfigElementReader(String elementName, String className)
412     {
413         ConfigElementReader elementReader = null;
414 
415         try
416         {
417             @SuppressWarnings("unchecked")
418             Class clazz = Class.forName(className);
419             elementReader = (ConfigElementReader) clazz.newInstance();
420         }
421         catch (Throwable e)
422         {
423             throw new ConfigException("Could not instantiate element reader for '" + elementName + "' with class: "
424                     + className, e);
425 
426         }
427 
428         return elementReader;
429     }
430 
431     /**
432      * Gets the element reader from the in-memory 'cache' for the given element name
433      * 
434      * @param elementName Name of the element to get the reader for
435      * @return ConfigElementReader object or null if it doesn't exist
436      */
437     private ConfigElementReader getConfigElementReader(String elementName)
438     {
439         return (ConfigElementReader) getElementReaders().get(elementName);
440     }
441     
442     /**
443      * Put the config element reader into the in-memory 'cache' for the given element name
444      * 
445      * @param elementName
446      * @param elementReader
447      */
448     private void putConfigElementReader(String elementName, ConfigElementReader elementReader)
449     {
450         getElementReaders().put(elementName, elementReader);
451     }
452     
453     /**
454      * Get the elementReaders from the in-memory 'cache'
455      * 
456      * @return elementReaders
457      */
458     protected Map<String, ConfigElementReader> getElementReaders()
459     {
460         return elementReaders;
461     }  
462     
463     /**
464      * Put the elementReaders into the in-memory 'cache'
465      * 
466      * @param elementReaders
467      */
468     protected void putElementReaders(Map<String, ConfigElementReader> elementReaders)
469     {
470         this.elementReaders = elementReaders;
471     }  
472     
473     /**
474      * Remove the elementReaders from the in-memory 'cache'
475      */
476     protected void removeElementReaders()
477     {
478         elementReaders.clear();
479         elementReaders = null;
480     } 
481         
482     /**
483      * Provides access to property values 
484      */
485     public static class PropertyConfigurer extends PropertyPlaceholderConfigurer
486     {
487 		private PlaceholderResolvingStringValueResolver resolver;
488         
489         /**
490          * Initialise
491          */
492         /*package*/ void init()
493         {
494             try
495             {
496                 Properties properties = mergeProperties();
497                 this.resolver = new PlaceholderResolvingStringValueResolver(properties, DEFAULT_PLACEHOLDER_PREFIX, DEFAULT_PLACEHOLDER_SUFFIX, DEFAULT_VALUE_SEPARATOR, true);
498             }
499             catch(IOException e)
500             {
501                 throw new ConfigException("Failed to retrieve properties", e);
502             }
503         }
504 
505         /**
506          * Resolve values
507          * 
508          * @param value
509          * @return resolved value
510          */
511         public String resolveValue(String val)
512         {
513         	return resolver.resolveStringValue(val);
514         }
515     }
516     
517 	/**
518 	 * BeanDefinitionVisitor that resolves placeholders in String values,
519 	 * delegating to the <code>parseStringValue</code> method of the
520 	 * containing class.
521 	 */
522     public static class PlaceholderResolvingStringValueResolver implements StringValueResolver
523     {
524 		private final PropertyPlaceholderHelper helper;
525 		private final Properties props;
526 
527 		public PlaceholderResolvingStringValueResolver(Properties props, String placeholderPrefix, String placeholderSuffix, String valueSeparator, boolean ignoreUnresolvablePlaceholders) 
528 		{
529 			this.helper = new PropertyPlaceholderHelper(placeholderPrefix, placeholderSuffix, valueSeparator, ignoreUnresolvablePlaceholders);
530 			this.props = props;
531 		}
532 
533 		public String resolveStringValue(String strVal) throws BeansException 
534 		{
535 			String value = this.helper.replacePlaceholders(strVal, props);
536 			return value;
537 		}
538 	}
539 }