//
//  ========================================================================
//  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.annotations;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import javax.servlet.ServletContainerInitializer;
import javax.servlet.annotation.HandlesTypes;

import org.eclipse.jetty.annotations.AnnotationParser.Handler;
import org.eclipse.jetty.plus.annotation.ContainerInitializer;
import org.eclipse.jetty.util.ConcurrentHashSet;
import org.eclipse.jetty.util.MultiException;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.statistic.CounterStatistic;
import org.eclipse.jetty.webapp.AbstractConfiguration;
import org.eclipse.jetty.webapp.FragmentDescriptor;
import org.eclipse.jetty.webapp.MetaDataComplete;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.webapp.WebDescriptor;

/**
 * Configuration for Annotations
 */
public class AnnotationConfiguration extends AbstractConfiguration
{
    private static final Logger LOG = Log.getLogger(AnnotationConfiguration.class);
    
    public static final String SERVLET_CONTAINER_INITIALIZER_EXCLUSION_PATTERN = "org.eclipse.jetty.containerInitializerExclusionPattern";
    public static final String SERVLET_CONTAINER_INITIALIZER_ORDER = "org.eclipse.jetty.containerInitializerOrder";
    public static final String CLASS_INHERITANCE_MAP  = "org.eclipse.jetty.classInheritanceMap";
    public static final String CONTAINER_INITIALIZERS = "org.eclipse.jetty.containerInitializers";
    public static final String CONTAINER_INITIALIZER_STARTER = "org.eclipse.jetty.containerInitializerStarter";
    public static final String MULTI_THREADED = "org.eclipse.jetty.annotations.multiThreaded";
    public static final String MAX_SCAN_WAIT = "org.eclipse.jetty.annotations.maxWait";
    
    public static final int DEFAULT_MAX_SCAN_WAIT = 60; /* time in sec */  
    public static final boolean DEFAULT_MULTI_THREADED = true;
    
    protected List<AbstractDiscoverableAnnotationHandler> _discoverableAnnotationHandlers = new ArrayList<AbstractDiscoverableAnnotationHandler>();
    protected ClassInheritanceHandler _classInheritanceHandler;
    protected List<ContainerInitializerAnnotationHandler> _containerInitializerAnnotationHandlers = new ArrayList<ContainerInitializerAnnotationHandler>();
   
    protected List<ParserTask> _parserTasks;
    protected WebAppClassNameResolver _webAppClassNameResolver;
    protected ContainerClassNameResolver _containerClassNameResolver;

    protected CounterStatistic _containerPathStats;
    protected CounterStatistic _webInfLibStats;
    protected CounterStatistic _webInfClassesStats;
    protected Pattern _sciExcludePattern;
    protected ServiceLoader<ServletContainerInitializer> _loadedInitializers = null;
    /**
     * TimeStatistic
     *
     * Simple class to capture elapsed time of an operation.
     * 
     */
    public class TimeStatistic 
    {
        public long _start = 0;
        public long _end = 0;
        
        public void start ()
        {
            _start = System.nanoTime();
        }
        
        public void end ()
        {
            _end = System.nanoTime();
        }
        
        public long getStart()
        {
            return _start;
        }
        
        public long getEnd ()
        {
            return _end;
        }
        
        public long getElapsed ()
        {
            return (_end > _start?(_end-_start):0);
        }
    }
    
    
    /**
     * ParserTask
     *
     * Task to executing scanning of a resource for annotations.
     * 
     */
    public class ParserTask implements Callable<Void>
    {
        protected Exception _exception;
        protected final AnnotationParser _parser;
        protected final Set<? extends Handler> _handlers;
        protected final ClassNameResolver _resolver;
        protected final Resource _resource;
        protected TimeStatistic _stat;
        
        public ParserTask (AnnotationParser parser, Set<? extends Handler>handlers, Resource resource, ClassNameResolver resolver)
        {
            _parser = parser;
            _handlers = handlers;
            _resolver = resolver;
            _resource = resource;
        }
        
        public void setStatistic(TimeStatistic stat)
        {
           _stat = stat; 
        }

        public Void call() throws Exception
        {            
            if (_stat != null)
                _stat.start();
            if (_parser != null)
                _parser.parse(_handlers, _resource, _resolver); 
            if (_stat != null)
                _stat.end();
            return null;
        }
        
        public TimeStatistic getStatistic()
        {
            return _stat;
        }
        
        public Resource getResource()
        {
            return _resource;
        }
        
    }

    /**
     * WebAppClassNameResolver
     *
     * Checks to see if a classname belongs to hidden or visible packages when scanning,
     * and whether a classname that is a duplicate should override a previously
     * scanned classname. 
     * 
     * This is analogous to the management of classes that the WebAppClassLoader is doing,
     * however we don't want to load the classes at this point so we are doing it on
     * the name only.
     * 
     */
    public class WebAppClassNameResolver implements ClassNameResolver
    {
        private WebAppContext _context;

