001 /*
002 * Copyright 2007-2008 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package org.kuali.rice.core.config;
017
018 import org.apache.commons.lang.StringUtils;
019 import org.apache.commons.lang.text.StrLookup;
020 import org.apache.commons.lang.text.StrSubstitutor;
021 import org.apache.log4j.Logger;
022 import org.kuali.rice.core.util.RiceUtilities;
023 import org.kuali.rice.core.util.XmlJotter;
024 import org.w3c.dom.Document;
025 import org.w3c.dom.Element;
026 import org.w3c.dom.Node;
027 import org.w3c.dom.NodeList;
028 import org.xml.sax.SAXException;
029
030 import javax.xml.parsers.DocumentBuilderFactory;
031 import javax.xml.parsers.ParserConfigurationException;
032 import javax.xml.transform.TransformerException;
033 import java.io.IOException;
034 import java.io.InputStream;
035 import java.util.LinkedHashMap;
036 import java.util.Map;
037 import java.util.Random;
038
039 /**
040 * ConfigParser implementation that supports a hierarchy of configs, in which
041 * configs can include other configs. Variable tokens are resolved at parse time,
042 * in the order in which they are encountered. This class relies on Spring for resource
043 * loading and Apache Commons Lang for variable replacement.
044 *
045 * @author Kuali Rice Team (rice.collab@kuali.org)
046 */
047 public class ConfigParserImpl implements ConfigParser {
048 // keep the same random
049 private static final Random RANDOM = new Random();
050
051 private static final Logger LOG = Logger.getLogger(ConfigParserImpl.class);
052 private static final String IMPORT_NAME = "config.location";
053 private static final String PARAM_NAME= "param";
054 private static final String NAME_ATTR = "name";
055 private static final String OVERRIDE_ATTR = "override";
056 private static final String RANDOM_ATTR = "random";
057 private static final String INDENT = " ";
058
059 public static final String ALTERNATE_BUILD_LOCATION_KEY = "alt.build.location";
060
061 /**
062 * A StrLookup implementation that delegates to System properties if the key is not
063 * found in the supplied map.
064 * @author Kuali Rice Team (rice.collab@kuali.org)
065 */
066 private static class SystemPropertiesDelegatingStrLookup extends StrLookup {
067 private final Map map;
068 private SystemPropertiesDelegatingStrLookup(Map map) {
069 this.map = map;
070 }
071 @Override
072 public String lookup(String key) {
073 Object o = map.get(key);
074 if (o != null) {
075 return String.valueOf(o);
076 } else {
077 String s = System.getProperty(key);
078 if (s != null) {
079 return s;
080 } else {
081 // implement behavior for missing property here...e.g. return ""
082 // returning null will result in the substitutor not substituting
083 return "";
084 }
085 }
086 }
087 }
088
089 /**
090 * @see org.kuali.rice.core.config.ConfigParser#parse(java.lang.String[])
091 */
092 public void parse(Map props, String[] locations) throws IOException {
093 LinkedHashMap params = new LinkedHashMap();
094 params.putAll(props);
095 parse(params, locations);
096 props.putAll(params);
097 }
098
099 /**
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 }