001    /**
002     * Copyright (c) 2010 Yahoo! Inc. All rights reserved.
003     * Licensed under the Apache License, Version 2.0 (the "License");
004     * you may not use this file except in compliance with the License.
005     * You may obtain a copy of the License at
006     *
007     *   http://www.apache.org/licenses/LICENSE-2.0
008     *
009     *  Unless required by applicable law or agreed to in writing, software
010     *  distributed under the License is distributed on an "AS IS" BASIS,
011     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012     *  See the License for the specific language governing permissions and
013     *  limitations under the License. See accompanying LICENSE file.
014     */
015    package org.apache.oozie.util;
016    
017    import org.apache.hadoop.conf.Configuration;
018    import org.w3c.dom.DOMException;
019    import org.w3c.dom.Document;
020    import org.w3c.dom.Element;
021    import org.w3c.dom.Node;
022    import org.w3c.dom.NodeList;
023    import org.w3c.dom.Text;
024    import org.xml.sax.SAXException;
025    import org.xml.sax.InputSource;
026    
027    import javax.xml.parsers.DocumentBuilder;
028    import javax.xml.parsers.DocumentBuilderFactory;
029    import javax.xml.parsers.ParserConfigurationException;
030    import java.io.IOException;
031    import java.io.InputStream;
032    import java.io.Reader;
033    import java.io.ByteArrayOutputStream;
034    import java.util.Map;
035    import java.util.Properties;
036    import java.util.regex.Matcher;
037    import java.util.regex.Pattern;
038    
039    /**
040     * Extends Hadoop Configuration providing a new constructor which reads an XML configuration from an InputStream. <p/>
041     * OConfiguration(InputStream is).
042     */
043    public class XConfiguration extends Configuration {
044    
045        /**
046         * Create an empty configuration. <p/> Default values are not loaded.
047         */
048        public XConfiguration() {
049            super(false);
050        }
051    
052        /**
053         * Create a configuration from an InputStream. <p/> Code canibalized from <code>Configuration.loadResource()</code>.
054         *
055         * @param is inputstream to read the configuration from.
056         * @throws IOException thrown if the configuration could not be read.
057         */
058        public XConfiguration(InputStream is) throws IOException {
059            this();
060            parse(is);
061        }
062    
063        /**
064         * Create a configuration from an Reader. <p/> Code canibalized from <code>Configuration.loadResource()</code>.
065         *
066         * @param reader reader to read the configuration from.
067         * @throws IOException thrown if the configuration could not be read.
068         */
069        public XConfiguration(Reader reader) throws IOException {
070            this();
071            parse(reader);
072        }
073    
074        /**
075         * Create an configuration from a Properties instance.
076         *
077         * @param props Properties instance to get all properties from.
078         */
079        public XConfiguration(Properties props) {
080            this();
081            for (Map.Entry entry : props.entrySet()) {
082                set((String) entry.getKey(), (String) entry.getValue());
083            }
084    
085        }
086    
087        /**
088         * Return a Properties instance with the configuration properties.
089         *
090         * @return a Properties instance with the configuration properties.
091         */
092        public Properties toProperties() {
093            Properties props = new Properties();
094            for (Map.Entry<String, String> entry : this) {
095                props.setProperty(entry.getKey(), entry.getValue());
096            }
097            return props;
098        }
099    
100        // overriding get() & substitueVars from Configuration to honor defined variables
101        // over system properties
102        //wee need this because substituteVars() is a private method and does not behave like virtual
103        //in Configuration
104        /**
105         * Get the value of the <code>name</code> property, <code>null</code> if
106         * no such property exists.
107         *
108         * Values are processed for <a href="#VariableExpansion">variable expansion</a>
109         * before being returned.
110         *
111         * @param name the property name.
112         * @return the value of the <code>name</code> property,
113         *         or null if no such property exists.
114         */
115        @Override
116        public String get(String name) {
117          return substituteVars(getRaw(name));
118        }
119    
120        /**
121         * Get the value of the <code>name</code> property. If no such property
122         * exists, then <code>defaultValue</code> is returned.
123         *
124         * @param name property name.
125         * @param defaultValue default value.
126         * @return property value, or <code>defaultValue</code> if the property
127         *         doesn't exist.
128         */
129        @Override
130        public String get(String name, String defaultValue) {
131            String value = getRaw(name);
132            if (value == null) {
133                value = defaultValue;
134            }
135            else {
136                value = substituteVars(value);
137            }
138            return value;
139        }
140    
141        private static Pattern varPat = Pattern.compile("\\$\\{[^\\}\\$\u0020]+\\}");
142        private static int MAX_SUBST = 20;
143    
144        private String substituteVars(String expr) {
145            if (expr == null) {
146                return null;
147            }
148            Matcher match = varPat.matcher("");
149            String eval = expr;
150            for (int s = 0; s < MAX_SUBST; s++) {
151                match.reset(eval);
152                if (!match.find()) {
153                    return eval;
154                }
155                String var = match.group();
156                var = var.substring(2, var.length() - 1); // remove ${ .. }
157    
158                String val = getRaw(var);
159                if (val == null) {
160                    val = System.getProperty(var);
161                }
162    
163                if (val == null) {
164                    return eval; // return literal ${var}: var is unbound
165                }
166                // substitute
167                eval = eval.substring(0, match.start()) + val + eval.substring(match.end());
168            }
169            throw new IllegalStateException("Variable substitution depth too large: " + MAX_SUBST + " " + expr);
170        }
171    
172        /**
173         * This is a stop gap fix for <link href="https://issues.apache.org/jira/browse/HADOOP-4416">HADOOP-4416</link>.
174         */
175        public Class<?> getClassByName(String name) throws ClassNotFoundException {
176            return super.getClassByName(name.trim());
177        }
178    
179        /**
180         * Copy configuration key/value pairs from one configuration to another if a property exists in the target, it gets
181         * replaced.
182         *
183         * @param source source configuration.
184         * @param target target configuration.
185         */
186        public static void copy(Configuration source, Configuration target) {
187            for (Map.Entry<String, String> entry : source) {
188                target.set(entry.getKey(), entry.getValue());
189            }
190        }
191    
192        /**
193         * Injects configuration key/value pairs from one configuration to another if the key does not exist in the target
194         * configuration.
195         *
196         * @param source source configuration.
197         * @param target target configuration.
198         */
199        public static void injectDefaults(Configuration source, Configuration target) {
200            for (Map.Entry<String, String> entry : source) {
201                if (target.get(entry.getKey()) == null) {
202                    target.set(entry.getKey(), entry.getValue());
203                }
204            }
205        }
206    
207        /**
208         * Returns a new XConfiguration with all values trimmed.
209         *
210         * @return a new XConfiguration with all values trimmed.
211         */
212        public XConfiguration trim() {
213            XConfiguration trimmed = new XConfiguration();
214            for (Map.Entry<String, String> entry : this) {
215                trimmed.set(entry.getKey(), entry.getValue().trim());
216            }
217            return trimmed;
218        }
219    
220        /**
221         * Returns a new XConfiguration instance with all inline values resolved.
222         *
223         * @return a new XConfiguration instance with all inline values resolved.
224         */
225        public XConfiguration resolve() {
226            XConfiguration resolved = new XConfiguration();
227            for (Map.Entry<String, String> entry : this) {
228                resolved.set(entry.getKey(), get(entry.getKey()));
229            }
230            return resolved;
231        }
232    
233        // Canibalized from Hadoop <code>Configuration.loadResource()</code>.
234        private void parse(InputStream is) throws IOException {
235            try {
236                DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
237                // ignore all comments inside the xml file
238                docBuilderFactory.setIgnoringComments(true);
239                DocumentBuilder builder = docBuilderFactory.newDocumentBuilder();
240                Document doc = builder.parse(is);
241                parseDocument(doc);
242            }
243            catch (SAXException e) {
244                throw new IOException(e);
245            }
246            catch (ParserConfigurationException e) {
247                throw new IOException(e);
248            }
249        }
250    
251        // Canibalized from Hadoop <code>Configuration.loadResource()</code>.
252        private void parse(Reader reader) throws IOException {
253            try {
254                DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
255                // ignore all comments inside the xml file
256                docBuilderFactory.setIgnoringComments(true);
257                DocumentBuilder builder = docBuilderFactory.newDocumentBuilder();
258                Document doc = builder.parse(new InputSource(reader));
259                parseDocument(doc);
260            }
261            catch (SAXException e) {
262                throw new IOException(e);
263            }
264            catch (ParserConfigurationException e) {
265                throw new IOException(e);
266            }
267        }
268    
269        // Canibalized from Hadoop <code>Configuration.loadResource()</code>.
270        private void parseDocument(Document doc) throws IOException {
271            try {
272                Element root = doc.getDocumentElement();
273                if (!"configuration".equals(root.getTagName())) {
274                    throw new IOException("bad conf file: top-level element not <configuration>");
275                }
276                NodeList props = root.getChildNodes();
277                for (int i = 0; i < props.getLength(); i++) {
278                    Node propNode = props.item(i);
279                    if (!(propNode instanceof Element)) {
280                        continue;
281                    }
282                    Element prop = (Element) propNode;
283                    if (!"property".equals(prop.getTagName())) {
284                        throw new IOException("bad conf file: element not <property>");
285                    }
286                    NodeList fields = prop.getChildNodes();
287                    String attr = null;
288                    String value = null;
289                    for (int j = 0; j < fields.getLength(); j++) {
290                        Node fieldNode = fields.item(j);
291                        if (!(fieldNode instanceof Element)) {
292                            continue;
293                        }
294                        Element field = (Element) fieldNode;
295                        if ("name".equals(field.getTagName()) && field.hasChildNodes()) {
296                            attr = ((Text) field.getFirstChild()).getData().trim();
297                        }
298                        if ("value".equals(field.getTagName()) && field.hasChildNodes()) {
299                            value = ((Text) field.getFirstChild()).getData();
300                        }
301                    }
302    
303                    if (attr != null && value != null) {
304                        set(attr, value);
305                    }
306                }
307    
308            }
309            catch (DOMException e) {
310                throw new IOException(e);
311            }
312        }
313    
314        /**
315         * Return a string with the configuration in XML format.
316         *
317         * @return a string with the configuration in XML format.
318         */
319        public String toXmlString() {
320            return toXmlString(true);
321        }
322    
323        public String toXmlString(boolean prolog) {
324            String xml;
325            try {
326                ByteArrayOutputStream baos = new ByteArrayOutputStream();
327                this.writeXml(baos);
328                baos.close();
329                xml = new String(baos.toByteArray());
330            }
331            catch (IOException ex) {
332                throw new RuntimeException("It should not happen, " + ex.getMessage(), ex);
333            }
334            if (!prolog) {
335                xml = xml.substring(xml.indexOf("<configuration>"));
336            }
337            return xml;
338        }
339    
340    }