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 }