001 package biz.hammurapi.configx; 002 003 import java.io.File; 004 import java.io.IOException; 005 import java.io.InputStream; 006 import java.io.Reader; 007 import java.lang.reflect.Constructor; 008 import java.net.MalformedURLException; 009 import java.net.URL; 010 import java.net.URLClassLoader; 011 import java.util.ArrayList; 012 import java.util.Enumeration; 013 import java.util.HashMap; 014 import java.util.Hashtable; 015 import java.util.Iterator; 016 import java.util.List; 017 import java.util.Map; 018 import java.util.logging.Level; 019 import java.util.logging.Logger; 020 021 import org.apache.xmlbeans.XmlException; 022 import org.apache.xmlbeans.XmlObject; 023 import org.apache.xmlbeans.XmlOptions; 024 import org.w3c.dom.Document; 025 026 import biz.hammurapi.config.Command; 027 import biz.hammurapi.config.Component; 028 import biz.hammurapi.config.ConfigurationException; 029 import biz.hammurapi.config.Context; 030 import biz.hammurapi.config.DomConfigFactory; 031 import biz.hammurapi.config.MapContext; 032 import biz.hammurapi.config.RestartCommand; 033 import biz.hammurapi.config.Restartable; 034 import biz.hammurapi.config.RuntimeConfigurationException; 035 import biz.hammurapi.configx.xmltypes.ComponentDocument; 036 import biz.hammurapi.configx.xmltypes.NamedObjectSpecification; 037 import biz.hammurapi.configx.xmltypes.ObjectSpecification; 038 import biz.hammurapi.configx.xmltypes.Path; 039 import biz.hammurapi.configx.xmltypes.Property; 040 import biz.hammurapi.configx.xmltypes.Typed; 041 import biz.hammurapi.convert.ConvertingService; 042 import biz.hammurapi.xml.dom.DOMUtils; 043 044 /** 045 * This class instantiates Java objects from XML configuration 046 * parsed by XML Beans. 047 * 048 * This class is a counterpart and replacement for biz.hammurapi.config.DomConfigFactory 049 * @author Pavel 050 * 051 */ 052 public class XmlConfigFactory { 053 054 public static final String XML_EXTENSION = ".xml"; 055 public static final String CONFIG_RESOURCE_PREFIX = "META-INF/config/"; 056 057 private static final Logger logger = Logger.getLogger(XmlConfigFactory.class.getName()); 058 059 /** 060 * Creates class loader from specification 061 * @param spec Specification. Can be null. 062 * @param parent Parent classloader. Can be null. 063 * @param context Context URL. Can be null. 064 * @return New class loader if spec is not null, or parent if it is not null or classloader used to load XmlConfigFactory class. 065 * @throws MalformedURLException 066 */ 067 public static ClassLoader getClassLoader(Path spec, ClassLoader parent, URL context) throws MalformedURLException { 068 ClassLoader ret = parent==null ? XmlConfigFactory.class.getClassLoader() : parent; 069 070 ArrayList<URL> elements = new ArrayList<URL>(); 071 collectElements(spec, elements, context); 072 if (!elements.isEmpty()) { 073 ret = new URLClassLoader(elements.toArray(new URL[elements.size()]), ret); 074 } 075 076 return ret; 077 } 078 079 private static void collectElements(Path path, List<URL> receiver, URL context) throws MalformedURLException { 080 if (path!=null) { 081 if (path.getContextUri()!=null) { 082 context = new URL(context, path.getContextUri()); 083 } 084 085 String[] pea = path.getPathElementArray(); 086 for (int i=0; i<pea.length; ++i) { 087 receiver.add(new URL(context, pea[i])); 088 } 089 090 Path[] pa = path.getPathArray(); 091 for (int i=0; i<pa.length; ++i) { 092 collectElements(pa[i], receiver, context); 093 } 094 } 095 } 096 097 /** 098 * Validates specification and then creates object. Use this method on 099 * top-level elements. 100 * @param spec 101 * @param classLoader 102 * @param context 103 * @return 104 * @throws ConfigurationException 105 */ 106 public static Object validateAndCreate(Typed spec, ClassLoader classLoader, URL context) throws ConfigurationException { 107 ArrayList<?> validationErrors = new ArrayList<Object>(); 108 XmlOptions validationOptions = new XmlOptions(); 109 validationOptions.setErrorListener(validationErrors); 110 if (!spec.validate(validationOptions)) { 111 StringBuffer eb = new StringBuffer("Invalid specification: "); 112 for (Object err: validationErrors) { 113 eb.append("\n\t>> " + err); 114 } 115 116 throw new ConfigurationException(eb.toString()); 117 } 118 119 return create(spec, classLoader, context); 120 } 121 122 /** 123 * Instantiates object from Typed XML definition or its subclass. 124 * @param spec Specification 125 * @param classLoader Class loader. If it is null, then factory's classloader is used. 126 * @return 127 */ 128 @SuppressWarnings("unchecked") 129 public static <T> T create(Typed spec, ClassLoader classLoader, URL context) throws ConfigurationException { 130 131 if (spec.getType()==null) { 132 throw new ConfigurationException("Type is not set"); 133 } 134 135 try { 136 137 if (spec instanceof ObjectSpecification) { 138 biz.hammurapi.configx.xmltypes.ObjectSpecification.Constructor constructorSpec = ((ObjectSpecification) spec).getConstructor(); 139 if (constructorSpec!=null) { 140 throw new UnsupportedOperationException("Constructors are not yet supported"); 141 } 142 } 143 144 ClassLoader theClassLoader = getClassLoader(spec.getClasspath(), classLoader, context); 145 146 Class<T> clazz = (Class<T>) theClassLoader.loadClass(spec.getType()); 147 if (XmlConfigurable.class.isAssignableFrom(clazz)) { 148 T ret = clazz.newInstance(); 149 ((XmlConfigurable) ret).configure(spec); 150 return ret; 151 } 152 153 Constructor<T> candidate = null; 154 Constructor<T>[] ca = clazz.getConstructors(); 155 for (int i = 0; i<ca.length; ++i) { 156 Class<?>[] parameterTypes = ca[i].getParameterTypes(); 157 if (parameterTypes.length==1 158 && parameterTypes[0].isInstance(spec) 159 && (candidate==null || candidate.getParameterTypes()[0].isAssignableFrom(parameterTypes[0]))) { 160 candidate = ca[i]; 161 } 162 } 163 164 if (candidate!=null) { 165 return candidate.newInstance(new Object[] {spec}); 166 } 167 168 T ret = clazz.newInstance(); 169 if (spec instanceof ObjectSpecification) { 170 Map<String, Object> properties = instantiate(((ObjectSpecification) spec).getPropertyArray()); 171 if (!properties.isEmpty()) { 172 DomConfigFactory.inject(ret, new MapContext(properties)); 173 } 174 Map<String, Object> attributes = instantiate(((ObjectSpecification) spec).getAttributeArray(), theClassLoader, context); 175 if (!attributes.isEmpty()) { 176 DomConfigFactory.inject(ret, new MapContext(attributes)); 177 } 178 } 179 180 return ret; 181 } catch (ConfigurationException e) { 182 throw e; 183 } catch (Exception e) { 184 throw new ConfigurationException("Could not instantiate object from specification: "+e, e); 185 } 186 } 187 188 /** 189 * Instantiates properties from XML defintion 190 * @param property 191 * @return 192 * @throws ConfigurationException 193 */ 194 public static Hashtable<String, Object> instantiate(Property[] properties) throws ConfigurationException { 195 /** 196 * Marker class for multi-value entries. 197 * @author Pavel 198 */ 199 class MultiValueList extends ArrayList<Object> { 200 201 } 202 203 Hashtable<String, Object> ret = new Hashtable<String, Object>(); 204 if (properties!=null) { 205 for (int i=0; i<properties.length; ++i) { 206 String pName = properties[i].getName(); 207 Object pValue = properties[i].getStringValue(); 208 String pType = properties[i].getType(); 209 if (pType!=null && pType.trim().length()!=0) { 210 try { 211 pValue = ConvertingService.convert(pValue, Class.forName(pType)); 212 } catch (ClassNotFoundException e) { 213 throw new ConfigurationException("Cannot instantiate property of type "+properties[i].getType()+" from value "+properties[i].getStringValue()); 214 } 215 } 216 217 Object eValue = ret.get(pName); 218 if (eValue==null) { 219 ret.put(pName, pValue); 220 } else if (eValue instanceof MultiValueList) { 221 ((MultiValueList) eValue).add(pValue); 222 } else { 223 MultiValueList mvl = new MultiValueList(); 224 mvl.add(eValue); 225 mvl.add(pValue); 226 ret.put(pName, mvl); 227 } 228 } 229 } 230 return ret; 231 } 232 233 /** 234 * Instantiates properties from XML defintion 235 * @param property 236 * @return 237 * @throws ConfigurationException 238 */ 239 public static Hashtable<String, Object> instantiate(NamedObjectSpecification[] attributes, ClassLoader classLoader, URL context) throws ConfigurationException { 240 /** 241 * Marker class for multi-value entries. 242 * @author Pavel 243 */ 244 class MultiValueList extends ArrayList<Object> { 245 246 } 247 248 Hashtable<String, Object> ret = new Hashtable<String, Object>(); 249 if (attributes!=null) { 250 for (int i=0; i<attributes.length; ++i) { 251 Object aValue = create(attributes[i], classLoader, context); 252 String aName = attributes[i].getName(); 253 254 Object eValue = ret.get(aName); 255 if (eValue==null) { 256 ret.put(aName, aValue); 257 } else if (eValue instanceof MultiValueList) { 258 ((MultiValueList) eValue).add(aValue); 259 } else { 260 MultiValueList mvl = new MultiValueList(); 261 mvl.add(eValue); 262 mvl.add(aValue); 263 ret.put(aName, mvl); 264 } 265 } 266 } 267 return ret; 268 } 269 270 private static Object create(Document doc) throws XmlException, ConfigurationException { 271 XmlObject spec = XmlObject.Factory.parse(doc.getDocumentElement()); 272 ArrayList<?> validationErrors = new ArrayList<Object>(); 273 XmlOptions validationOptions = new XmlOptions(); 274 validationOptions.setErrorListener(validationErrors); 275 if (!spec.validate(validationOptions)) { 276 logger.severe("Configuration validation failed:"); 277 for (Object err: validationErrors) { 278 logger.severe("\t>> " + err); 279 } 280 281 System.exit(2); 282 } 283 if (spec instanceof Typed) { 284 return create((Typed) spec, null, null); 285 } 286 287 logger.severe("Invalid configuration, expected typed element"); 288 System.exit(1); 289 return null; 290 } 291 292 /** 293 * @param args 294 */ 295 public static void main(final String[] args) { 296 final long start=System.currentTimeMillis(); 297 if (args.length==0) { 298 logger.info("Usage: java <options> "+XmlConfigFactory.class.getName()+" <configuration URL> [<additional parameters>]"); 299 System.exit(1); 300 } 301 302 final boolean stopInHook = "yes".equalsIgnoreCase(System.getProperty("biz.hammurapi.configx.XmlFactory:shutdownHook")); 303 final Object[] oa = {null}; 304 305 if (stopInHook) { 306 Runtime.getRuntime().addShutdownHook( 307 new Thread() { 308 public void run() { 309 if (oa[0] instanceof Component) { 310 try { 311 ((Component) oa[0]).stop(); 312 } catch (ConfigurationException e) { 313 logger.severe("Could not properly stop "+oa[0]); 314 e.printStackTrace(); 315 } 316 logger.info("Total execution time: "+((System.currentTimeMillis()-start)/1000)+" sec."); 317 } 318 } 319 }); 320 } 321 322 RestartCommand run = new RestartCommand() { 323 324 int attempt; 325 326 public void run() { 327 try { 328 if (attempt > 0) { 329 long restartDelay = getRestartDelay(); 330 logger.info("Restarting in "+restartDelay+" milliseconds. Attempt " + (attempt+1)); 331 try { 332 Thread.sleep(restartDelay); 333 } catch (InterruptedException ie) { 334 ie.printStackTrace(); 335 System.exit(4); 336 } 337 } 338 339 ++attempt; 340 341 if (args[0].startsWith("url:")) { 342 oa[0] = create(DOMUtils.parse(new URL(args[0].substring("url:".length())).openStream())); 343 } else if (args[0].startsWith(biz.hammurapi.config.DomConfigFactory.RESOURCE_PREFIX)) { 344 InputStream stream = XmlConfigFactory.class.getClassLoader().getResourceAsStream(args[0].substring(biz.hammurapi.config.DomConfigFactory.RESOURCE_PREFIX.length())); 345 if (stream==null) { 346 logger.severe("Resource does not exist."); 347 System.exit(1); 348 } 349 oa[0] = create(DOMUtils.parse(stream)); 350 } else { 351 File file = new File(args[0]); 352 if (!file.exists()) { 353 logger.severe("File does not exist or not a file."); 354 System.exit(1); 355 } 356 if (!file.isFile()) { 357 logger.severe("Not a file."); 358 System.exit(1); 359 } 360 oa[0] = create(DOMUtils.parse(file)); 361 } 362 363 if (oa[0] instanceof Component) { 364 ((Component) oa[0]).start(); 365 } 366 367 if (oa[0] instanceof Restartable) { 368 ((Restartable) oa[0]).setRestartCommand(this); 369 } 370 371 try { 372 if (oa[0] instanceof Context) { 373 Context container=(Context) oa[0]; 374 for (int i=1; i<args.length; i++) { 375 Object toExecute=container.get(args[i]); 376 if (toExecute instanceof Command) { 377 ((Command) toExecute).execute(args); 378 } else if (toExecute==null) { 379 logger.warning("Name not found: " +args[i]); 380 } else { 381 logger.warning("Not executable: (" +args[i]+") "+toExecute.getClass().getName()); 382 } 383 } 384 } else if (oa[0] instanceof Command) { 385 ((Command) oa[0]).execute(args); 386 } 387 } finally { 388 if (oa[0] instanceof Component) { 389 if (!stopInHook) { 390 ((Component) oa[0]).stop(); 391 } 392 } 393 } 394 } catch (MalformedURLException e) { 395 logger.severe("Bad configuration URL: "+args[0]); 396 System.exit(2); 397 } catch (Exception e) { 398 e.printStackTrace(); 399 if (attempt > 1) { 400 if (oa[0] instanceof Component) { 401 try { 402 ((Component) oa[0]).stop(); 403 } catch (Exception ex) { 404 logger.severe("Cannot stop component before restart: "+e); 405 ex.printStackTrace(); 406 } 407 } 408 new Thread(this, "Restart thread "+getAttempt()).start(); // Use a new thread to avoid stack overflowing in the case of too many attempts. 409 } else { 410 System.exit(3); 411 } 412 } 413 } 414 415 public int getAttempt() { 416 return attempt; 417 } 418 419 public long getRestartDelay() { 420 String rd = System.getProperty(RESTART_DELAY_PROPERTY); 421 if (rd!=null) { 422 try { 423 return Long.parseLong(rd); 424 } catch (NumberFormatException e) { 425 // Ignore 426 } 427 } 428 429 return DEFAULT_RESTART_DELAY; 430 } 431 }; 432 433 run.run(); 434 435 } 436 437 /** 438 * Parses component document (as defined in configx.xsd) at URL and creates object from it. 439 * @param <T> 440 * @param url 441 * @param classLoader 442 * @return 443 * @throws ConfigurationException 444 */ 445 public static Object create(URL configURL, ClassLoader classLoader) throws ConfigurationException { 446 try { 447 ComponentDocument cd = ComponentDocument.Factory.parse(configURL); 448 return create(cd, classLoader, configURL); 449 } catch (ConfigurationException e) { 450 throw e; 451 } catch (Exception e) { 452 throw new ConfigurationException(e); 453 } 454 } 455 456 /** 457 * Parses component document (as defined in configx.xsd) at URL and creates object from it. 458 * @param <T> 459 * @param Configuration document reader. 460 * @param classLoader 461 * @return Configured object. 462 * @throws ConfigurationException 463 */ 464 public static Object create(Reader reader, ClassLoader classLoader) throws ConfigurationException { 465 try { 466 ComponentDocument cd = ComponentDocument.Factory.parse(reader); 467 return create(cd, classLoader, null); 468 } catch (ConfigurationException e) { 469 throw e; 470 } catch (Exception e) { 471 throw new ConfigurationException(e); 472 } 473 } 474 475 public static Object create( 476 ComponentDocument cd, 477 ClassLoader classLoader, 478 URL baseURL) 479 throws ConfigurationException { 480 481 ArrayList<?> validationErrors = new ArrayList<Object>(); 482 XmlOptions validationOptions = new XmlOptions(); 483 validationOptions.setErrorListener(validationErrors); 484 if (!cd.validate(validationOptions)) { 485 StringBuffer eb = new StringBuffer("Invalid definition: "); 486 for (Object err: validationErrors) { 487 eb.append("\n\t>> " + err); 488 } 489 490 throw new ConfigurationException(eb.toString()); 491 } 492 493 return create(cd.getComponent(), classLoader, baseURL); 494 } 495 496 /** 497 * Loads providers for a given service. Providers are loaded anew at every 498 * invocation of this method. 499 * @param <T> 500 * @param service Service class. 501 * @param classLoader Class loader 502 * @return Iterator of providers of given class. 503 * @throws ConfigurationException 504 */ 505 public static <T> Iterator<T> loadProviders(final Class<T> service, final ClassLoader classLoader) { 506 final String resName = CONFIG_RESOURCE_PREFIX+service.getName()+XML_EXTENSION; 507 return new Iterator<T>() { 508 509 private Enumeration<URL> resources; 510 511 private synchronized Enumeration<URL> getResources() { 512 if (resources==null) { 513 try { 514 resources = classLoader==null ? ClassLoader.getSystemResources(resName) : classLoader.getResources(resName); 515 } catch (IOException e) { 516 throw new RuntimeConfigurationException(e); 517 } 518 } 519 return resources; 520 } 521 522 public boolean hasNext() { 523 return getResources().hasMoreElements(); 524 } 525 526 @SuppressWarnings("unchecked") 527 public T next() { 528 try { 529 return (T) create(getResources().nextElement(), classLoader); 530 } catch (ConfigurationException e) { 531 throw new RuntimeConfigurationException(e); 532 } 533 } 534 535 public void remove() { 536 throw new UnsupportedOperationException("This operation is not supported"); 537 } 538 539 }; 540 541 } 542 543 private static class ServiceEntry<T> { 544 Class<T> clazz; 545 ClassLoader classLoader; 546 547 @Override 548 public int hashCode() { 549 final int prime = 31; 550 int result = 1; 551 result = prime * result + ((classLoader == null) ? 0 : classLoader.hashCode()); 552 result = prime * result + ((clazz == null) ? 0 : clazz.hashCode()); 553 return result; 554 } 555 556 @SuppressWarnings("unchecked") 557 @Override 558 public boolean equals(Object obj) { 559 if (this == obj) 560 return true; 561 if (obj == null) 562 return false; 563 if (getClass() != obj.getClass()) 564 return false; 565 566 ServiceEntry<T> other = (ServiceEntry<T>) obj; 567 if (classLoader == null) { 568 if (other.classLoader != null) 569 return false; 570 } else if (!classLoader.equals(other.classLoader)) 571 return false; 572 if (clazz == null) { 573 if (other.clazz != null) 574 return false; 575 } else if (!clazz.equals(other.clazz)) 576 return false; 577 return true; 578 } 579 } 580 581 private static final Map<ServiceEntry<?>, Iterable<?>> providers = new HashMap<ServiceEntry<?>, Iterable<?>>(); 582 583 /** 584 * Loads and caches providers. If provider implements Component, then its start 585 * method is invoked after creation, and stop() method is invoked in shutdown hook. 586 * @param <T> 587 * @param service 588 * @param classLoader 589 * @return 590 * @throws ConfigurationException 591 */ 592 @SuppressWarnings("unchecked") 593 public static <T> Iterable<T> providers(final Class<T> service, final ClassLoader classLoader) throws ConfigurationException { 594 synchronized (providers) { 595 ServiceEntry<T> key = new ServiceEntry<T>(); 596 key.clazz = service; 597 key.classLoader = classLoader; 598 599 Iterable<T> ret = (Iterable<T>) providers.get(key); 600 if (ret==null) { 601 ArrayList<T> rt = new ArrayList<T>(); 602 Iterator<T> pIt = loadProviders(service, classLoader); 603 while (pIt.hasNext()) { 604 T nextProvider = pIt.next(); 605 if (nextProvider instanceof Component) { 606 ((Component) nextProvider).start(); 607 } 608 rt.add(nextProvider); 609 } 610 611 if (!rt.isEmpty()) { 612 if (providers.isEmpty()) { 613 Runtime.getRuntime().addShutdownHook(new Thread() { 614 615 @Override 616 public void run() { 617 synchronized (providers) { 618 for (Iterable<?> typeProviders: providers.values()) { 619 for (Object typeProvider: typeProviders) { 620 if (typeProvider instanceof Component) { 621 try { 622 ((Component) typeProvider).stop(); 623 } catch (Exception e) { 624 logger.log(Level.WARNING, "Could not stop service provider "+typeProvider+": "+e, e); 625 } 626 } 627 } 628 } 629 } 630 } 631 }); 632 } 633 } 634 635 ret=rt; 636 providers.put(key, ret); 637 } 638 return ret; 639 } 640 641 } 642 643 private static Map<ServiceEntry<?>, Object> services = new HashMap<ServiceEntry<?>, Object>(); 644 645 /** 646 * Returns composite service. In order to use this method providers shall implement 647 * CompositeProvider interface. 648 * @param <T> 649 * @param service 650 * @return 651 * @throws ConfigurationException 652 */ 653 @SuppressWarnings("unchecked") 654 public static <T> T getService(Class<T> service, ClassLoader classLoader) throws ConfigurationException { 655 synchronized (services) { 656 ServiceEntry<T> key = new ServiceEntry<T>(); 657 key.clazz = service; 658 key.classLoader = classLoader; 659 T ret = (T) services.get(key); 660 if (ret == null) { 661 for (T provider: providers(service, classLoader)) { 662 if (ret==null) { 663 ret = provider; 664 } else if (ret instanceof CompositeProvider) { 665 ((CompositeProvider) ret).merge(provider); 666 } else if (provider instanceof CompositeProvider) { 667 ((CompositeProvider) provider).merge(ret); 668 ret = provider; 669 } else { 670 throw new ConfigurationException("Cannot merge providers for service "+service+" - neither of them implements " + CompositeProvider.class.getName()); 671 } 672 } 673 services.put(key, ret); 674 } 675 return ret; 676 } 677 } 678 }