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 019package org.apache.oozie.util; 020 021import org.apache.hadoop.conf.Configuration; 022import org.apache.oozie.service.ConfigurationService; 023import org.apache.oozie.service.Services; 024import org.w3c.dom.DOMException; 025import org.w3c.dom.Document; 026import org.w3c.dom.Element; 027import org.w3c.dom.Node; 028import org.w3c.dom.NodeList; 029import org.w3c.dom.Text; 030import org.xml.sax.SAXException; 031import org.xml.sax.InputSource; 032 033import javax.xml.parsers.DocumentBuilder; 034import javax.xml.parsers.DocumentBuilderFactory; 035import javax.xml.parsers.ParserConfigurationException; 036import java.io.IOException; 037import java.io.InputStream; 038import java.io.Reader; 039import java.io.ByteArrayOutputStream; 040import java.util.Map; 041import java.util.Properties; 042import java.util.regex.Matcher; 043import java.util.regex.Pattern; 044 045/** 046 * Extends Hadoop Configuration providing a new constructor which reads an XML configuration from an InputStream. <p> 047 * OConfiguration(InputStream is). 048 */ 049public class XConfiguration extends Configuration { 050 051 public static final String CONFIGURATION_SUBSTITUTE_DEPTH = "oozie.configuration.substitute.depth"; 052 053 private boolean restrictSystemProperties = true; 054 private boolean restrictParser = true; 055 /** 056 * Create an empty configuration. <p> Default values are not loaded. 057 */ 058 public XConfiguration() { 059 super(false); 060 initSubstituteDepth(); 061 } 062 063 /** 064 * Create a configuration from an InputStream. <p> Code canibalized from <code>Configuration.loadResource()</code>. 065 * 066 * @param is inputstream to read the configuration from. 067 * @param restrictParser whether to restrict the parser 068 * @throws IOException thrown if the configuration could not be read. 069 */ 070 public XConfiguration(InputStream is, boolean restrictParser) throws IOException { 071 this(); 072 this.restrictParser = restrictParser; 073 parse(is); 074 } 075 076 /** 077 * Create a configuration from an InputStream. <p> Code canibalized from <code>Configuration.loadResource()</code>. 078 * 079 * @param is inputstream to read the configuration from. 080 * @throws IOException thrown if the configuration could not be read. 081 */ 082 public XConfiguration(InputStream is) throws IOException { 083 this(is, true); 084 } 085 086 /** 087 * Create a configuration from an Reader. <p> Code canibalized from <code>Configuration.loadResource()</code>. 088 * 089 * @param reader reader to read the configuration from. 090 * @param restrictParser whether to restrict the parser 091 * @throws IOException thrown if the configuration could not be read. 092 */ 093 public XConfiguration(Reader reader, boolean restrictParser) throws IOException { 094 this(); 095 this.restrictParser = restrictParser; 096 parse(reader); 097 } 098 099 /** 100 * Create a configuration from an Reader. <p> Code canibalized from <code>Configuration.loadResource()</code>. 101 * 102 * @param reader reader to read the configuration from. 103 * @throws IOException thrown if the configuration could not be read. 104 */ 105 public XConfiguration(Reader reader) throws IOException { 106 this(reader, true); 107 } 108 109 /** 110 * Create an configuration from a Properties instance. 111 * 112 * @param props Properties instance to get all properties from. 113 */ 114 public XConfiguration(Properties props) { 115 this(); 116 for (Map.Entry entry : props.entrySet()) { 117 set((String) entry.getKey(), (String) entry.getValue()); 118 } 119 } 120 121 /** 122 * Return a Properties instance with the configuration properties. 123 * 124 * @return a Properties instance with the configuration properties. 125 */ 126 public Properties toProperties() { 127 Properties props = new Properties(); 128 for (Map.Entry<String, String> entry : this) { 129 props.setProperty(entry.getKey(), entry.getValue()); 130 } 131 return props; 132 } 133 134 // overriding get() & substitueVars from Configuration to honor defined variables 135 // over system properties 136 //wee need this because substituteVars() is a private method and does not behave like virtual 137 //in Configuration 138 /** 139 * Get the value of the <code>name</code> property, <code>null</code> if 140 * no such property exists. 141 * 142 * Values are processed for <a href="#VariableExpansion">variable expansion</a> 143 * before being returned. 144 * 145 * @param name the property name. 146 * @return the value of the <code>name</code> property, 147 * or null if no such property exists. 148 */ 149 @Override 150 public String get(String name) { 151 return substituteVars(getRaw(name)); 152 } 153 154 /** 155 * Get the value of the <code>name</code> property. If no such property 156 * exists, then <code>defaultValue</code> is returned. 157 * 158 * @param name property name. 159 * @param defaultValue default value. 160 * @return property value, or <code>defaultValue</code> if the property 161 * doesn't exist. 162 */ 163 @Override 164 public String get(String name, String defaultValue) { 165 String value = getRaw(name); 166 if (value == null) { 167 value = defaultValue; 168 } 169 else { 170 value = substituteVars(value); 171 } 172 return value; 173 } 174 175 private static Pattern varPat = Pattern.compile("\\$\\{[^\\}\\$\u0020]+\\}"); 176 private static int MAX_SUBST = 20; 177 protected static volatile boolean initalized = false; 178 private static void initSubstituteDepth() { 179 if (!initalized && Services.get() != null && Services.get().get(ConfigurationService.class) != null) { 180 MAX_SUBST = ConfigurationService.getInt(CONFIGURATION_SUBSTITUTE_DEPTH); 181 initalized = true; 182 } 183 } 184 185 private String substituteVars(String expr) { 186 if (expr == null) { 187 return null; 188 } 189 Matcher match = varPat.matcher(""); 190 String eval = expr; 191 int s = 0; 192 while (MAX_SUBST == -1 || s < MAX_SUBST ) { 193 match.reset(eval); 194 if (!match.find()) { 195 return eval; 196 } 197 String var = match.group(); 198 var = var.substring(2, var.length() - 1); // remove ${ .. } 199 200 String val = getRaw(var); 201 202 if (val == null && !this.restrictSystemProperties) { 203 val = System.getProperty(var); 204 } 205 206 if (val == null) { 207 return eval; // return literal ${var}: var is unbound 208 } 209 // substitute 210 eval = eval.substring(0, match.start()) + val + eval.substring(match.end()); 211 s++; 212 } 213 throw new IllegalStateException("Variable substitution depth too large: " + MAX_SUBST + " " + expr); 214 } 215 216 /** 217 * This is a stop gap fix for HADOOP-4416. 218 */ 219 public Class<?> getClassByName(String name) throws ClassNotFoundException { 220 return super.getClassByName(name.trim()); 221 } 222 223 /** 224 * Copy configuration key/value pairs from one configuration to another if a property exists in the target, it gets 225 * replaced. 226 * 227 * @param source source configuration. 228 * @param target target configuration. 229 */ 230 public static void copy(Configuration source, Configuration target) { 231 for (Map.Entry<String, String> entry : source) { 232 target.set(entry.getKey(), entry.getValue()); 233 } 234 } 235 236 /** 237 * Injects configuration key/value pairs from one configuration to another if the key does not exist in the target 238 * configuration. 239 * 240 * @param source source configuration. 241 * @param target target configuration. 242 */ 243 public static void injectDefaults(Configuration source, Configuration target) { 244 if (source != null) { 245 for (Map.Entry<String, String> entry : source) { 246 if (target.get(entry.getKey()) == null) { 247 target.set(entry.getKey(), entry.getValue()); 248 } 249 } 250 } 251 } 252 253 /** 254 * Returns a new XConfiguration with all values trimmed. 255 * 256 * @return a new XConfiguration with all values trimmed. 257 */ 258 public XConfiguration trim() { 259 XConfiguration trimmed = new XConfiguration(); 260 for (Map.Entry<String, String> entry : this) { 261 trimmed.set(entry.getKey(), entry.getValue().trim()); 262 } 263 return trimmed; 264 } 265 266 /** 267 * Returns a new XConfiguration instance with all inline values resolved. 268 * 269 * @return a new XConfiguration instance with all inline values resolved. 270 */ 271 public XConfiguration resolve() { 272 XConfiguration resolved = new XConfiguration(); 273 for (Map.Entry<String, String> entry : this) { 274 resolved.set(entry.getKey(), get(entry.getKey())); 275 } 276 return resolved; 277 } 278 279 // Canibalized from Hadoop <code>Configuration.loadResource()</code>. 280 private void parse(InputStream is) throws IOException { 281 try { 282 Document doc = getDocumentBuilder().parse(is); 283 parseDocument(doc); 284 285 } 286 catch (SAXException e) { 287 throw new IOException(e); 288 } 289 catch (ParserConfigurationException e) { 290 throw new IOException(e); 291 } 292 } 293 294 // Canibalized from Hadoop <code>Configuration.loadResource()</code>. 295 private void parse(Reader reader) throws IOException { 296 try { 297 Document doc = getDocumentBuilder().parse(new InputSource(reader)); 298 parseDocument(doc); 299 } 300 catch (SAXException e) { 301 throw new IOException(e); 302 } 303 catch (ParserConfigurationException e) { 304 throw new IOException(e); 305 } 306 } 307 308 // Canibalized from Hadoop <code>Configuration.loadResource()</code>. 309 private void parseDocument(Document doc) throws IOException { 310 Element root = doc.getDocumentElement(); 311 if (!"configuration".equals(root.getLocalName())) { 312 throw new IOException("bad conf file: top-level element not <configuration>"); 313 } 314 processNodes(root); 315 } 316 317 // Cannibalized from Hadoop <code>Configuration.loadResource()</code>. 318 private void processNodes(Element root) throws IOException { 319 try { 320 NodeList props = root.getChildNodes(); 321 for (int i = 0; i < props.getLength(); i++) { 322 Node propNode = props.item(i); 323 if (!(propNode instanceof Element)) { 324 continue; 325 } 326 Element prop = (Element) propNode; 327 if (prop.getLocalName().equals("configuration")) { 328 processNodes(prop); 329 continue; 330 } 331 if (!"property".equals(prop.getLocalName())) { 332 throw new IOException("bad conf file: element not <property>"); 333 } 334 NodeList fields = prop.getChildNodes(); 335 String attr = null; 336 String value = null; 337 for (int j = 0; j < fields.getLength(); j++) { 338 Node fieldNode = fields.item(j); 339 if (!(fieldNode instanceof Element)) { 340 continue; 341 } 342 Element field = (Element) fieldNode; 343 if ("name".equals(field.getLocalName()) && field.hasChildNodes()) { 344 attr = ((Text) field.getFirstChild()).getData().trim(); 345 } 346 if ("value".equals(field.getLocalName()) && field.hasChildNodes()) { 347 value = ((Text) field.getFirstChild()).getData(); 348 } 349 } 350 if (attr != null && value != null) { 351 set(attr, value); 352 } 353 } 354 355 } 356 catch (DOMException e) { 357 throw new IOException(e); 358 } 359 } 360 361 /** 362 * Return a string with the configuration in XML format. 363 * 364 * @return a string with the configuration in XML format. 365 */ 366 public String toXmlString() { 367 return toXmlString(true); 368 } 369 370 public String toXmlString(boolean prolog) { 371 String xml; 372 try { 373 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 374 this.writeXml(baos); 375 baos.close(); 376 xml = new String(baos.toByteArray()); 377 } 378 catch (IOException ex) { 379 throw new RuntimeException("It should not happen, " + ex.getMessage(), ex); 380 } 381 if (!prolog) { 382 xml = xml.substring(xml.indexOf("<configuration>")); 383 } 384 return xml; 385 } 386 387 /** 388 * Get the comma delimited values of the name property as an array of trimmed Strings. If no such property is specified then 389 * null is returned. 390 * 391 * @param name property name. 392 * @return property value as an array of trimmed Strings, or null. 393 */ 394 public String[] getTrimmedStrings(String name) { 395 String[] values = getStrings(name); 396 if (values != null) { 397 for (int i = 0; i < values.length; i++) { 398 values[i] = values[i].trim(); 399 } 400 } 401 return values; 402 } 403 404 private DocumentBuilder getDocumentBuilder() throws ParserConfigurationException { 405 DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); 406 docBuilderFactory.setNamespaceAware(true); 407 docBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); 408 docBuilderFactory.setXIncludeAware(true); 409 if(this.restrictParser) { 410 docBuilderFactory.setXIncludeAware(false); 411 //Redundant with disallow-doctype, but just in case 412 docBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); 413 docBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 414 docBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 415 } 416 docBuilderFactory.setExpandEntityReferences(false); 417 // ignore all comments inside the xml file 418 docBuilderFactory.setIgnoringComments(true); 419 return docBuilderFactory.newDocumentBuilder(); 420 } 421 422 /** 423 * Restrict the parser 424 * @param restrictParser 425 */ 426 public void setRestrictParser(boolean restrictParser) { 427 this.restrictParser = restrictParser; 428 } 429 430 public boolean getRestrictParser() { 431 return restrictParser; 432 } 433 /** 434 * Restrict reading property from System.getProperty() 435 * @param restrictSystemProperties 436 */ 437 public void setRestrictSystemProperties(boolean restrictSystemProperties) { 438 this.restrictSystemProperties = restrictSystemProperties; 439 } 440 441 public boolean getRestrictSystemProperties() { 442 return restrictSystemProperties; 443 } 444 445 446 447}