001 /* 002 @license.text@ 003 */ 004 package biz.hammurapi.config; 005 006 import java.io.File; 007 import java.io.IOException; 008 import java.io.InputStream; 009 import java.io.Reader; 010 import java.lang.reflect.Array; 011 import java.lang.reflect.Constructor; 012 import java.lang.reflect.Field; 013 import java.lang.reflect.Method; 014 import java.lang.reflect.Modifier; 015 import java.net.MalformedURLException; 016 import java.net.URL; 017 import java.util.ArrayList; 018 import java.util.Collection; 019 import java.util.Collections; 020 import java.util.Enumeration; 021 import java.util.HashMap; 022 import java.util.HashSet; 023 import java.util.Iterator; 024 import java.util.LinkedList; 025 import java.util.List; 026 import java.util.Map; 027 import java.util.Set; 028 029 import javax.xml.parsers.DocumentBuilderFactory; 030 031 import org.w3c.dom.Element; 032 import org.w3c.dom.Node; 033 import org.w3c.dom.NodeList; 034 import org.xml.sax.InputSource; 035 036 import biz.hammurapi.config.adapters.File2InputStreamConfigurableAdapter; 037 import biz.hammurapi.config.adapters.InputStream2DomConfigurableAdapter; 038 import biz.hammurapi.config.adapters.URL2InputStreamConfigurableAdapter; 039 import biz.hammurapi.convert.ConvertingService; 040 import biz.hammurapi.convert.DuckConverterFactory; 041 import biz.hammurapi.xml.dom.DOMUtils; 042 043 044 /** 045 * Creates and configures objects from DOM {@link org.w3c.dom.Element} (XML file). 046 * DOM Element can be read from InputStream, File or URL. 047 * Instantiation and configuration happens as follows:<P/> 048 * <B>Instantiation</B> 049 * <ul> 050 * <li>If there is no <code>'type'</code> attribute then type defaults to 051 * {@link java.lang.String} and text of the element will be returned. 052 * E.g. <name>Pavel</name> will yield string 'Pavel'. <code>'type'</code> 053 * attribute name can be changed through {@link biz.hammurapi.config.DomConfigInfo#setCodeExpression(String)} 054 * method. Create {@link biz.hammurapi.config.DomConfigInfo}, change code expression and then 055 * use {@link #DomConfigFactory(DomConfigInfo)} to instantiate DomConfigFactory. 056 * </li> 057 * 058 * <li>Otherwise class specified in <code>'type'</code> attribute will be loaded and 059 * verified by classAcceptor (if any)</li> 060 * 061 * <li>If there is no nested <code>'constructor'</code>' element and element text is blank then default 062 * constructor will be used</li> 063 * 064 * <li>If there is no nested <code>'constructor'</code>' element and element text is not blank then constructor which takes a single argument of type 065 * java.lang.String will be used</li> 066 * 067 * <li>If there is nested <code>'constructor'</code> element then <code>'arg'</code> elements of 068 * <code>'constructor'</code> element are iterated to create a list of arguments. 069 * Arguments are constructed in the same way as described here. <code>'arg'</code> element 070 * also supports <code>'context-ref'</code> attribute. If this attribute is set argument 071 * value will be taken from context entry set by {@link DomConfigFactory#setContextEntry(String, Object)} method 072 * </li> 073 * </ul> 074 * <P/> 075 * 076 * <B>Configuration</B> 077 * <ul> 078 * <li>If element has attribute <code>'url'</code> and instantiated object (instance) 079 * is instance of {@link biz.hammurapi.config.URLConfigurable} then {@link biz.hammurapi.config.URLConfigurable#configure(URL, Map)} 080 * is invoked to configure instance</li> 081 * 082 * <li>If element has attribute <code>'file'</code> and instance 083 * is instance of {@link biz.hammurapi.config.FileConfigurable} then {@link biz.hammurapi.config.FileConfigurable#configure(File, Map)} 084 * is invoked to configure instance</li> 085 * 086 * <li>If instance is instance of 087 * {@link biz.hammurapi.config.InputStreamConfigurable} 088 * then <ul> 089 * <li>If element has attribute <code>'url'</code> then that url is opened as InsputStream</li> 090 * <li>If element has attribute <code>'file'</code> then that file is opened as InputStream</li> 091 * <li>If element has attribute <code>'resource'</code> then that resource is opened as InputStream. 092 * Instance's class is used to obtain resource which allows to use relative resource names.</li> 093 * </ul> 094 * then that InputStream is passed to 095 * {@link biz.hammurapi.config.InputStreamConfigurable#configure(InputStream, Map)} 096 * to configure instance. If none of aforementioned attributes is present then ConfigurationException is thrown.</li> 097 * 098 * <li>If instance is instance of {@link biz.hammurapi.config.DomConfigurable} then 099 * <ul> 100 * <li>If element has attribute <code>'url'</code> then that url is opened as InsputStream and parsed to DOM tree</li> 101 * <li>If element has attribute <code>'file'</code> then that file is opened as InputStream and parsed to DOM tree</li> 102 * <li>If element has attribute <code>'resource'</code> then that resource is opened as InputStream and parsed to DOM tree. 103 * Instance's class is used to obtain resource which allows to use relative resource names.</li> 104 * </ul> 105 * then that parsed document is passed to {@link biz.hammurapi.config.DomConfigurable#configure(Node, Context, ClassLoader)}. 106 * If none of the aforementioned attributes is present then element itself is passed to 107 * {@link biz.hammurapi.config.DomConfigurable#configure(Node, Context, ClassLoader)}</li> 108 * 109 * <li>If instance is instance of {@link biz.hammurapi.config.Parameterizable} then 110 * <ul> 111 * <li>If there are subelements <code>'parameter'</code> with attribute <code>'name'</code> 112 * then value of <code>'name'</code> is used as parameter name</li> 113 * <li>Otherwise names of nested elements used as parameter names</li> 114 * </ul> 115 * Parameter values are evaluated in the same way as <code>'arg'</code> elements for 116 * constructors. 117 * {@link biz.hammurapi.config.Parameterizable#setParameter(String, Object)} is invoked for each of parameter elements. 118 * 119 * {@link biz.hammurapi.config.Parameterizable#setParameter(String, Object)} is also invoked for context entries 120 * with names which did not match with parameter names. E.g. if there are two context entries 'age' and 'name' and parameter 121 * 'name' then setParameter("name", <I>value of parameter 'name'</I>) will be invoked and after that 122 * setParameter("age", <I>value of context entry 'age'</I>) will be invoked. 123 * </li> 124 * 125 * <li>If instance is instance of {@link biz.hammurapi.config.StringConfigurable} then element text is passed to 126 * {@link StringConfigurable#configure(String, Map)} method</li> 127 * 128 * <li>If instance is instance of {@link java.util.Map} then <code>'entry'</code> subelements are iterated; <code>'key'</code> 129 * (Configurable through {@link biz.hammurapi.config.DomConfigInfo}) and <code>'value'</code> 130 * (Configurable through {@link biz.hammurapi.config.DomConfigInfo}) subelements are evaluated in the same way as 131 * <code>'arg'</code> constructors subelements and put to instance by {@link java.util.Map#put(java.lang.Object, java.lang.Object)}</li> 132 * 133 * <li>If instance is instance of {@link java.util.Collection} then <code>'element'</code> subelements are iterated, elements 134 * are istantiated in the same way as constructor arguments and then placed into instance by invoking {@link java.util.Collection#add(java.lang.Object)} 135 * method.</li> 136 * 137 * <li>If none of above conditions are met then reflection is used to inject values into instance fields/properties in a similar way as parameters for 138 * {@link biz.hammurapi.config.Parameterizable} are set. Special note about injection: If field type or setter parameter type (target type) is compatible with 139 * instantiated value then the value is used as is. Otherwise if target type is compatible with source XML Element then the element is used. If value is instance of 140 * {@link biz.hammurapi.config.Wrapper} and wrapper's master is compatible with the target type then the master is used. If wrapper is also a component then its setOwner() and start() methods 141 * are invoked before obtaining master. If none of aforementioned conditions are true then value is converted to target type. 142 * using {@link biz.hammurapi.convert.CompositeConverter}.</li> 143 * 144 * <li>If object acceptor is not null then its {@link biz.hammurapi.config.ObjectAcceptor#accept(Object)} is invoked 145 * to validate that object has been constructed and configured correctly</li> 146 * 147 * <li>If instance is instance of {@link biz.hammurapi.config.Validatable} then {@link biz.hammurapi.config.Validatable#validate()} is 148 * invoked for the instance to validate itself. 149 * </ul> 150 * 151 * <B>Examples</B> 152 * <OL> 153 * <li><CODE><name>Pavel</name></CODE> will yield java.lang.String with value 'Pavel'</li> 154 * <li><CODE><age type="java.lang.Integer">33</age></CODE> will yield java.lang.Integer with value '33'</li> 155 * <li><CODE><config type="org.myself.myproject.MyConfig" url="http://myproject.myself.org/MyConfig.xml"/></CODE> will load 156 * configuration from URL and configure MyConfig object</li> 157 * <li><PRE><config type="org.myself.myproject.MyParameterizableConfig"> 158 * <parameter name="pi" type="java.lang.Double">3.14159</parameter> 159 * </config></PRE> will create MyParameterizableConfig object and then invoke its setParameter() method if MyParameterizableConfig 160 * implements {@link biz.hammurapi.config.Parameterizable} or invoke setPi() method if there is such method. In lenient mode 161 * nothing will happen if there is no setPi() method. Otherwise exception will be thrown.</li> 162 * <li><PRE><config type="org.myself.myproject.MyParameterizableConfig"> 163 * <pi type="java.lang.Double">3.14159</pi> 164 * </config></PRE> same as above.</li> 165 * </OL> 166 * 167 * It is recommended to use XML Beans generated classes instead of this factory. 168 * 169 * @author Pavel Vlasov 170 * @version $Revision: 1.12 $ 171 */ 172 public class DomConfigFactory { 173 public static final String XML_EXTENSION = ".xml"; 174 public static final String CONFIG_RESOURCE_PREFIX = "META-INF/config/"; 175 176 public static final String RESOURCE_PREFIX = "resource:"; 177 public static final String CLASS_LOADER = ClassLoader.class.getName(); 178 public static final String CODE_EXPRESSION = "@type"; 179 public static final String MAP_KEY_EXPRESSION = "key"; 180 public static final String MAP_VALUE_EXPRESSION = "value"; 181 182 private static final String CONTEXT_REF = "context-ref"; 183 public static final Map PRIMITIVES; 184 private Context context; 185 186 static { 187 Map primitives=new HashMap(); 188 primitives.put("boolean", Boolean.TYPE); 189 primitives.put("byte", Byte.TYPE); 190 primitives.put("char", Character.TYPE); 191 primitives.put("double", Double.TYPE); 192 primitives.put("float", Float.TYPE); 193 primitives.put("int", Integer.TYPE); 194 primitives.put("long", Long.TYPE); 195 primitives.put("short", Short.TYPE); 196 PRIMITIVES=Collections.unmodifiableMap(primitives); 197 } 198 199 private ClassLoader classLoader; 200 201 /** 202 * Default constructor 203 */ 204 public DomConfigFactory() { 205 super(); 206 } 207 208 /** 209 * Default constructor 210 */ 211 public DomConfigFactory(Context context) { 212 super(); 213 this.context=context; 214 } 215 216 public DomConfigFactory(ClassLoader classLoader) { 217 super(); 218 this.classLoader=classLoader; 219 } 220 221 public DomConfigFactory(ClassLoader classLoader, Context context) { 222 super(); 223 this.classLoader=classLoader; 224 this.context=context; 225 } 226 227 /** 228 * Creates object. Same as create(node, null, null) 229 * @param node 230 * @return 231 * @throws ConfigurationException 232 */ 233 public Object create(Node node) throws ConfigurationException { 234 return create(node, null, null); 235 } 236 237 /** 238 * Parses file and returns object. Same as create(file, xPath, null, null) 239 * @param file XML configuration file 240 * @param xPath XPath expression, can be null 241 * @return configured object 242 */ 243 public Object create(File file, String xPath) throws ConfigurationException, IOException { 244 return create(file, xPath, null, null); 245 } 246 247 /** 248 * Parses file and returns object 249 * @param file XML configuration file 250 * @param xPath XPath expression, can be null 251 * @param classAcceptor Class acceptor, validates that class about to be instantiated is 'the right one' 252 * @param objectAcceptor Object acceptor, validates instantiated object. 253 * @return Configured object 254 */ 255 public Object create(File file, String xPath, ClassAcceptor classAcceptor, ObjectAcceptor objectAcceptor) throws ConfigurationException, IOException { 256 try { 257 Node node = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file).getDocumentElement(); 258 if (xPath!=null) { 259 node=DOMUtils.selectSingleNode(node, xPath); 260 } 261 return create(node, classAcceptor, objectAcceptor); 262 } catch (Exception e) { 263 throw new ConfigurationException(e); 264 } 265 } 266 267 /** 268 * Same as create(in, xPath, null, null) 269 * @param in Input stream 270 * @param xPath XPath expression, can be null 271 * @return Configured object 272 * @throws ConfigurationException 273 * @throws IOException 274 */ 275 public Object create(InputStream in, String xPath) throws ConfigurationException, IOException { 276 return create(in, xPath, null, null); 277 } 278 279 /** 280 * Creates and configures object from InputStream 281 * @param in Input stream 282 * @param xPath XPath expression, can be null 283 * @param classAcceptor 284 * @param objectAcceptor 285 * @return Configured object 286 * @throws ConfigurationException 287 * @throws IOException 288 */ 289 public Object create(InputStream in, String xPath, ClassAcceptor classAcceptor, ObjectAcceptor objectAcceptor) throws ConfigurationException, IOException { 290 try { 291 Node node = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(in).getDocumentElement(); 292 if (xPath!=null) { 293 node=DOMUtils.selectSingleNode(node, xPath); 294 } 295 return create(node, classAcceptor, objectAcceptor); 296 } catch (Exception e) { 297 throw new ConfigurationException(e); 298 } 299 } 300 301 /** 302 * Same as create(in, xPath, null, null) 303 * @param in Reader 304 * @param xPath XPath expression, can be null 305 * @return Configured object 306 * @throws ConfigurationException 307 * @throws IOException 308 */ 309 public Object create(Reader in, String xPath) throws ConfigurationException, IOException { 310 return create(in, xPath, null, null); 311 } 312 313 /** 314 * Creates and configures object from InputStream 315 * @param in Reader 316 * @param xPath XPath expression, can be null 317 * @param classAcceptor 318 * @param objectAcceptor 319 * @return Configured object 320 * @throws ConfigurationException 321 * @throws IOException 322 */ 323 public Object create(Reader in, String xPath, ClassAcceptor classAcceptor, ObjectAcceptor objectAcceptor) throws ConfigurationException, IOException { 324 try { 325 Node node = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(in)).getDocumentElement(); 326 if (xPath!=null) { 327 node=DOMUtils.selectSingleNode(node, xPath); 328 } 329 return create(node, classAcceptor, objectAcceptor); 330 } catch (Exception e) { 331 throw new ConfigurationException(e); 332 } 333 } 334 335 /** 336 * Same as create(url, xPath, null, null) 337 * @param url URL to read configuration from 338 * @param xPath XPath expression, can be null 339 * @return Configured object 340 * @throws ConfigurationException 341 * @throws IOException 342 */ 343 public Object create(URL url, String xPath) throws ConfigurationException, IOException { 344 return create(url, xPath, null, null); 345 } 346 347 /** 348 * Creates and configures object from URL 349 * @param url Url 350 * @param xPath XPath expression, can be null 351 * @param classAcceptor 352 * @param objectAcceptor 353 * @return Configured object 354 * @throws ConfigurationException 355 * @throws IOException 356 */ 357 public Object create(URL url, String xPath, ClassAcceptor classAcceptor, ObjectAcceptor objectAcceptor) throws ConfigurationException, IOException { 358 try { 359 Node node = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(url.openStream()).getDocumentElement(); 360 if (xPath!=null) { 361 node=DOMUtils.selectSingleNode(node, xPath); 362 } 363 return create(node, classAcceptor, objectAcceptor); 364 } catch (Exception e) { 365 throw new ConfigurationException(e); 366 } 367 } 368 369 /** 370 * Creates and configures object 371 * @param node 372 * @param classAcceptor 373 * @param objectAcceptor 374 * @param cxpa Cached XPath API to accelerate XPath expressions evaluation. 375 * @return Configured object 376 * @throws ConfigurationException 377 */ 378 public Object create(Node node, ClassAcceptor classAcceptor, ObjectAcceptor objectAcceptor) throws ConfigurationException { 379 try { 380 String className=DOMUtils.eval(node, CODE_EXPRESSION).toString(); 381 if (className.trim().length()==0) { 382 if (classAcceptor!=null) { 383 classAcceptor.accept(String.class); 384 } 385 return DOMUtils.eval(node, "text()").toString(); 386 } 387 388 Class clazz = forName(className); 389 if (classAcceptor!=null) { 390 classAcceptor.accept(clazz); 391 } 392 393 Object instance; 394 Node constructorNode=DOMUtils.selectSingleNode(node, "constructor"); 395 if (constructorNode==null) { 396 String body=DOMUtils.eval(node, "text()").toString().trim(); 397 if (body.length()==0 || DomConfigurable.class.isAssignableFrom(clazz)) { 398 instance=clazz.newInstance(); 399 } else { 400 Constructor c=clazz.getConstructor(new Class[] {String.class}); 401 instance=c.newInstance(new Object[] {body}); 402 } 403 } else { 404 NodeList argList=DOMUtils.selectNodeList(constructorNode, "arg"); 405 Class[] argTypes=new Class[argList.getLength()]; 406 for (int i=0; i<argList.getLength(); i++) { 407 String argTypeName=DOMUtils.eval(argList.item(i), CODE_EXPRESSION).toString(); 408 if (argTypeName.trim().length()==0) { 409 argTypes[i]=String.class; 410 } else { 411 argTypes[i]=(Class) PRIMITIVES.get(argTypeName); 412 if (argTypes[i]==null) { 413 argTypes[i]=forName(argTypeName); 414 } 415 } 416 } 417 418 Constructor constructor=clazz.getConstructor(argTypes); 419 Object[] args=new Object[argList.getLength()]; 420 for (int i=0; i<argList.getLength(); i++) { 421 Element argElement = ((Element) argList.item(i)); 422 if (argTypes[i].isPrimitive()) { 423 args[i] = ConvertingService.convert( 424 DOMUtils.eval(argList.item(i), "text()").toString(), 425 argTypes[i]); 426 } else if (argElement.hasAttribute(CONTEXT_REF)) { 427 args[i]=context.get(argElement.getAttribute(CONTEXT_REF)); 428 } else { 429 args[i]=create(argList.item(i),null,null); 430 } 431 } 432 instance=constructor.newInstance(args); 433 } 434 435 if (hasAttribute(node, "url") && instance instanceof URLConfigurable) { 436 ((URLConfigurable) instance).configure(new URL(((Element) node).getAttribute("url")), context, classLoader); 437 } else if (hasAttribute(node, "file") && instance instanceof FileConfigurable) { 438 ((FileConfigurable) instance).configure(new File(((Element) node).getAttribute("file")), context, classLoader); 439 } else if (instance instanceof InputStreamConfigurable) { 440 if (hasAttribute(node, "url")) { 441 new URL2InputStreamConfigurableAdapter((InputStreamConfigurable) instance).configure(new URL(((Element) node).getAttribute("url")), context, classLoader); 442 } else if (hasAttribute(node, "file")) { 443 new File2InputStreamConfigurableAdapter((InputStreamConfigurable) instance).configure(new File(((Element) node).getAttribute("file")), context, classLoader); 444 } else if (hasAttribute(node, "resource")) { 445 ((InputStreamConfigurable) instance).configure(clazz.getResourceAsStream(((Element) node).getAttribute("resource")), context, classLoader); 446 } else { 447 throw new ConfigurationException("Cannot configure "+instance.getClass().getName()); 448 } 449 } else if (instance instanceof DomConfigurable) { // Dom configurable 450 if (hasAttribute(node, "url")) { 451 new URL2InputStreamConfigurableAdapter( 452 new InputStream2DomConfigurableAdapter( 453 (DomConfigurable) instance)).configure(new URL(((Element) node).getAttribute("url")), context, classLoader); 454 } else if (hasAttribute(node, "file")) { 455 new File2InputStreamConfigurableAdapter( 456 new InputStream2DomConfigurableAdapter( 457 (DomConfigurable) instance)).configure(new File(((Element) node).getAttribute("file")), context, classLoader); 458 } else if (hasAttribute(node, "resource")) { 459 new InputStream2DomConfigurableAdapter((DomConfigurable) instance).configure(getClass().getResourceAsStream(((Element) node).getAttribute("resource")), context, classLoader); 460 } else { 461 ((DomConfigurable) instance).configure(node, context, classLoader); 462 } 463 } else if (instance instanceof Parameterizable) { // Parameterizable 464 Map localContext=new HashMap(); 465 466 if (DOMUtils.selectSingleNode(node, "parameter[@name]")==null) { 467 // Use element names as parameter names 468 NodeList nl=node.getChildNodes(); 469 for (int i=0; i<nl.getLength(); i++) { 470 if (nl.item(i) instanceof Element) { 471 String parameterName = nl.item(i).getNodeName(); 472 ((Parameterizable) instance).setParameter(parameterName, getValue((Element) nl.item(i))); 473 localContext.remove(parameterName); 474 } 475 } 476 } else { 477 NodeList nl=DOMUtils.selectNodeList(node, "parameter[@name]"); 478 for (int i=0, l=nl.getLength(); i<l; ++i) { 479 Element paramElement = (Element) nl.item(i); 480 String parameterName = paramElement.getAttribute("name"); 481 ((Parameterizable) instance).setParameter(parameterName, getValue(paramElement)); 482 localContext.remove(parameterName); 483 } 484 } 485 486 Iterator it=localContext.entrySet().iterator(); 487 while (it.hasNext()) { 488 Map.Entry entry=(Map.Entry) it.next(); 489 ((Parameterizable) instance).setParameter((String) entry.getKey(), entry.getValue()); 490 } 491 } else if (instance instanceof StringConfigurable) { // String configurable 492 ((StringConfigurable) instance).configure(DOMUtils.eval(node, "text()").toString(), context); 493 } else if (instance instanceof Map) { // Map 494 NodeList nl=DOMUtils.selectNodeList(node, "entry"); 495 for (int i=0, l=nl.getLength(); i<l; ++i) { 496 Element entryElement = (Element) nl.item(i); 497 ((Map) instance).put(getValue((Element) DOMUtils.selectSingleNode(entryElement,MAP_KEY_EXPRESSION)), 498 getValue((Element) DOMUtils.selectSingleNode(entryElement,MAP_VALUE_EXPRESSION))); 499 } 500 } else if (instance instanceof Collection) { // Collection 501 NodeList nl=DOMUtils.selectNodeList(node, "element"); 502 for (int i=0, l=nl.getLength(); i<l; ++i) { 503 Element element = (Element) nl.item(i); 504 ((Collection) instance).add(getValue(element)); 505 } 506 } else { 507 Map toInject=new HashMap(); 508 Context localContext=new MapContext(toInject, context); 509 510 if (DOMUtils.selectSingleNode(node, "parameter[@name]")==null) { 511 // Use element names as parameter names 512 NodeList nl=node.getChildNodes(); 513 for (int i=0; i<nl.getLength(); i++) { 514 if (nl.item(i) instanceof Element) { 515 String name = nl.item(i).getNodeName(); 516 Element element = (Element) nl.item(i); 517 Object existing = toInject.get(name); 518 if (existing==null) { 519 toInject.put(name, getInjectEntry(element)); 520 } else if (existing instanceof InjectEntryCollection) { 521 ((Collection) existing).add(getInjectEntry(element)); 522 } else { 523 Collection col = new InjectEntryCollection(); 524 col.add(existing); 525 col.add(getInjectEntry(element)); 526 toInject.put(name, col); 527 } 528 } 529 } 530 } else { 531 NodeList nl =DOMUtils.selectNodeList(node, "parameter[@name]"); 532 for (int i=0, l=nl.getLength(); i<l; ++i) { 533 Element paramElement = (Element) nl.item(i); 534 String name = paramElement.getAttribute("name"); 535 Object existing = toInject.get(name); 536 if (existing==null) { 537 toInject.put(name, getInjectEntry(paramElement)); 538 } else if (existing instanceof InjectEntryCollection) { 539 ((Collection) existing).add(getInjectEntry(paramElement)); 540 } else { 541 Collection col = new InjectEntryCollection(); 542 col.add(existing); 543 col.add(getInjectEntry(paramElement)); 544 toInject.put(name, col); 545 } 546 } 547 } 548 549 inject(instance, localContext); 550 } 551 552 if (objectAcceptor!=null) { 553 objectAcceptor.accept(instance); 554 } 555 556 if (instance instanceof Validatable) { 557 ((Validatable) instance).validate(); 558 } 559 560 return instance; 561 } catch (Exception e) { 562 throw new ConfigurationException(e); 563 } 564 } 565 566 /** 567 * Marker class 568 * @author Tatyana Konukova 569 * 570 */ 571 private static class InjectEntryCollection extends ArrayList { 572 573 } 574 575 /** 576 * Holder for element and its instantiation. 577 * @author Pavel Vlasov 578 * @revision $Revision$ 579 */ 580 private class InjectEntry { 581 Element element; 582 Object instance; 583 584 /** 585 * @param element 586 * @param instance 587 */ 588 public InjectEntry(Element element, Object instance) { 589 super(); 590 this.element = element; 591 this.instance = instance; 592 } 593 594 } 595 596 /** 597 * @param className 598 * @return 599 * @throws ClassNotFoundException 600 */ 601 private Class forName(String className) throws ClassNotFoundException { 602 return classLoader==null ? Class.forName(className) : classLoader.loadClass(className); 603 } 604 605 /** 606 * Converts instantiated value to InjectEntry 607 * @param cxpa 608 * @param parameterElement 609 * @return 610 * @throws ConfigurationException 611 */ 612 private InjectEntry getInjectEntry(Element parameterElement) throws ConfigurationException { 613 return new InjectEntry(parameterElement, getValue(parameterElement)); 614 } 615 616 /** 617 * @param cxpa 618 * @param parameterElement 619 * @return 620 * @throws ConfigurationException 621 */ 622 private Object getValue(Element parameterElement) throws ConfigurationException { 623 if (parameterElement.hasAttribute(CONTEXT_REF)) { 624 return context.get((parameterElement).getAttribute(CONTEXT_REF)); 625 } 626 627 return create(parameterElement,null,null); 628 } 629 630 631 /** 632 * Converts InjectEntry to target class. 633 * Takes into account wrappers and org.w3c.Element 634 * @param entry 635 * @param targetClass 636 * @return 637 */ 638 private static Object getValue(Object value, Class targetClass, Object owner) { 639 640 if (value==null) { 641 return null; 642 } 643 644 if (value instanceof InjectEntryCollection) { 645 return value; 646 } 647 648 if (value instanceof InjectEntry) { 649 InjectEntry injectEntry=(InjectEntry) value; 650 651 // If instance is already compatible - return instance. 652 if (targetClass.isInstance(injectEntry.instance)) { 653 return injectEntry.instance; 654 } 655 656 // If element is compatible - return element 657 if (targetClass.isInstance(injectEntry.element)) { 658 return injectEntry.element; 659 } 660 661 // If is Wrapper and master is compatible - return master 662 if (injectEntry.instance instanceof Wrapper) { 663 //Start wrapper if it is also a component. 664 if (injectEntry.instance instanceof Component) { 665 try { 666 Component component = (Component) injectEntry.instance; 667 component.setOwner(owner); 668 component.start(); 669 } catch (ConfigurationException e) { 670 throw new RuntimeConfigurationException("Could not start wrapper component "+injectEntry.instance.getClass().getName()+": "+e, e); 671 } 672 } 673 Object master=((Wrapper) injectEntry.instance).getMaster(); 674 if (targetClass.isInstance(master)) { 675 return master; 676 } 677 } 678 679 // The last resort - use converter. 680 return ConvertingService.convert(injectEntry.instance, targetClass); 681 } 682 683 // If instance is already compatible - return instance. 684 if (targetClass.isInstance(value)) { 685 return value; 686 } 687 688 // The last resort - use converter. 689 return ConvertingService.convert(value, targetClass); 690 } 691 692 /** 693 * Sets property (field or through setter or appender) using reflection. 694 * This method can inject single or multiple values from the context to the instance 695 * using setXXX(argType arg), setXXX(argType[] arg), setXXX(Collection args), or 696 * addXXX(argType arg). For multi-value injections the context shall return collection, 697 * array, or iterator instance from its get() method. 698 * @param instance 699 * @param context Source of data to inject. 700 */ 701 public static void inject(Object instance, Context context) throws ConfigurationException { 702 Method[] ma=instance.getClass().getMethods(); 703 704 Set usedKeys = new HashSet(); 705 LinkedList setters = new LinkedList(); 706 LinkedList appenders = new LinkedList(); 707 for (int i=0; i<ma.length; i++) { 708 if (Modifier.isPublic(ma[i].getModifiers()) && ma[i].getParameterTypes().length==1) { 709 if (ma[i].getName().startsWith("set")) { 710 setters.add(ma[i]); 711 } else if (ma[i].getName().startsWith("add")) { 712 appenders.add(ma[i]); 713 } 714 } 715 } 716 717 // Injection of single values using setters. 718 Method[] sa = (Method[]) setters.toArray(new Method[setters.size()]); 719 for (int i=0; i<sa.length; ++i) { 720 Method m=sa[i]; 721 if (m!=null) { 722 String methodName = m.getName(); 723 int methodIndex = i; 724 int methodNameLength = methodName.length(); 725 Class pt = m.getParameterTypes()[0]; 726 String key=methodNameLength==3 ? pt.getName() : (Character.toLowerCase(methodName.charAt(3)) + (methodNameLength<5 ? "" : methodName.substring(4))); 727 if (!usedKeys.contains(key)) { 728 Object value=getValue(context.get(key), pt, instance); 729 730 // Take single value. 731 if (value instanceof InjectEntryCollection) { 732 if (!((Collection) value).isEmpty() && !pt.isArray()) { 733 value = getValue(((InjectEntryCollection) value).get(0), pt, instance); 734 } else { 735 value = null; 736 } 737 } 738 739 if (value!=null) { 740 for (int j=0; j<sa.length; ++j) { 741 Method m2=sa[j]; 742 if (m2!=null && m2.getName().equals(m.getName()) && pt.isAssignableFrom(m2.getParameterTypes()[0])) { 743 m=m2; 744 methodIndex=j; 745 } 746 } 747 748 try { 749 m.invoke(instance, new Object[] {value}); 750 } catch (Exception e) { 751 throw new ConfigurationException(e); 752 } 753 754 usedKeys.add(key); 755 setters.remove(m); 756 sa[methodIndex]=null; 757 } 758 } 759 } 760 } 761 762 // Injection of single values using appenders. 763 Method[] aa = (Method[]) appenders.toArray(new Method[appenders.size()]); 764 for (int i=0; i<aa.length; ++i) { 765 Method m=aa[i]; 766 if (m!=null) { 767 String methodName = m.getName(); 768 int methodIndex = i; 769 int methodNameLength = methodName.length(); 770 Class pt = m.getParameterTypes()[0]; 771 String key=methodNameLength==3 ? pt.getName() : (Character.toLowerCase(methodName.charAt(3)) + (methodNameLength<5 ? "" : methodName.substring(4))); 772 if (!usedKeys.contains(key)) { 773 Object cValue = context.get(key); 774 Object value=getValue(cValue, pt, instance); 775 776 if (value!=null && !(value instanceof InjectEntryCollection)) { 777 for (int j=0; j<aa.length; ++j) { 778 Method m2=aa[j]; 779 if (m2!=null && m2.getName().equals(m.getName()) && pt.isAssignableFrom(m2.getParameterTypes()[0])) { 780 m=m2; 781 methodIndex=j; 782 } 783 } 784 785 try { 786 m.invoke(instance, new Object[] {value}); 787 } catch (Exception e) { 788 throw new ConfigurationException(e); 789 } 790 791 usedKeys.add(key); 792 appenders.remove(m); 793 aa[methodIndex] = null; 794 } 795 } 796 } 797 } 798 799 // Injection of multiple values using appenders. 800 Iterator ait = appenders.iterator(); 801 while (ait.hasNext()) { 802 Method m=(Method) ait.next(); 803 String methodName = m.getName(); 804 int methodNameLength = methodName.length(); 805 Class pt = m.getParameterTypes()[0]; 806 String key=methodNameLength==3 ? pt.getName() : (Character.toLowerCase(methodName.charAt(3)) + (methodNameLength<5 ? "" : methodName.substring(4))); 807 if (!usedKeys.contains(key)) { 808 Object val=context.get(key); 809 Iterator vit = null; 810 if (val instanceof Iterator) { 811 vit = (Iterator) val; 812 } else if (val instanceof Collection) { 813 vit = ((Collection) val).iterator(); 814 } else if (val!=null && val.getClass().isArray()) { 815 Collection col = new ArrayList(); 816 for (int i=0, l=Array.getLength(val); i<l; ++i) { 817 col.add(Array.get(val, i)); 818 } 819 vit = col.iterator(); 820 } 821 822 if (vit!=null) { 823 usedKeys.add(key); 824 while (vit.hasNext()) { 825 Object value = getValue(vit.next(), pt, instance); 826 if (value!=null) { 827 try { 828 m.invoke(instance, new Object[] {value}); 829 } catch (Exception e) { 830 throw new ConfigurationException(e); 831 } 832 } 833 } 834 ait.remove(); 835 } 836 } 837 } 838 839 // Injection of multiple values using setters which take collections. 840 Iterator sit=setters.iterator(); 841 while (sit.hasNext()) { 842 Method m=(Method) sit.next(); 843 String methodName = m.getName(); 844 int methodNameLength = methodName.length(); 845 Class pt = m.getParameterTypes()[0]; 846 String key=methodNameLength==3 ? pt.getName() : (Character.toLowerCase(methodName.charAt(3)) + (methodNameLength<5 ? "" : methodName.substring(4))); 847 if (!usedKeys.contains(key) && pt.isAssignableFrom(ArrayList.class)) { 848 List values = new ArrayList(); 849 850 Object val=context.get(key); 851 if (val instanceof Iterator) { 852 Iterator vit = (Iterator) val; 853 while (vit.hasNext()) { 854 values.add(getValue(vit.next(), Object.class, instance)); 855 } 856 857 } else if (val instanceof Collection) { 858 Iterator vit = ((Collection) val).iterator(); 859 while (vit.hasNext()) { 860 values.add(getValue(vit.next(), Object.class, instance)); 861 } 862 } else if (val!=null && val.getClass().isArray()) { 863 for (int i=0, l=Array.getLength(val); i<l; ++i) { 864 values.add(Array.get(val, i)); 865 } 866 } else { 867 Object vl = getValue(val, Object.class, instance); 868 if (vl!=null) { 869 values.add(vl); 870 } 871 } 872 873 if (!values.isEmpty()) { 874 try { 875 m.invoke(instance, new Object[] {values}); 876 usedKeys.add(key); 877 } catch (Exception e) { 878 throw new ConfigurationException(e); 879 } 880 } 881 sit.remove(); 882 } 883 } 884 885 // Injection of multiple values using setters which take arrays. 886 sit=setters.iterator(); 887 while (sit.hasNext()) { 888 Method m=(Method) sit.next(); 889 String methodName = m.getName(); 890 int methodNameLength = methodName.length(); 891 Class pt = m.getParameterTypes()[0]; 892 String key=methodNameLength==3 ? pt.getName() : (Character.toLowerCase(methodName.charAt(3)) + (methodNameLength<5 ? "" : methodName.substring(4))); 893 if (!usedKeys.contains(key) && pt.isArray()) { 894 List values = new ArrayList(); 895 896 Object val=context.get(key); 897 Class componentType = pt.getComponentType(); 898 if (val instanceof Iterator) { 899 Iterator vit = (Iterator) val; 900 while (vit.hasNext()) { 901 values.add(getValue(vit.next(), componentType, instance)); 902 } 903 } else if (val instanceof Collection) { 904 Iterator vit = ((Collection) val).iterator(); 905 while (vit.hasNext()) { 906 values.add(getValue(vit.next(), componentType, instance)); 907 } 908 } else if (val!=null && val.getClass().isArray()) { 909 for (int i=0, l=Array.getLength(val); i<l; ++i) { 910 values.add(Array.get(val, i)); 911 } 912 } else { 913 Object vl = getValue(val, componentType, instance); 914 if (vl!=null) { 915 values.add(vl); 916 } 917 } 918 919 if (!values.isEmpty()) { 920 try { 921 Object array = Array.newInstance(componentType, values.size()); 922 Iterator vit = values.iterator(); 923 for (int i=0; vit.hasNext(); ++i) { 924 Array.set(array, i, vit.next()); 925 } 926 m.invoke(instance, new Object[] {array}); 927 usedKeys.add(key); 928 } catch (Exception e) { 929 throw new ConfigurationException(e); 930 } 931 } 932 sit.remove(); 933 } 934 } 935 936 // Injection of first of multiple values using setters. 937 sit = setters.iterator(); 938 while (sit.hasNext()) { 939 Method m=(Method) sit.next(); 940 String methodName = m.getName(); 941 int methodNameLength = methodName.length(); 942 Class pt = m.getParameterTypes()[0]; 943 String key=methodNameLength==3 ? pt.getName() : (Character.toLowerCase(methodName.charAt(3)) + (methodNameLength<5 ? "" : methodName.substring(4))); 944 if (!usedKeys.contains(key)) { 945 Object val=context.get(key); 946 Iterator vit = null; 947 if (val instanceof Iterator) { 948 vit = (Iterator) val; 949 } else if (val instanceof Collection) { 950 vit = ((Collection) val).iterator(); 951 } else if (val!=null && val.getClass().isArray()) { 952 Collection col = new ArrayList(); 953 for (int i=0, l=Array.getLength(val); i<l; ++i) { 954 col.add(Array.get(val, i)); 955 } 956 vit = col.iterator(); 957 } 958 959 if (vit!=null) { 960 usedKeys.add(key); 961 while (vit.hasNext()) { 962 Object value = getValue(vit.next(), pt, instance); 963 if (value!=null) { 964 try { 965 m.invoke(instance, new Object[] {value}); 966 } catch (Exception e) { 967 throw new ConfigurationException(e); 968 } 969 break; 970 } 971 } 972 sit.remove(); 973 } 974 } 975 } 976 977 978 Field[] fa=instance.getClass().getFields(); 979 for (int i=0; i<fa.length; i++) { 980 if (Modifier.isPublic(fa[i].getModifiers()) && usedKeys.add(fa[i].getName())) { 981 try { 982 Object value=getValue(context.get(fa[i].getName()), fa[i].getType(), instance); 983 if (value!=null) { 984 fa[i].set(instance, value); 985 } 986 } catch (IllegalArgumentException e) { 987 throw new ConfigurationException(e); 988 } catch (IllegalAccessException e) { 989 throw new ConfigurationException(e); 990 } 991 } 992 } 993 } 994 995 /** 996 * @param node 997 * @return 998 */ 999 private boolean hasAttribute(Node node, String attribute) { 1000 return node instanceof Element && ((Element) node).hasAttribute(attribute); 1001 } 1002 1003 public static void main(final String[] args) { 1004 final long start=System.currentTimeMillis(); 1005 if (args.length==0) { 1006 System.err.println("Usage: java <options> "+DomConfigFactory.class.getName()+" <configuration URL> [<additional parameters>]"); 1007 System.exit(1); 1008 } 1009 1010 final boolean stopInHook = "yes".equalsIgnoreCase(System.getProperty("biz.hammurapi.config.DomConfigFactory:shutdownHook")); 1011 final Object[] oa = {null}; 1012 1013 if (stopInHook) { 1014 Runtime.getRuntime().addShutdownHook( 1015 new Thread() { 1016 public void run() { 1017 if (oa[0] instanceof Component) { 1018 try { 1019 ((Component) oa[0]).stop(); 1020 } catch (ConfigurationException e) { 1021 System.err.println("Could not properly stop "+oa[0]); 1022 e.printStackTrace(); 1023 } 1024 System.out.println("Total execution time: "+((System.currentTimeMillis()-start)/1000)+" sec."); 1025 } 1026 } 1027 }); 1028 } 1029 1030 RestartCommand run = new RestartCommand() { 1031 1032 int attempt; 1033 1034 public void run() { 1035 try { 1036 if (attempt > 0) { 1037 long restartDelay = getRestartDelay(); 1038 System.out.print("Restarting in "+restartDelay+" milliseconds. Attempt " + (attempt+1)); 1039 try { 1040 Thread.sleep(restartDelay); 1041 } catch (InterruptedException ie) { 1042 ie.printStackTrace(); 1043 System.exit(4); 1044 } 1045 } 1046 1047 ++attempt; 1048 1049 DomConfigFactory factory=new DomConfigFactory(); 1050 1051 if (args[0].startsWith("url:")) { 1052 oa[0] = factory.create(new URL(args[0].substring("url:".length())), null); 1053 } else if (args[0].startsWith(RESOURCE_PREFIX)) { 1054 InputStream stream = DomConfigFactory.class.getClassLoader().getResourceAsStream(args[0].substring(RESOURCE_PREFIX.length())); 1055 if (stream==null) { 1056 System.err.println("Resource does not exist."); 1057 System.exit(1); 1058 } 1059 oa[0] = factory.create(stream, null); 1060 } else { 1061 File file = new File(args[0]); 1062 if (!file.exists()) { 1063 System.err.println("File does not exist or not a file."); 1064 System.exit(1); 1065 } 1066 if (!file.isFile()) { 1067 System.err.println("Not a file."); 1068 System.exit(1); 1069 } 1070 oa[0] = factory.create(file, null); 1071 } 1072 1073 if (oa[0] instanceof Component) { 1074 ((Component) oa[0]).start(); 1075 } 1076 1077 if (oa[0] instanceof Restartable) { 1078 ((Restartable) oa[0]).setRestartCommand(this); 1079 } 1080 1081 try { 1082 if (oa[0] instanceof Context) { 1083 Context container=(Context) oa[0]; 1084 for (int i=1; i<args.length; i++) { 1085 Object toExecute=container.get(args[i]); 1086 if (toExecute instanceof Command) { 1087 ((Command) toExecute).execute(args); 1088 } else if (toExecute==null) { 1089 System.err.print("[WARN] Name not found: " +args[i]); 1090 } else { 1091 System.err.print("[WARN] Not executable: (" +args[i]+") "+toExecute.getClass().getName()); 1092 } 1093 } 1094 } else if (oa[0] instanceof Command) { 1095 ((Command) oa[0]).execute(args); 1096 } 1097 } finally { 1098 if (oa[0] instanceof Component) { 1099 if (!stopInHook) { 1100 ((Component) oa[0]).stop(); 1101 } 1102 } 1103 } 1104 } catch (MalformedURLException e) { 1105 System.err.println("Bad configuration URL: "+args[0]); 1106 System.exit(2); 1107 } catch (Exception e) { 1108 e.printStackTrace(); 1109 if (attempt > 1) { 1110 if (oa[0] instanceof Component) { 1111 try { 1112 ((Component) oa[0]).stop(); 1113 } catch (Exception ex) { 1114 System.err.println("Cannot stop component before restart: "+e); 1115 ex.printStackTrace(); 1116 } 1117 } 1118 new Thread(this, "Restart thread "+getAttempt()).start(); // Use a new thread to avoid stack overflowing in the case of too many attempts. 1119 } else { 1120 System.exit(3); 1121 } 1122 } 1123 } 1124 1125 public int getAttempt() { 1126 return attempt; 1127 } 1128 1129 public long getRestartDelay() { 1130 String rd = System.getProperty(RESTART_DELAY_PROPERTY); 1131 if (rd!=null) { 1132 try { 1133 return Long.parseLong(rd); 1134 } catch (NumberFormatException e) { 1135 // Ignore 1136 } 1137 } 1138 1139 return DEFAULT_RESTART_DELAY; 1140 } 1141 }; 1142 1143 run.run(); 1144 1145 } 1146 1147 /** 1148 * Loads providers for a given service. Providers are loaded anew at every 1149 * invocation of this method. 1150 * @param <T> 1151 * @param service Service class. 1152 * @param classLoader Class loader 1153 * @return Iterator of providers of given class. 1154 * @throws ConfigurationException 1155 */ 1156 public static Iterator loadProviders(final Class service, final ClassLoader classLoader) { 1157 final String resName = CONFIG_RESOURCE_PREFIX+service.getName()+XML_EXTENSION; 1158 return new Iterator() { 1159 1160 private Enumeration resources; 1161 DomConfigFactory factory; 1162 1163 private synchronized Enumeration getResources() { 1164 if (resources==null) { 1165 try { 1166 if (classLoader==null) {// System classes 1167 ClassLoader alternativeClassLoader = DuckConverterFactory.getChildClassLoader(service.getClassLoader(), this.getClass().getClassLoader()); 1168 if (alternativeClassLoader==null) { 1169 alternativeClassLoader = this.getClass().getClassLoader(); 1170 } 1171 resources = alternativeClassLoader==null ? ClassLoader.getSystemResources(resName) : alternativeClassLoader.getResources(resName); 1172 factory = new DomConfigFactory(alternativeClassLoader); 1173 } else { 1174 resources = classLoader.getResources(resName); 1175 factory = new DomConfigFactory(classLoader); 1176 } 1177 1178 } catch (IOException e) { 1179 throw new RuntimeConfigurationException(e); 1180 } 1181 } 1182 return resources; 1183 } 1184 1185 public boolean hasNext() { 1186 return getResources().hasMoreElements(); 1187 } 1188 1189 public Object next() { 1190 try { 1191 return factory.create((URL) getResources().nextElement(), null); 1192 } catch (Exception e) { 1193 throw new RuntimeConfigurationException(e); 1194 } 1195 } 1196 1197 public void remove() { 1198 throw new UnsupportedOperationException("This operation is not supported"); 1199 } 1200 1201 }; 1202 1203 } 1204 1205 }