        public WebAppClassNameResolver (WebAppContext context)
        {
            _context = context;
        }

        public boolean isExcluded (String name)
        {
            if (_context.isSystemClass(name)) return true;
            if (_context.isServerClass(name)) return false;
            return false;
        }

        public boolean shouldOverride (String name)
        {
            //looking at webapp classpath, found already-parsed class 
            //of same name - did it come from system or duplicate in webapp?
            if (_context.isParentLoaderPriority())
                return false;
            return true;
        }
    }

    
    /**
     * ContainerClassNameResolver
     *
     * Checks to see if a classname belongs to a hidden or visible package
     * when scanning for annotations and thus whether it should be excluded from
     * consideration or not.
     * 
     * This is analogous to the management of classes that the WebAppClassLoader is doing,
     * however we don't want to load the classes at this point so we are doing it on
     * the name only.
     * 
     */
    public class ContainerClassNameResolver implements ClassNameResolver
    { 
        private WebAppContext _context;
        
        public ContainerClassNameResolver (WebAppContext context)
        {
            _context = context;
        }
        public boolean isExcluded (String name)
        {
            if (_context.isSystemClass(name)) return false;
            if (_context.isServerClass(name)) return true;
            return false;
        }

        public boolean shouldOverride (String name)
        {
            //visiting the container classpath, 
            if (_context.isParentLoaderPriority())
                return true;
            return false;
        }
    }
    
    
    /**
     * ServletContainerInitializerOrdering
     *
     * A list of classnames of ServletContainerInitializers in the order in which
     * they are to be called back. One name only in the list can be "*", which is a
     * wildcard which matches any other ServletContainerInitializer name not already
     * matched.
     */
    public class ServletContainerInitializerOrdering 
    {
        private Map<String, Integer> _indexMap = new HashMap<String, Integer>();
        private Integer _star = null;
        private String _ordering = null;
        
        public ServletContainerInitializerOrdering (String ordering)
        {
            if (ordering != null)
            {
                _ordering = ordering;
                
                String[] tmp = StringUtil.csvSplit(ordering);
                
                for (int i=0; i<tmp.length; i++)
                {
                    String s = tmp[i].trim();
                    _indexMap.put(s, Integer.valueOf(i));
                    if ("*".equals(s))
                    {
                        if (_star != null)
                            throw new IllegalArgumentException("Duplicate wildcards in ServletContainerInitializer ordering "+ordering);
                        _star = Integer.valueOf(i);
                    }
                    
                }
            }
        }
        
        /**
         * @return true if "*" is one of the values.
         */
        public boolean hasWildcard()
        {
            return _star != null;
        }
        
        /**
         * @return the index of the "*" element, if it is specified. -1 otherwise.
         */
        public int getWildcardIndex()
        {
            if (!hasWildcard())
                return -1;
            return _star.intValue();
        }
        
        /**
         * @return true if the ordering contains a single value of "*"
         */
        public boolean isDefaultOrder ()
        {
            return (getSize() == 1 && hasWildcard());
        }
        
        /**
         * Get the order index of the given classname
         * @param name the classname to look up
         * @return the index of the class name (or -1 if not found)
         */
        public int getIndexOf (String name)
        {
            Integer i = _indexMap.get(name);
            if (i == null)
                return -1;
            return i.intValue();
        }
        
        /**
         * Get the number of elements of the ordering
         * @return the size of the index
         */
        public int getSize()
        {
            return _indexMap.size();
        }
        
        public String toString()
        {
            if (_ordering == null)
                return "";
            return _ordering;
        }
    }
    
    
    
    /**
     * ServletContainerInitializerComparator
     *
     * Comparator impl that orders a set of ServletContainerInitializers according to the
     * list of classnames (optionally containing a "*" wildcard character) established in a
     * ServletContainerInitializerOrdering.
     * @see ServletContainerInitializerOrdering
     */
    public class ServletContainerInitializerComparator implements Comparator<ServletContainerInitializer>
    {
        private ServletContainerInitializerOrdering _ordering;
        
        
        public ServletContainerInitializerComparator (ServletContainerInitializerOrdering ordering)
        {
            _ordering = ordering;
        }

        @Override
        public int compare(ServletContainerInitializer sci1, ServletContainerInitializer sci2)
        {
            String c1 = (sci1 != null? sci1.getClass().getName() : null);
            String c2 = (sci2 != null? sci2.getClass().getName() : null);

            if (c1 == null && c2 == null)
                return 0;
            
            int i1 = _ordering.getIndexOf(c1);
            if (i1 < 0 && _ordering.hasWildcard())
                i1 = _ordering.getWildcardIndex();
            int i2 = _ordering.getIndexOf(c2);
            if (i2 < 0 && _ordering.hasWildcard())
                i2 = _ordering.getWildcardIndex();
           
            return Integer.compare(i1, i2);
        }
    }
    
