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 }