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    }