    @Override
    public void preConfigure(final WebAppContext context) throws Exception
    {
        _webAppClassNameResolver = new WebAppClassNameResolver(context);
        _containerClassNameResolver = new ContainerClassNameResolver(context);
        String tmp = (String)context.getAttribute(SERVLET_CONTAINER_INITIALIZER_EXCLUSION_PATTERN);
        _sciExcludePattern = (tmp==null?null:Pattern.compile(tmp));
    }

   
    public void addDiscoverableAnnotationHandler(AbstractDiscoverableAnnotationHandler handler)
    {
        _discoverableAnnotationHandlers.add(handler);
    }

    @Override
    public void deconfigure(WebAppContext context) throws Exception
    {
        context.removeAttribute(CLASS_INHERITANCE_MAP);
        context.removeAttribute(CONTAINER_INITIALIZERS);
        ServletContainerInitializersStarter starter = (ServletContainerInitializersStarter)context.getAttribute(CONTAINER_INITIALIZER_STARTER);
        if (starter != null)
        {
            context.removeBean(starter);
            context.removeAttribute(CONTAINER_INITIALIZER_STARTER);
        }
        
        if (_loadedInitializers != null)
            _loadedInitializers.reload();
    }
    
    /** 
     * @see org.eclipse.jetty.webapp.AbstractConfiguration#configure(org.eclipse.jetty.webapp.WebAppContext)
     */
    @Override
    public void configure(WebAppContext context) throws Exception
    {
       context.getObjectFactory().addDecorator(new AnnotationDecorator(context));

       //Even if metadata is complete, we still need to scan for ServletContainerInitializers - if there are any
      
       if (!context.getMetaData().isMetaDataComplete())
       {
           //If metadata isn't complete, if this is a servlet 3 webapp or isConfigDiscovered is true, we need to search for annotations
           if (context.getServletContext().getEffectiveMajorVersion() >= 3 || context.isConfigurationDiscovered())
           {
               _discoverableAnnotationHandlers.add(new WebServletAnnotationHandler(context));
               _discoverableAnnotationHandlers.add(new WebFilterAnnotationHandler(context));
               _discoverableAnnotationHandlers.add(new WebListenerAnnotationHandler(context));
           }
       }

       //Regardless of metadata, if there are any ServletContainerInitializers with @HandlesTypes, then we need to scan all the
       //classes so we can call their onStartup() methods correctly
       createServletContainerInitializerAnnotationHandlers(context, getNonExcludedInitializers(context));

       if (!_discoverableAnnotationHandlers.isEmpty() || _classInheritanceHandler != null || !_containerInitializerAnnotationHandlers.isEmpty())
           scanForAnnotations(context);   
       
       // Resolve container initializers
       List<ContainerInitializer> initializers = 
                   (List<ContainerInitializer>)context.getAttribute(AnnotationConfiguration.CONTAINER_INITIALIZERS);
       if (initializers != null && initializers.size()>0)
       {
           Map<String, Set<String>> map = ( Map<String, Set<String>>) context.getAttribute(AnnotationConfiguration.CLASS_INHERITANCE_MAP);
           for (ContainerInitializer i : initializers)
                   i.resolveClasses(context,map);
       }
    }


    /** 
     * @see org.eclipse.jetty.webapp.AbstractConfiguration#postConfigure(org.eclipse.jetty.webapp.WebAppContext)
     */
    @Override
    public void postConfigure(WebAppContext context) throws Exception
    {
        ConcurrentHashMap<String, ConcurrentHashSet<String>> classMap = (ClassInheritanceMap)context.getAttribute(CLASS_INHERITANCE_MAP);
        List<ContainerInitializer> initializers = (List<ContainerInitializer>)context.getAttribute(CONTAINER_INITIALIZERS);
        
        context.removeAttribute(CLASS_INHERITANCE_MAP);
        if (classMap != null)
            classMap.clear();
        
        context.removeAttribute(CONTAINER_INITIALIZERS);
        if (initializers != null)
            initializers.clear();
        
        if (_discoverableAnnotationHandlers != null)
            _discoverableAnnotationHandlers.clear();

        _classInheritanceHandler = null;
        if (_containerInitializerAnnotationHandlers != null)
            _containerInitializerAnnotationHandlers.clear();

        if (_parserTasks != null)
        {
            _parserTasks.clear();
            _parserTasks = null;
        }
        
        super.postConfigure(context);
    }
    
    
    
