001: /*
002: * Copyright 2005-2006 The Kuali Foundation.
003: *
004: *
005: * Licensed under the Educational Community License, Version 1.0 (the "License");
006: * you may not use this file except in compliance with the License.
007: * You may obtain a copy of the License at
008: *
009: * http://www.opensource.org/licenses/ecl1.php
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.kuali.rice.config;
018:
019: import java.io.FileInputStream;
020: import java.io.FileNotFoundException;
021: import java.io.IOException;
022: import java.io.InputStream;
023: import java.net.MalformedURLException;
024: import java.net.URL;
025: import java.util.Collections;
026: import java.util.LinkedHashMap;
027: import java.util.LinkedList;
028: import java.util.Map;
029: import java.util.Properties;
030:
031: import javax.xml.parsers.DocumentBuilderFactory;
032: import javax.xml.parsers.ParserConfigurationException;
033: import javax.xml.transform.TransformerException;
034:
035: import org.apache.log4j.Logger;
036: import org.kuali.rice.util.XmlJotter;
037: import org.w3c.dom.Document;
038: import org.w3c.dom.Element;
039: import org.w3c.dom.Node;
040: import org.w3c.dom.NodeList;
041: import org.xml.sax.SAXException;
042:
043: /**
044: * A configuration parser that can get properties already parsed passed in and
045: * override them. Also, can do token replace based on values already parsed
046: * using ${ } to denote tokens.
047: *
048: * @author Kuali Rice Team (kuali-rice@googlegroups.com)
049: */
050: public class HierarchicalConfigParser {
051:
052: private static final Logger LOG = Logger
053: .getLogger(HierarchicalConfigParser.class);
054:
055: private static final String VAR_START_TOKEN = "${";
056:
057: private static final String VAR_END_TOKEN = "}";
058:
059: public static final String ALTERNATE_BUILD_LOCATION_KEY = "alt.build.location";
060:
061: // this Map is for token replacement this represents the set of tokens that
062: // has been made
063: // by a parent config file of the one this is parsing
064: Map currentProperties;
065:
066: public HierarchicalConfigParser(final Map currentProperties) {
067: if (currentProperties == null) {
068: this .currentProperties = new LinkedHashMap();
069: } else {
070: this .currentProperties = currentProperties;
071: }
072: }
073:
074: public Map<String, Object> parse(String fileLoc) throws IOException {
075: Map<String, Object> fileProperties = new LinkedHashMap<String, Object>();
076: parse(fileLoc, fileProperties, true);
077: return fileProperties;
078: }
079:
080: private void parse(String fileLoc,
081: Map<String, Object> fileProperties, boolean baseFile)
082: throws IOException {
083: InputStream configStream = getConfigAsStream(fileLoc);
084: if (configStream == null) {
085: LOG.warn("###############################");
086: LOG.warn("#");
087: LOG.warn("# Configuration file " + fileLoc + " not found!");
088: LOG.warn("#");
089: LOG.warn("###############################");
090: return;
091: }
092:
093: LOG.info("Preparing to parse config file " + fileLoc);
094:
095: if (!baseFile) {
096: fileProperties.put(fileLoc, new Properties());
097: }
098: Properties props = (Properties) fileProperties.get(fileLoc);
099: Document doc;
100: try {
101: doc = DocumentBuilderFactory.newInstance()
102: .newDocumentBuilder().parse(configStream);
103: if (LOG.isDebugEnabled()) {
104: LOG.debug("Parsed config file " + fileLoc + ": \n"
105: + XmlJotter.jotNode(doc, true));
106: }
107: } catch (SAXException se) {
108: IOException ioe = new IOException(
109: "Error parsing config resource: " + fileLoc);
110: ioe.initCause(se);
111: throw ioe;
112: } catch (ParserConfigurationException pce) {
113: IOException ioe = new IOException(
114: "Unable to obtain document builder");
115: ioe.initCause(pce);
116: throw ioe;
117: } finally {
118: configStream.close();
119: }
120:
121: Element root = doc.getDocumentElement();
122: // ignore the actual type of the document element for now
123: // so that plugin descriptors can be parsed
124: NodeList list = root.getChildNodes();
125: StringBuffer content = new StringBuffer();
126: for (int i = 0; i < list.getLength(); i++) {
127: Node node = list.item(i);
128: if (node.getNodeType() != Node.ELEMENT_NODE)
129: continue;
130: if (!"param".equals(node.getNodeName())) {
131: LOG.warn("Encountered non-param config node: "
132: + node.getNodeName());
133: continue;
134: }
135: Element param = (Element) node;
136: String name = param.getAttribute("name");
137: Boolean override = new Boolean(true);
138: if (param.getAttribute("override") != null
139: && param.getAttribute("override").trim().length() > 0) {
140: override = new Boolean(param.getAttribute("override"));
141: }
142: if (name == null) {
143: LOG.error("Unnamed parameter in config resource '"
144: + fileLoc + "': " + XmlJotter.jotNode(param));
145: continue;
146: }
147: NodeList contents = param.getChildNodes();
148: // accumulate all content (preserving any XML content)
149: try {
150: content.setLength(0);
151: for (int j = 0; j < contents.getLength(); j++) {
152: content.append(XmlJotter.writeNode(
153: contents.item(j), true));
154: }
155: String contentValue;
156: try {
157: contentValue = resolvePropertyTokens(content
158: .toString(), fileProperties);
159: } catch (Exception e) {
160: LOG.error("Exception caught parsing " + content, e);
161: throw new RuntimeException(e);
162: }
163: if (name.equals("config.location")) {
164: if (contentValue
165: .indexOf(ALTERNATE_BUILD_LOCATION_KEY) < 0) {
166: parse(contentValue, fileProperties, false);
167: }
168: } else {
169: if (props == null) {
170: updateProperties(fileProperties, override,
171: name, contentValue, fileProperties);
172: } else {
173: updateProperties(props, override, name,
174: contentValue, fileProperties);
175: }
176: }
177: } catch (TransformerException te) {
178: IOException ioe = new IOException(
179: "Error obtaining parameter '" + name
180: + "' from config resource: " + fileLoc);
181: ioe.initCause(te);
182: throw ioe;
183: }
184: }
185: }
186:
187: private void updateProperties(Map props, Boolean override,
188: String name, String value,
189: Map<String, Object> fileProperties) {
190: if (value == null || "null".equals(value)) {
191: LOG
192: .warn("Not adding property ["
193: + name
194: + "] because it is null - most likely no token could be found for substituion.");
195: return;
196: }
197: if (override) {
198: props.put(name, value);
199: } else {
200: if (!override && !fileProperties.containsKey(name)) {
201: props.put(name, value);
202: }
203: }
204: }
205:
206: public static InputStream getConfigAsStream(String fileLoc)
207: throws MalformedURLException, IOException {
208: if (fileLoc.lastIndexOf("classpath:") > -1) {
209: String configName = fileLoc.split("classpath:")[1];
210: return Thread.currentThread().getContextClassLoader()
211: .getResourceAsStream(configName);
212: } else if (fileLoc.lastIndexOf("http://") > -1
213: || fileLoc.lastIndexOf("file:/") > -1) {
214: return new URL(fileLoc).openStream();
215: } else {
216: try {
217: return new FileInputStream(fileLoc);
218: } catch (FileNotFoundException e) {
219: return null; // logged by caller
220: }
221: }
222: }
223:
224: private String resolvePropertyTokens(String content,
225: Map<String, Object> properties) {
226: if (content.indexOf(VAR_START_TOKEN) > -1) {
227: int tokenStart = content.indexOf(VAR_START_TOKEN);
228: int tokenEnd = content.indexOf(VAR_END_TOKEN, tokenStart
229: + VAR_START_TOKEN.length());
230: if (tokenEnd == -1) {
231: throw new RuntimeException(
232: "No ending bracket on token in value "
233: + content);
234: }
235: String token = content.substring(tokenStart
236: + VAR_START_TOKEN.length(), tokenEnd);
237: String tokenValue = null;
238:
239: // get all the properties from all the potentially nested configs in
240: // the master set
241: // of propertiesUsed. Do it now so that all the values are available
242: // for token replacement
243: // next iteration
244: //
245: // The properties map is sorted with the top of the hierarchy as the
246: // first element in the iteration, however
247: // we want to include starting with the bottom of the hierarchy, so
248: // we will iterate over the Map in reverse
249: // order (this reverse iteration fixes the bug referenced by EN-68.
250: LinkedList<Map.Entry<String, Object>> propertiesList = new LinkedList<Map.Entry<String, Object>>(
251: properties.entrySet());
252: Collections.reverse(propertiesList);
253: for (Map.Entry<String, Object> config : propertiesList) {
254: if (!(config.getValue() instanceof Properties)) {
255: if (token.equals(config.getKey())) {
256: tokenValue = (String) config.getValue();
257: break;
258: } else {
259: continue;
260: }
261: }
262: Properties configProps = (Properties) config.getValue();
263: tokenValue = (String) configProps.get(token);
264: if (tokenValue != null) {
265: break;
266: }
267:
268: LOG
269: .debug("Found token " + token
270: + " in included config file "
271: + config.getKey());
272: }
273:
274: if (tokenValue == null) {
275: if (token.indexOf(ALTERNATE_BUILD_LOCATION_KEY) > -1) {
276: return token;
277: }
278: LOG.debug("Did not find token " + token
279: + " in local properties. Looking in parent.");
280: tokenValue = (String) this .currentProperties.get(token);
281: if (tokenValue == null) {
282: LOG
283: .debug("Did not find token "
284: + token
285: + " in parent properties. Looking in system properties.");
286: tokenValue = System.getProperty(token);
287: if (tokenValue == null) {
288: LOG
289: .warn("Did not find token "
290: + token
291: + " in all available configuration properties!");
292: } else {
293: LOG.debug("Found token " + token
294: + " in sytem properties");
295: }
296: } else {
297: LOG.debug("Found token " + token + "=" + tokenValue
298: + " in parent.");
299: }
300: } else {
301: LOG.debug("Found token in local properties");
302: }
303:
304: String tokenizedContent = content.substring(0, tokenStart)
305: + tokenValue
306: + content.substring(tokenEnd
307: + VAR_END_TOKEN.length(), content.length());
308: // give it back to this method so we can have multiple tokens per
309: // config entry.
310: return resolvePropertyTokens(tokenizedContent, properties);
311: }
312:
313: return content;
314: }
315: }
|