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 */
018package org.apache.oozie.util;
019
020import org.apache.hadoop.conf.Configuration;
021import org.w3c.dom.DOMException;
022import org.w3c.dom.Document;
023import org.w3c.dom.Element;
024import org.w3c.dom.Node;
025import org.w3c.dom.NodeList;
026import org.w3c.dom.Text;
027import org.xml.sax.SAXException;
028import org.xml.sax.InputSource;
029
030import javax.xml.parsers.DocumentBuilder;
031import javax.xml.parsers.DocumentBuilderFactory;
032import javax.xml.parsers.ParserConfigurationException;
033import java.io.IOException;
034import java.io.InputStream;
035import java.io.Reader;
036import java.io.ByteArrayOutputStream;
037import java.util.Map;
038import java.util.Properties;
039import java.util.regex.Matcher;
040import 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 */
046public 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        if (source != null) {
204            for (Map.Entry<String, String> entry : source) {
205                if (target.get(entry.getKey()) == null) {
206                    target.set(entry.getKey(), entry.getValue());
207                }
208            }
209        }
210    }
211
212    /**
213     * Returns a new XConfiguration with all values trimmed.
214     *
215     * @return a new XConfiguration with all values trimmed.
216     */
217    public XConfiguration trim() {
218        XConfiguration trimmed = new XConfiguration();
219        for (Map.Entry<String, String> entry : this) {
220            trimmed.set(entry.getKey(), entry.getValue().trim());
221        }
222        return trimmed;
223    }
224
225    /**
226     * Returns a new XConfiguration instance with all inline values resolved.
227     *
228     * @return a new XConfiguration instance with all inline values resolved.
229     */
230    public XConfiguration resolve() {
231        XConfiguration resolved = new XConfiguration();
232        for (Map.Entry<String, String> entry : this) {
233            resolved.set(entry.getKey(), get(entry.getKey()));
234        }
235        return resolved;
236    }
237
238    // Canibalized from Hadoop <code>Configuration.loadResource()</code>.
239    private void parse(InputStream is) throws IOException {
240        try {
241            DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
242            // support for includes in the xml file
243            docBuilderFactory.setNamespaceAware(true);
244            docBuilderFactory.setXIncludeAware(true);
245            // ignore all comments inside the xml file
246            docBuilderFactory.setIgnoringComments(true);
247            DocumentBuilder builder = docBuilderFactory.newDocumentBuilder();
248            Document doc = builder.parse(is);
249            parseDocument(doc);
250
251        }
252        catch (SAXException e) {
253            throw new IOException(e);
254        }
255        catch (ParserConfigurationException e) {
256            throw new IOException(e);
257        }
258    }
259
260    // Canibalized from Hadoop <code>Configuration.loadResource()</code>.
261    private void parse(Reader reader) throws IOException {
262        try {
263            DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
264            // support for includes in the xml file
265            docBuilderFactory.setNamespaceAware(true);
266            docBuilderFactory.setXIncludeAware(true);
267            // ignore all comments inside the xml file
268            docBuilderFactory.setIgnoringComments(true);
269            DocumentBuilder builder = docBuilderFactory.newDocumentBuilder();
270            Document doc = builder.parse(new InputSource(reader));
271            parseDocument(doc);
272        }
273        catch (SAXException e) {
274            throw new IOException(e);
275        }
276        catch (ParserConfigurationException e) {
277            throw new IOException(e);
278        }
279    }
280
281    // Canibalized from Hadoop <code>Configuration.loadResource()</code>.
282    private void parseDocument(Document doc) throws IOException {
283        Element root = doc.getDocumentElement();
284        if (!"configuration".equals(root.getTagName())) {
285            throw new IOException("bad conf file: top-level element not <configuration>");
286        }
287        processNodes(root);
288    }
289
290    // Canibalized from Hadoop <code>Configuration.loadResource()</code>.
291    private void processNodes(Element root) throws IOException {
292        try {
293            NodeList props = root.getChildNodes();
294            for (int i = 0; i < props.getLength(); i++) {
295                Node propNode = props.item(i);
296                if (!(propNode instanceof Element)) {
297                    continue;
298                }
299                Element prop = (Element) propNode;
300                if (prop.getTagName().equals("configuration")) {
301                    processNodes(prop);
302                    continue;
303                }
304                if (!"property".equals(prop.getTagName())) {
305                    throw new IOException("bad conf file: element not <property>");
306                }
307                NodeList fields = prop.getChildNodes();
308                String attr = null;
309                String value = null;
310                for (int j = 0; j < fields.getLength(); j++) {
311                    Node fieldNode = fields.item(j);
312                    if (!(fieldNode instanceof Element)) {
313                        continue;
314                    }
315                    Element field = (Element) fieldNode;
316                    if ("name".equals(field.getTagName()) && field.hasChildNodes()) {
317                        attr = ((Text) field.getFirstChild()).getData().trim();
318                    }
319                    if ("value".equals(field.getTagName()) && field.hasChildNodes()) {
320                        value = ((Text) field.getFirstChild()).getData();
321                    }
322                }
323                if (attr != null && value != null) {
324                    set(attr, value);
325                }
326            }
327
328        }
329        catch (DOMException e) {
330            throw new IOException(e);
331        }
332    }
333
334    /**
335     * Return a string with the configuration in XML format.
336     *
337     * @return a string with the configuration in XML format.
338     */
339    public String toXmlString() {
340        return toXmlString(true);
341    }
342
343    public String toXmlString(boolean prolog) {
344        String xml;
345        try {
346            ByteArrayOutputStream baos = new ByteArrayOutputStream();
347            this.writeXml(baos);
348            baos.close();
349            xml = new String(baos.toByteArray());
350        }
351        catch (IOException ex) {
352            throw new RuntimeException("It should not happen, " + ex.getMessage(), ex);
353        }
354        if (!prolog) {
355            xml = xml.substring(xml.indexOf("<configuration>"));
356        }
357        return xml;
358    }
359
360    /**
361     * Get the comma delimited values of the name property as an array of trimmed Strings. If no such property is specified then
362     * null is returned.
363     *
364     * @param name property name.
365     * @return property value as an array of trimmed Strings, or null.
366     */
367    public String[] getTrimmedStrings(String name) {
368        String[] values = getStrings(name);
369        if (values != null) {
370            for (int i = 0; i < values.length; i++) {
371                values[i] = values[i].trim();
372            }
373        }
374        return values;
375    }
376}