    /**
     * Perform scanning of classes for annotations
     * 
     * @param context the context for the scan
     * @throws Exception if unable to scan
     */
    protected void scanForAnnotations (WebAppContext context)
    throws Exception
    {
        AnnotationParser parser = createAnnotationParser();
        _parserTasks = new ArrayList<ParserTask>();

        long start = 0; 


        if (LOG.isDebugEnabled())
            LOG.debug("Annotation scanning commencing: webxml={}, metadatacomplete={}, configurationDiscovered={}, multiThreaded={}, maxScanWait={}", 
                      context.getServletContext().getEffectiveMajorVersion(), 
                      context.getMetaData().isMetaDataComplete(),
                      context.isConfigurationDiscovered(),
                      isUseMultiThreading(context),
                      getMaxScanWait(context));

             
        parseContainerPath(context, parser);
        //email from Rajiv Mordani jsrs 315 7 April 2010
        //    If there is a <others/> then the ordering should be
        //          WEB-INF/classes the order of the declared elements + others.
        //    In case there is no others then it is
        //          WEB-INF/classes + order of the elements.
        parseWebInfClasses(context, parser);
        parseWebInfLib (context, parser); 
        
        start = System.nanoTime();
        
        //execute scan, either effectively synchronously (1 thread only), or asynchronously (limited by number of processors available) 
        final Semaphore task_limit = (isUseMultiThreading(context)? new Semaphore(Runtime.getRuntime().availableProcessors()):new Semaphore(1));     
        final CountDownLatch latch = new CountDownLatch(_parserTasks.size());
        final MultiException me = new MultiException();
    
        for (final ParserTask p:_parserTasks)
        {
            task_limit.acquire();
            context.getServer().getThreadPool().execute(new Runnable()
            {
                @Override
                public void run()
                {
                   try
                   {
                       p.call();
                   }
                   catch (Exception e)
                   {
                       me.add(e);
                   }
                   finally
                   {
                       task_limit.release();
                       latch.countDown();
                   }
                }         
            });
        }
       
        boolean timeout = !latch.await(getMaxScanWait(context), TimeUnit.SECONDS);
          
        if (LOG.isDebugEnabled())
        {
            for (ParserTask p:_parserTasks)
                LOG.debug("Scanned {} in {}ms", p.getResource(), TimeUnit.MILLISECONDS.convert(p.getStatistic().getElapsed(), TimeUnit.NANOSECONDS));

            LOG.debug("Scanned {} container path jars, {} WEB-INF/lib jars, {} WEB-INF/classes dirs in {}ms for context {}",
                    _containerPathStats.getTotal(), _webInfLibStats.getTotal(), _webInfClassesStats.getTotal(),
                    (TimeUnit.MILLISECONDS.convert(System.nanoTime()-start, TimeUnit.NANOSECONDS)),
                    context);
        }

        if (timeout)
            me.add(new Exception("Timeout scanning annotations"));
        me.ifExceptionThrow();   
    }

    
    
    /**
     * @return a new AnnotationParser. This method can be overridden to use a different implementation of
     * the AnnotationParser. Note that this is considered internal API.
     */
    protected AnnotationParser createAnnotationParser()
    {
        return new AnnotationParser();
    }
    
    /**
     * Check if we should use multiple threads to scan for annotations or not
     * @param context the context of the multi threaded setting
     * @return true if multi threading is enabled on the context, server, or via a System property.
     * @see #MULTI_THREADED
     */
    protected boolean isUseMultiThreading(WebAppContext context)
    {
        //try context attribute to see if we should use multithreading
        Object o = context.getAttribute(MULTI_THREADED);
        if (o instanceof Boolean)
        {
            return ((Boolean)o).booleanValue();
        }
        //try server attribute to see if we should use multithreading
        o = context.getServer().getAttribute(MULTI_THREADED);
        if (o instanceof Boolean)
        {
            return ((Boolean)o).booleanValue();
        }
        //try system property to see if we should use multithreading
        return Boolean.valueOf(System.getProperty(MULTI_THREADED, Boolean.toString(DEFAULT_MULTI_THREADED)));
    }

   
    
    /**
     * Work out how long we should wait for the async scanning to occur.
     * 
     * @param context the context of the max scan wait setting
     * @return the max scan wait setting on the context, or server, or via a System property.
     * @see #MAX_SCAN_WAIT
     */
    protected int getMaxScanWait (WebAppContext context)
    {
        //try context attribute to get max time in sec to wait for scan completion
        Object o = context.getAttribute(MAX_SCAN_WAIT);
        if (o != null && o instanceof Number)
        {
            return ((Number)o).intValue();
        }
        //try server attribute to get max time in sec to wait for scan completion
        o = context.getServer().getAttribute(MAX_SCAN_WAIT);
        if (o != null && o instanceof Number)
        {
            return ((Number)o).intValue();
        }
        //try system property to get max time in sec to wait for scan completion
        return Integer.getInteger(MAX_SCAN_WAIT, DEFAULT_MAX_SCAN_WAIT).intValue();
    }
    
