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                // support for includes in the xml file
241                docBuilderFactory.setNamespaceAware(true);
242                docBuilderFactory.setXIncludeAware(true);
243                // ignore all comments inside the xml file
244                docBuilderFactory.setIgnoringComments(true);
245                DocumentBuilder builder = docBuilderFactory.newDocumentBuilder();
246                Document doc = builder.parse(is);
247                parseDocument(doc);
248    
249            }
250            catch (SAXException e) {
251                throw new IOException(e);
252            }
253            catch (ParserConfigurationException e) {
254                throw new IOException(e);
255            }
256        }
257    
258        // Canibalized from Hadoop <code>Configuration.loadResource()</code>.
259        private void parse(Reader reader) throws IOException {
260            try {
261                DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
262                // support for includes in the xml file
263                docBuilderFactory.setNamespaceAware(true);
264                docBuilderFactory.setXIncludeAware(true);
265                // ignore all comments inside the xml file
266                docBuilderFactory.setIgnoringComments(true);
267                DocumentBuilder builder = docBuilderFactory.newDocumentBuilder();
268                Document doc = builder.parse(new InputSource(reader));
269                parseDocument(doc);
270            }
271            catch (SAXException e) {
272                throw new IOException(e);
273            }
274            catch (ParserConfigurationException e) {
275                throw new IOException(e);
276            }
277        }
278    
279        // Canibalized from Hadoop <code>Configuration.loadResource()</code>.
280        private void parseDocument(Document doc) throws IOException {
281            Element root = doc.getDocumentElement();
282            if (!"configuration".equals(root.getTagName())) {
283                throw new IOException("bad conf file: top-level element not <configuration>");
284            }
285            processNodes(root);
286        }
287    
288        // Canibalized from Hadoop <code>Configuration.loadResource()</code>.
289        private void processNodes(Element root) throws IOException {
290            try {
291                NodeList props = root.getChildNodes();
292                for (int i = 0; i < props.getLength(); i++) {
293                    Node propNode = props.item(i);
294                    if (!(propNode instanceof Element)) {
295                        continue;
296                    }
297                    Element prop = (Element) propNode;
298                    if (prop.getTagName().equals("configuration")) {
299                        processNodes(prop);
300                        continue;
301                    }
302                    if (!"property".equals(prop.getTagName())) {
303                        throw new IOException("bad conf file: element not <property>");
304                    }
305                    NodeList fields = prop.getChildNodes();
306                    String attr = null;
307                    String value = null;
308                    for (int j = 0; j < fields.getLength(); j++) {
309                        Node fieldNode = fields.item(j);
310                        if (!(fieldNode instanceof Element)) {
311                            continue;
312                        }
313                        Element field = (Element) fieldNode;
314                        if ("name".equals(field.getTagName()) && field.hasChildNodes()) {
315                            attr = ((Text) field.getFirstChild()).getData().trim();
316                        }
317                        if ("value".equals(field.getTagName()) && field.hasChildNodes()) {
318                            value = ((Text) field.getFirstChild()).getData();
319                        }
320                    }
321                    if (attr != null && value != null) {
322                        set(attr, value);
323                    }
324                }
325    
326            }
327            catch (DOMException e) {
328                throw new IOException(e);
329            }
330        }
331    
332        /**
333         * Return a string with the configuration in XML format.
334         *
335         * @return a string with the configuration in XML format.
336         */
337        public String toXmlString() {
338            return toXmlString(true);
339        }
340    
341        public String toXmlString(boolean prolog) {
342            String xml;
343            try {
344                ByteArrayOutputStream baos = new ByteArrayOutputStream();
345                this.writeXml(baos);
346                baos.close();
347                xml = new String(baos.toByteArray());
348            }
349            catch (IOException ex) {
350                throw new RuntimeException("It should not happen, " + ex.getMessage(), ex);
351            }
352            if (!prolog) {
353                xml = xml.substring(xml.indexOf("<configuration>"));
354            }
355            return xml;
356        }
357    
358        /**
359         * Get the comma delimited values of the name property as an array of trimmed Strings. If no such property is specified then
360         * null is returned.
361         *
362         * @param name property name.
363         * @return property value as an array of trimmed Strings, or null.
364         */
365        public String[] getTrimmedStrings(String name) {
366            String[] values = getStrings(name);
367            if (values != null) {
368                for (int i = 0; i < values.length; i++) {
369                    values[i] = values[i].trim();
370                }
371            }
372            return values;
373        }
374    }