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}