    /** 
     * @see org.eclipse.jetty.webapp.AbstractConfiguration#cloneConfigure(org.eclipse.jetty.webapp.WebAppContext, org.eclipse.jetty.webapp.WebAppContext)
     */
    @Override
    public void cloneConfigure(WebAppContext template, WebAppContext context) throws Exception
    {
        context.getObjectFactory().addDecorator(new AnnotationDecorator(context));
    }

    public void createServletContainerInitializerAnnotationHandlers (WebAppContext context, List<ServletContainerInitializer> scis)
    throws Exception
    {
        if (scis == null || scis.isEmpty())
            return; // nothing to do

        List<ContainerInitializer> initializers = new ArrayList<ContainerInitializer>();
        context.setAttribute(CONTAINER_INITIALIZERS, initializers);

        for (ServletContainerInitializer service : scis)
        {
            HandlesTypes annotation = service.getClass().getAnnotation(HandlesTypes.class);
            ContainerInitializer initializer = null;
            if (annotation != null)
            {    
                //There is a HandlesTypes annotation on the on the ServletContainerInitializer
                Class<?>[] classes = annotation.value();
                if (classes != null)
                {

                    if (LOG.isDebugEnabled()){LOG.debug("HandlesTypes {} on initializer {}",Arrays.asList(classes),service.getClass());}
                    
                    initializer = new ContainerInitializer(service, classes);

                    //If we haven't already done so, we need to register a handler that will
                    //process the whole class hierarchy to satisfy the ServletContainerInitializer
                    if (context.getAttribute(CLASS_INHERITANCE_MAP) == null)
                    {
                        //MultiMap<String> map = new MultiMap<>();
                        ConcurrentHashMap<String, ConcurrentHashSet<String>> map = new ClassInheritanceMap();
                        context.setAttribute(CLASS_INHERITANCE_MAP, map);
                        _classInheritanceHandler = new ClassInheritanceHandler(map);
                    }

                    for (Class<?> c: classes)
                    {
                        //The value of one of the HandlesTypes classes is actually an Annotation itself so
                        //register a handler for it
                        if (c.isAnnotation())
                        {
                            if (LOG.isDebugEnabled()) LOG.debug("Registering annotation handler for "+c.getName());
                           _containerInitializerAnnotationHandlers.add(new ContainerInitializerAnnotationHandler(initializer, c));
                        }
                    }
                }
                else
                {
                    initializer = new ContainerInitializer(service, null);
                    if (LOG.isDebugEnabled()) LOG.debug("No classes in HandlesTypes on initializer "+service.getClass());
                }
            }
            else
            {
                initializer = new ContainerInitializer(service, null);
                if (LOG.isDebugEnabled()) LOG.debug("No HandlesTypes annotation on initializer "+service.getClass());
            }
            
            initializers.add(initializer);
        }
        
        
        //add a bean to the context which will call the servletcontainerinitializers when appropriate
        ServletContainerInitializersStarter starter = (ServletContainerInitializersStarter)context.getAttribute(CONTAINER_INITIALIZER_STARTER);
        if (starter != null)
            throw new IllegalStateException("ServletContainerInitializersStarter already exists");
        starter = new ServletContainerInitializersStarter(context);
        context.setAttribute(CONTAINER_INITIALIZER_STARTER, starter);
        context.addBean(starter, true);
    }

    public Resource getJarFor (ServletContainerInitializer service) 
    throws MalformedURLException, IOException
    {
        //try the thread context classloader to get the jar that loaded the class
        URL jarURL = Thread.currentThread().getContextClassLoader().getResource(service.getClass().getName().replace('.','/')+".class");
        
        //if for some reason that failed (eg we're in osgi and the TCCL does not know about the service) try the classloader that
        //loaded the class
        if (jarURL == null)
            jarURL = service.getClass().getClassLoader().getResource(service.getClass().getName().replace('.','/')+".class");
  
        String loadingJarName = jarURL.toString();

        int i = loadingJarName.indexOf(".jar");
        if (i < 0)
            return null; //not from a jar
        
        loadingJarName = loadingJarName.substring(0,i+4);
        loadingJarName = (loadingJarName.startsWith("jar:")?loadingJarName.substring(4):loadingJarName);
        return Resource.newResource(loadingJarName);
    }
    
