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    }