View Javadoc

1   /*
2    * Copyright 2007-2008 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.lang.StringUtils;
19  import org.apache.commons.lang.text.StrLookup;
20  import org.apache.commons.lang.text.StrSubstitutor;
21  import org.apache.log4j.Logger;
22  import org.kuali.rice.core.util.RiceUtilities;
23  import org.kuali.rice.core.util.XmlJotter;
24  import org.w3c.dom.Document;
25  import org.w3c.dom.Element;
26  import org.w3c.dom.Node;
27  import org.w3c.dom.NodeList;
28  import org.xml.sax.SAXException;
29  
30  import javax.xml.parsers.DocumentBuilderFactory;
31  import javax.xml.parsers.ParserConfigurationException;
32  import javax.xml.transform.TransformerException;
33  import java.io.IOException;
34  import java.io.InputStream;
35  import java.util.LinkedHashMap;
36  import java.util.Map;
37  import java.util.Random;
38  
39  /**
40   * ConfigParser implementation that supports a hierarchy of configs, in which 
41   * configs can include other configs.  Variable tokens are resolved at parse time,
42   * in the order in which they are encountered.  This class relies on Spring for resource
43   * loading and Apache Commons Lang for variable replacement.
44   *
45   * @author Kuali Rice Team (rice.collab@kuali.org)
46   */
47  public class ConfigParserImpl implements ConfigParser {
48      // keep the same random
49      private static final Random RANDOM = new Random();
50  
51      private static final Logger LOG = Logger.getLogger(ConfigParserImpl.class);
52      private static final String IMPORT_NAME = "config.location";
53      private static final String PARAM_NAME= "param";
54      private static final String NAME_ATTR = "name";
55      private static final String OVERRIDE_ATTR = "override";
56      private static final String RANDOM_ATTR = "random";
57      private static final String INDENT = "  ";
58      
59      public static final String ALTERNATE_BUILD_LOCATION_KEY = "alt.build.location";
60  
61      /**
62       * A StrLookup implementation that delegates to System properties if the key is not
63       * found in the supplied map.
64       * @author Kuali Rice Team (rice.collab@kuali.org)
65       */
66      private static class SystemPropertiesDelegatingStrLookup extends StrLookup {
67          private final Map map;
68          private SystemPropertiesDelegatingStrLookup(Map map) {
69              this.map = map;
70          }
71          @Override
72          public String lookup(String key) {
73              Object o = map.get(key);
74              if (o != null) {
75                  return String.valueOf(o);
76              } else {
77                  String s = System.getProperty(key);
78                  if (s != null) {
79                      return s;
80                  } else {
81                      // implement behavior for missing property here...e.g. return ""
82                      // returning null will result in the substitutor not substituting
83                      return "";
84                  }
85              }
86          }
87      }
88  
89      /**
90       * @see org.kuali.rice.core.config.ConfigParser#parse(java.lang.String[])
91       */
92      public void parse(Map props, String[] locations) throws IOException {
93          LinkedHashMap params = new LinkedHashMap();
94          params.putAll(props);
95          parse(params, locations);
96          props.putAll(params);
97      }
98  
99      /**
100      * Parses a list of locations
101      * @param params the current parameter map
102      * @param locations a list of locations to parse
103      * @throws IOException
104      */
105     protected void parse(LinkedHashMap<String, Object> params, String[] locations) throws IOException {
106         StrSubstitutor subs = new StrSubstitutor(new SystemPropertiesDelegatingStrLookup(params));
107         for (String location: locations) {
108             parse(params, location, subs, 0);
109         }
110     }
111 
112     /**
113      * Parses a single config location
114      * @param params the current parameter map
115      * @param location the location to parse
116      * @param subs a StrSubstitutor used to substitute variable tokens
117      * @throws IOException
118      */
119     protected void parse(LinkedHashMap<String, Object> params, String location, StrSubstitutor subs, int depth) throws IOException {
120         InputStream configStream = RiceUtilities.getResourceAsStream(location);
121         if (configStream == null) {
122             LOG.warn("###############################");
123             LOG.warn("#");
124             LOG.warn("# Configuration file '" + location + "' not found!");
125             LOG.warn("#");
126             LOG.warn("###############################");
127             return;
128         }
129         
130         final String prefix = StringUtils.repeat(INDENT, depth); 
131         LOG.info(prefix + "+ Parsing config: " + location);
132 
133         Document doc;
134         try {
135             doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(configStream);
136             if (LOG.isDebugEnabled()) {
137                 LOG.debug("Contents of config " + location + ": \n" + XmlJotter.jotNode(doc, true));
138             }
139         } catch (SAXException se) {
140             IOException ioe = new IOException("Error parsing config resource: " + location);
141             ioe.initCause(se);
142             throw ioe;
143         } catch (ParserConfigurationException pce) {
144             IOException ioe = new IOException("Unable to obtain document builder");
145             ioe.initCause(pce);
146             throw ioe;
147         } finally {
148             configStream.close();
149         }
150 
151         Element root = doc.getDocumentElement();
152         // ignore the actual type of the document element for now
153         // so that plugin descriptors can be parsed
154         NodeList list = root.getChildNodes();
155         StringBuilder content = new StringBuilder();
156         for (int i = 0; i < list.getLength(); i++) {
157             Node node = list.item(i);
158             if (node.getNodeType() != Node.ELEMENT_NODE)
159                 continue;
160             if (!PARAM_NAME.equals(node.getNodeName())) {
161                 LOG.warn("Encountered non-param config node: " + node.getNodeName());
162                 continue;
163             }
164             Element param = (Element) node;
165             String name = param.getAttribute(NAME_ATTR);
166             if (name == null) {
167                 LOG.error("Unnamed parameter in config resource '" + location + "': " + XmlJotter.jotNode(param));
168                 continue;
169             }
170             Boolean override = Boolean.TRUE;
171             String overrideVal = param.getAttribute(OVERRIDE_ATTR);
172             if (!StringUtils.isEmpty(overrideVal)) {
173                 override = Boolean.valueOf(overrideVal);
174             }
175 
176             content.setLength(0);
177             // accumulate all content (preserving any XML content)
178             getNodeValue(name, location, param, content);
179             String value = subs.replace(content);
180             if (LOG.isDebugEnabled()) {
181                 LOG.debug(prefix + INDENT + "* " + name + "=[" + ConfigLogger.getDisplaySafeValue(name, value) + "]");
182             }
183 
184             if (IMPORT_NAME.equals(name)) {
185                 // what is this...we don't follow a string with this substring in it? i.e. if the value does not
186                 // resolve then don't try to follow it (it won't find it anyway; this is the case for any path
187                 // with unresolved params...)?
188                 if (!value.contains(ALTERNATE_BUILD_LOCATION_KEY)) {
189                     parse(params, value, subs, depth + 1);
190                 }
191             } else {
192                 if (Boolean.valueOf(param.getAttribute(RANDOM_ATTR))) {
193                     // this is a special type of property whose value is a randomly generated number in the range specified
194                     value = String.valueOf(generateRandomInteger(value));   
195                 }
196                 setParam(params, override, name, value, prefix + INDENT);
197             }
198         }
199         LOG.info(prefix + "- Parsed config: " + location);
200     }
201     
202     /**
203      * Generates a random integer in the range specified by the specifier, in the format: min-max
204      * @param rangeSpec a range specification, 'min-max'
205      * @return a random integer in the range specified by the specifier, in the format: min-max
206      */
207     protected int generateRandomInteger(String rangeSpec) {
208         String[] range = rangeSpec.split("-");
209         if (range.length != 2) {
210             throw new RuntimeException("Invalid range specifier: " + rangeSpec);
211         }
212         int from = Integer.parseInt(range[0].trim());
213         int to = Integer.parseInt(range[1].trim());
214         if (from > to) {
215             int tmp = from;
216             from = to;
217             to = tmp;
218         }
219         int num;
220         // not very random huh...
221         if (from == to) {
222             num = from;
223         } else {
224             num = from + RANDOM.nextInt((to - from) + 1);
225         }
226         return num;
227     }
228 
229     /**
230      * @param name name of the node
231      * @param location config file location
232      * @param n the node
233      * @param sb a StringBuilder into which to set contents of the node, preserving any XML content
234      * @throws IOException
235      */
236     protected void getNodeValue(String name, String location, Node n, StringBuilder sb) throws IOException {
237         NodeList children = n.getChildNodes();
238         // accumulate all content (preserving any XML content)
239         try {
240             sb.setLength(0);
241             for (int j = 0; j < children.getLength(); j++) {
242                 sb.append(XmlJotter.writeNode(children.item(j), true));
243             }
244         } catch (TransformerException te) {
245             IOException ioe = new IOException("Error obtaining parameter '" + name + "' from config resource: " + location);
246             ioe.initCause(te);
247             throw ioe;
248         }
249     }
250 
251     /**
252      * Sets a parameter in the parameter map, based on the override setting and whether a parameter of the same
253      * name is already present 
254      * @param params the current parameter map
255      * @param override whether to override a previous parameter definition
256      * @param name the parameter name
257      * @param value the parameter value
258      */
259     private void setParam(Map params, Boolean override, String name, String value, String indent) {
260         if (value == null || "null".equals(value)) {
261             LOG.warn("Not adding property [" + name + "] because it is null - most likely no token could be found for substituion.");
262             return;
263         }
264         if (override) {
265             final String message;
266             Object existingValue = params.get(name);
267             if (existingValue != null) {
268                 //if (!existingValue.equals(value)) {
269                     message = indent + "Overriding property " + name + "=[" + existingValue + "] with " + name + "=[" + value + "]"; 
270                 //}
271                 params.remove(name);
272             } else {
273                 message = indent + "Defining property " + name + "=[" + value + "]";
274             }
275             LOG.debug(message);
276             params.put(name, value);
277         } else if (!params.containsKey(name)) {
278             LOG.debug(indent + "Defining property " + name + "=[" + value + "]");
279             params.put(name, value);
280         } else {
281             LOG.debug(indent + "Not overriding existing parameter: " + name + " '" + params.get(name) + "'");
282         }
283     }
284 }