    /**
     * Check to see if the ServletContainerIntializer loaded via the ServiceLoader came
     * from a jar that is excluded by the fragment ordering. See ServletSpec 3.0 p.85.
     * 
     * @param context the context for the jars
     * @param sci the servlet container initializer
     * @param sciResource  the resource for the servlet container initializer
     * @return true if excluded
     * @throws Exception if unable to determine exclusion
     */
    public boolean isFromExcludedJar (WebAppContext context, ServletContainerInitializer sci, Resource sciResource)
    throws Exception
    {
        if (sci == null)
            throw new IllegalArgumentException("ServletContainerInitializer null");
        if (context == null)
            throw new IllegalArgumentException("WebAppContext null");
        
        if (LOG.isDebugEnabled()) LOG.debug("Checking {} for jar exclusion", sci);
        
        //A ServletContainerInitializer that came from the container's classpath cannot be excluded by an ordering
        //of WEB-INF/lib jars
        if (isFromContainerClassPath(context, sci))
            return false;
        
        //If no ordering, nothing is excluded
        if (context.getMetaData().getOrdering() == null)
            return false;
        
        
        List<Resource> orderedJars = context.getMetaData().getOrderedWebInfJars();

        //there is an ordering, but there are no jars resulting from the ordering, everything excluded
        if (orderedJars.isEmpty())
            return true;

        if (sciResource == null)
            return false; //not from a jar therefore not from WEB-INF so not excludable
        
        URI loadingJarURI = sciResource.getURI();
        boolean found = false;
        Iterator<Resource> itor = orderedJars.iterator();
        while (!found && itor.hasNext())
        {
            Resource r = itor.next();
            found = r.getURI().equals(loadingJarURI);
        }

        return !found;
    }


    /**
     * Test if the ServletContainerIntializer is excluded by the 
     * o.e.j.containerInitializerExclusionPattern
     * 
     * @param sci the ServletContainerIntializer
     * @return true if the ServletContainerIntializer is excluded
     */
    public boolean matchesExclusionPattern(ServletContainerInitializer sci)
    {
        //no exclusion pattern, no SCI is excluded by it
        if (_sciExcludePattern == null)
            return false;
        
        //test if name of class matches the regex
        if (LOG.isDebugEnabled()) LOG.debug("Checking {} against containerInitializerExclusionPattern",sci.getClass().getName());
        return _sciExcludePattern.matcher(sci.getClass().getName()).matches();
    }
    
    
    /**
     * Test if the ServletContainerInitializer is from the container classpath
     * 
     * @param context the context for the webapp classpath
     * @param sci the ServletContainerIntializer
     * @return true if ServletContainerIntializer is from container classpath
     */
    public boolean isFromContainerClassPath (WebAppContext context, ServletContainerInitializer sci)
    {
        if (sci == null)
            return false;
        return sci.getClass().getClassLoader()==context.getClassLoader().getParent();
    }

