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