    /**
     * Get SCIs that are not excluded from consideration
     * @param context the web app context 
     * @return the list of non-excluded servlet container initializers
     * @throws Exception if unable to get list 
     */
    public List<ServletContainerInitializer> getNonExcludedInitializers (WebAppContext context)
    throws Exception
    {
        ArrayList<ServletContainerInitializer> nonExcludedInitializers = new ArrayList<ServletContainerInitializer>();
      
        //We use the ServiceLoader mechanism to find the ServletContainerInitializer classes to inspect
        long start = 0;

        ClassLoader old = Thread.currentThread().getContextClassLoader();
       
        try
        {        
            if (LOG.isDebugEnabled())
                start = System.nanoTime();
            Thread.currentThread().setContextClassLoader(context.getClassLoader());
            _loadedInitializers = ServiceLoader.load(ServletContainerInitializer.class);
        }
        finally
        {
            Thread.currentThread().setContextClassLoader(old);
        }
        
        if (LOG.isDebugEnabled())
            LOG.debug("Service loaders found in {}ms", (TimeUnit.MILLISECONDS.convert((System.nanoTime()-start), TimeUnit.NANOSECONDS)));
     
        
        Map<ServletContainerInitializer,Resource> sciResourceMap = new HashMap<ServletContainerInitializer,Resource>();
        ServletContainerInitializerOrdering initializerOrdering = getInitializerOrdering(context);

        //Get initial set of SCIs that aren't from excluded jars or excluded by the containerExclusionPattern, or excluded
        //because containerInitializerOrdering omits it
        for (ServletContainerInitializer sci:_loadedInitializers)
        { 
            
            if (matchesExclusionPattern(sci)) 
            {
                if (LOG.isDebugEnabled()) LOG.debug("{} excluded by pattern", sci);
                continue;
            }

            Resource sciResource = getJarFor(sci);
            if (isFromExcludedJar(context, sci, sciResource)) 
            { 
                if (LOG.isDebugEnabled()) LOG.debug("{} is from excluded jar", sci);
                continue;
            }
            
            //check containerInitializerOrdering doesn't exclude it
            String name = sci.getClass().getName();
            if (initializerOrdering != null
                && (!initializerOrdering.hasWildcard() && initializerOrdering.getIndexOf(name) < 0))
            {
                if (LOG.isDebugEnabled()) LOG.debug("{} is excluded by ordering", sci);
                continue;
            }
            
            sciResourceMap.put(sci, sciResource);
        }

        //Order the SCIs that are included
        if (initializerOrdering != null && !initializerOrdering.isDefaultOrder())
        {
            if (LOG.isDebugEnabled())
                LOG.debug("Ordering ServletContainerInitializers with "+initializerOrdering);

            //There is an ordering that is not just "*".
            //Arrange ServletContainerInitializers according to the ordering of classnames given, irrespective of coming from container or webapp classpaths
            nonExcludedInitializers.addAll(sciResourceMap.keySet());
            Collections.sort(nonExcludedInitializers, new ServletContainerInitializerComparator(initializerOrdering));
        }
        else
        {
            //No jetty-specific ordering specified, or just the wildcard value "*" specified.
            //Fallback to ordering the ServletContainerInitializers according to:
            //container classpath first, WEB-INF/classes then WEB-INF/lib (obeying any web.xml jar ordering)
             
            //no web.xml ordering defined, add SCIs in any order
            if (context.getMetaData().getOrdering() == null)
            {
                if (LOG.isDebugEnabled())  LOG.debug("No web.xml ordering, ServletContainerInitializers in random order");
                nonExcludedInitializers.addAll(sciResourceMap.keySet());
            }
            else
            {
                if (LOG.isDebugEnabled())  LOG.debug("Ordering ServletContainerInitializers with ordering {}",context.getMetaData().getOrdering());
                for (Map.Entry<ServletContainerInitializer, Resource> entry:sciResourceMap.entrySet())
                {
                    //add in SCIs from the container classpath
                    if (entry.getKey().getClass().getClassLoader()==context.getClassLoader().getParent())
                        nonExcludedInitializers.add(entry.getKey());
                    else if (entry.getValue() == null) //add in SCIs not in a jar, as they must be from WEB-INF/classes and can't be ordered
                        nonExcludedInitializers.add(entry.getKey());
                }
              
                //add SCIs according to the ordering of its containing jar
                for (Resource webInfJar:context.getMetaData().getOrderedWebInfJars())
                {
                    for (Map.Entry<ServletContainerInitializer, Resource> entry:sciResourceMap.entrySet())
                    {
                      if (webInfJar.equals(entry.getValue()))
                            nonExcludedInitializers.add(entry.getKey());
                    }
                }
            }
        }   

        if (LOG.isDebugEnabled()) 
        {
            int i=0;
            for (ServletContainerInitializer sci:nonExcludedInitializers)
                LOG.debug("ServletContainerInitializer: {} {} from {}",(++i), sci.getClass().getName(), sciResourceMap.get(sci));
        }    
        
        return nonExcludedInitializers;
    }


    /**
     * Jetty-specific extension that allows an ordering to be applied across ALL ServletContainerInitializers.
     * 
     * @param context the context for the initializer ordering configuration 
     * @return the ordering of the ServletContainerIntializer's
     */
    public ServletContainerInitializerOrdering getInitializerOrdering (WebAppContext context)
    {
        if (context == null)
            return null;
        
        String tmp = (String)context.getAttribute(SERVLET_CONTAINER_INITIALIZER_ORDER);
        if (tmp == null || "".equals(tmp.trim()))
            return null;
        
        return new ServletContainerInitializerOrdering(tmp);
    }


    /**
     * Scan jars on container path.
     * 
     * @param context the context for the scan
     * @param parser the parser to scan with
     * @throws Exception if unable to scan
     */
    public void parseContainerPath (final WebAppContext context, final AnnotationParser parser) throws Exception
    {
        //always parse for discoverable annotations as well as class hierarchy and servletcontainerinitializer related annotations
        final Set<Handler> handlers = new HashSet<Handler>();
        handlers.addAll(_discoverableAnnotationHandlers);
        handlers.addAll(_containerInitializerAnnotationHandlers);
        if (_classInheritanceHandler != null)
            handlers.add(_classInheritanceHandler);

        _containerPathStats = new CounterStatistic();

        for (Resource r : context.getMetaData().getContainerResources())
        {
            //queue it up for scanning if using multithreaded mode
            if (_parserTasks != null)
            {
                ParserTask task = new ParserTask(parser, handlers, r, _containerClassNameResolver);
                _parserTasks.add(task);  
                _containerPathStats.increment();
                if (LOG.isDebugEnabled())
                    task.setStatistic(new TimeStatistic());
            }
        } 
    }


    /**
     * Scan jars in WEB-INF/lib
     * 
     * @param context the context for the scan
     * @param parser the annotation parser to use
     * @throws Exception if unable to scan and/or parse
     */
    public void parseWebInfLib (final WebAppContext context, final AnnotationParser parser) throws Exception
    {   
        List<FragmentDescriptor> frags = context.getMetaData().getFragments();

        //email from Rajiv Mordani jsrs 315 7 April 2010
        //jars that do not have a web-fragment.xml are still considered fragments
        //they have to participate in the ordering
        ArrayList<URI> webInfUris = new ArrayList<URI>();

        List<Resource> jars = null;
        
        if (context.getMetaData().getOrdering() != null)
            jars = context.getMetaData().getOrderedWebInfJars();
        else
            //No ordering just use the jars in any order
            jars = context.getMetaData().getWebInfJars();

        _webInfLibStats = new CounterStatistic();

        for (Resource r : jars)
        {
            //for each jar, we decide which set of annotations we need to parse for
            final Set<Handler> handlers = new HashSet<Handler>();

            FragmentDescriptor f = getFragmentFromJar(r, frags);

            //if its from a fragment jar that is metadata complete, we should skip scanning for @webservlet etc
            // but yet we still need to do the scanning for the classes on behalf of  the servletcontainerinitializers
            //if a jar has no web-fragment.xml we scan it (because it is not excluded by the ordering)
            //or if it has a fragment we scan it if it is not metadata complete
            if (f == null || !isMetaDataComplete(f) || _classInheritanceHandler != null ||  !_containerInitializerAnnotationHandlers.isEmpty())
            {
                //register the classinheritance handler if there is one
                if (_classInheritanceHandler != null)
                    handlers.add(_classInheritanceHandler);

                //register the handlers for the @HandlesTypes values that are themselves annotations if there are any
                handlers.addAll(_containerInitializerAnnotationHandlers);

                //only register the discoverable annotation handlers if this fragment is not metadata complete, or has no fragment descriptor
                if (f == null || !isMetaDataComplete(f))
                    handlers.addAll(_discoverableAnnotationHandlers);

                if (_parserTasks != null)
                {
                    ParserTask task = new ParserTask(parser, handlers,r, _webAppClassNameResolver);
                    _parserTasks.add (task);
                    _webInfLibStats.increment();
                    if (LOG.isDebugEnabled())
                        task.setStatistic(new TimeStatistic());
                }
            }
        }
    }


    /**
     * Scan classes in WEB-INF/classes
     * 
     * @param context the context for the scan
     * @param parser the annotation parser to use
     * @throws Exception if unable to scan and/or parse
     */
    public void parseWebInfClasses (final WebAppContext context, final AnnotationParser parser)
    throws Exception
    {
        Set<Handler> handlers = new HashSet<Handler>();
        handlers.addAll(_discoverableAnnotationHandlers);
        if (_classInheritanceHandler != null)
            handlers.add(_classInheritanceHandler);
        handlers.addAll(_containerInitializerAnnotationHandlers);

        _webInfClassesStats = new CounterStatistic();

        for (Resource dir : context.getMetaData().getWebInfClassesDirs())
        {
            if (_parserTasks != null)
            {
                ParserTask task = new ParserTask(parser, handlers, dir, _webAppClassNameResolver);
                _parserTasks.add(task);
                _webInfClassesStats.increment();
                if (LOG.isDebugEnabled())
                    task.setStatistic(new TimeStatistic());
            }
        }
    }



    /**
     * Get the web-fragment.xml from a jar
     * 
     * @param jar the jar to look in for a fragment
     * @param frags the fragments previously found
     * @return true if the fragment if found, or null of not found
     * @throws Exception if unable to determine the the fragment contains
     */
    public FragmentDescriptor getFragmentFromJar (Resource jar,  List<FragmentDescriptor> frags)
    throws Exception
    {
        //check if the jar has a web-fragment.xml
        FragmentDescriptor d = null;
        for (FragmentDescriptor frag: frags)
        {
            Resource fragResource = frag.getResource(); //eg jar:file:///a/b/c/foo.jar!/META-INF/web-fragment.xml
            if (Resource.isContainedIn(fragResource,jar))
            {
                d = frag;
                break;
            }
        }
        return d;
    }

    public boolean isMetaDataComplete (WebDescriptor d)
    {
        return (d!=null && d.getMetaDataComplete() == MetaDataComplete.True);
    }

    public static class ClassInheritanceMap extends ConcurrentHashMap<String, ConcurrentHashSet<String>>
    {
        
        @Override
        public String toString()
        {
            return String.format("ClassInheritanceMap@%x{size=%d}",hashCode(),size());
        }
    }
}


