/*
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright
 *    statements and notices.  Redistributions must also contain a
 *    copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the
 *    above copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *
 * 3. The name "Exolab" must not be used to endorse or promote
 *    products derived from this Software without prior written
 *    permission of Intalio, Inc.  For written permission,
 *    please contact info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab"
 *    nor may "Exolab" appear in their names without prior written
 *    permission of Intalio, Inc. Exolab is a registered
 *    trademark of Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project
 *    (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * INTALIO, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 1999-2004 (C) Intalio, Inc. All Rights Reserved.
 *
 * This file was originally developed by Keith Visco during the
 * course of employment at Intalio Inc.
 * All portions of this file developed by Keith Visco after Jan 19 2005 are
 * Copyright (C) 2005 Keith Visco. All Rights Reserved.
 *
 * $Id: XMLClassDescriptorResolverImpl.java 8691 2010-08-20 22:36:39Z wguttmn $
 */
package org.exolab.castor.xml.util;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.castor.xml.InternalContext;
import org.exolab.castor.mapping.ClassDescriptor;
import org.exolab.castor.mapping.MappingLoader;
import org.exolab.castor.xml.Introspector;
import org.exolab.castor.xml.ResolverException;
import org.exolab.castor.xml.XMLClassDescriptor;
import org.exolab.castor.xml.XMLClassDescriptorResolver;
import org.exolab.castor.xml.util.resolvers.ResolveHelpers;

/**
 * The default implementation of the ClassDescriptorResolver interface.
 *
 * @author <a href="mailto:keith AT kvisco DOT com">Keith Visco</a>
 * @version $Revision: 8691 $ $Date: 2006-04-25 15:08:23 -0600 (Tue, 25 Apr 2006) $
 */
public class XMLClassDescriptorResolverImpl implements XMLClassDescriptorResolver {
    /** 
     * The Logger instance to use.
     */
    private static final Log LOG = LogFactory.getLog(XMLClassDescriptorResolverImpl.class);

    /**
     * All resolved descriptors are kept here.
     */
    private DescriptorCacheImpl _descriptorCache;
    /**
     * The MappingLoader instance to read descriptors from.
     */
    private MappingLoader _mappingLoader;
    /**
     * The domain class loader to use.
     */
    private ClassLoader _classLoader;
    /**
     * A flag to signal if introspection should be used or not.
     */
    private Boolean _useIntrospector;
    /**
     * A flag to signal if descriptors should be determines via package
     * file .castor.cdr .
     */
    private Boolean _loadPackageMappings;
    /**
     * The introspector to use.
     */
    private Introspector _introspector;
    /**
     * The place where all resolving strategies and their commands put the results into
     * and can be read from.
     */
    private ResolverStrategy _resolverStrategy;

    /**
     * Creates a new ClassDescriptorResolverImpl.
     * It is left empty to avoid cycles at construction. To guarantee
     * backward compatibility the backwardInit method will do all
     * required initialization if it hadn't happened before.
     */
    public XMLClassDescriptorResolverImpl() {
        super();
        _descriptorCache = new DescriptorCacheImpl();
    }

    /**
     * {@inheritDoc}
     * The InternalContext itself is not stored! But all values of interest are read
     * and stored in local attributes.
     */
    public void setInternalContext(final InternalContext internalContext) {
        _mappingLoader = internalContext.getMappingLoader();
        _classLoader = internalContext.getClassLoader();
        _useIntrospector = internalContext.getUseIntrospector();
        _loadPackageMappings = internalContext.getLoadPackageMapping();
        _introspector = internalContext.getIntrospector();
        _resolverStrategy = internalContext.getResolverStrategy();
    }

    /**
     * {@inheritDoc}
     */
    public MappingLoader getMappingLoader() {
        return _mappingLoader;
    }
    
    /**
     * {@inheritDoc}
     */
    public void setClassLoader(final ClassLoader loader) {
        _classLoader = loader;
    }

    /**
     * {@inheritDoc}
     */
    public void setUseIntrospection(final boolean enable) {
        _useIntrospector = Boolean.valueOf(enable);
    }
    
    /**
     * {@inheritDoc}
     */
    public void setLoadPackageMappings(final boolean loadPackageMappings) {
        _loadPackageMappings = Boolean.valueOf(loadPackageMappings);
    }
    
    /**
     * {@inheritDoc}
     */
    public void setMappingLoader(final MappingLoader mappingLoader) {
        _mappingLoader = mappingLoader;
        if (mappingLoader != null) {
            for (ClassDescriptor classDescriptor : mappingLoader.getDescriptors()) {
                _descriptorCache.addDescriptor(classDescriptor.getJavaClass().getName(), 
                        (XMLClassDescriptor) classDescriptor);
            }
        }
    }
    
    /**
     * {@inheritDoc}
     */
    public void setIntrospector(final Introspector introspector) {
        _introspector = introspector;
    }

    /**
     * {@inheritDoc}
     */
    public void setResolverStrategy(final ResolverStrategy resolverStrategy) {
        _resolverStrategy = resolverStrategy;
    }
    
    /**
     * XMLClassDescriptorResolver was originally build to collect all required
     * information by itself... now with introduction of XMLContext and a more
     * IoC like concepts that all information is injected into a class... things
     * are different but this methods is there to guarantee backward 
     * compatibility.
     * @return the {@link ResolverStrategy} to use
     */
    private ResolverStrategy getResolverStrategy() {
        setAttributesIntoStrategy();
        return _resolverStrategy;
    }
    
    /**
     * {@inheritDoc}
     */
    public ClassDescriptor resolve(final Class<?> type) throws ResolverException {
        if (type == null) {
            String message = "Type argument must not be null for resolve";
            LOG.warn(message);
            throw new IllegalArgumentException(message);
        }
        
        if (_descriptorCache.isMissingDescriptor(type.getName())) {
            if (LOG.isTraceEnabled()) {
                LOG.trace("Descriptor for " + type.getName() + " already marked as *MISSING*.");
            }
            return null;
        }
        
        if (_descriptorCache.getDescriptor(type.getName()) != null) {
            return _descriptorCache.getDescriptor(type.getName());
        }
        
        ClassLoader l = _classLoader;
        if (l == null) { l = type.getClassLoader(); }
        if (l == null) { l = Thread.currentThread().getContextClassLoader(); }
        
        return this.resolve(type.getName(), l);
    } // -- resolve(Class)

    /**
     * {@inheritDoc}
     */
    public XMLClassDescriptor resolve(final String className) throws ResolverException {
        if (className == null || className.length() == 0) {
            String message = "Cannot resolve a null or zero-length class name.";
            LOG.warn(message);
            throw new IllegalArgumentException(message);
        }
        
        if (_descriptorCache.isMissingDescriptor(className)) {
            if (LOG.isTraceEnabled()) {
                LOG.trace("Descriptor for " + className + " already marked as *MISSING*.");
            }
            return null;
        }
        
        if (_descriptorCache.getDescriptor(className) != null) {
            return _descriptorCache.getDescriptor(className);
        }
        
        ClassLoader l = _classLoader;
        if (l == null) { l = Thread.currentThread().getContextClassLoader(); }
        
        return this.resolve(className, l);
    }

    /**
     * {@inheritDoc}
     */
    public XMLClassDescriptor resolve(final String className, final ClassLoader loader)
    throws ResolverException {
        if (className == null || className.length() == 0) {
            String message = "Cannot resolve a null or zero-length class name.";
            LOG.warn(message);
            throw new IllegalArgumentException(message);
        }

        if (_descriptorCache.isMissingDescriptor(className)) {
            if (LOG.isTraceEnabled()) {
                LOG.trace("Descriptor for " + className + " already marked as *MISSING*.");
            }
            return null;
        }

        if (_descriptorCache.getDescriptor(className) != null) {
            return _descriptorCache.getDescriptor(className);
        }
        
        ClassLoader l = loader;
        if (l == null) { l = _classLoader; }
        if (l == null) { l = Thread.currentThread().getContextClassLoader(); }
        
        getResolverStrategy().setProperty(ResolverStrategy.PROPERTY_CLASS_LOADER, l);
        return (XMLClassDescriptor) getResolverStrategy().resolveClass(_descriptorCache, className);
    } //-- resolve(String, ClassLoader)

    /**
     * {@inheritDoc}
     */
    public XMLClassDescriptor resolveByXMLName(final String xmlName, final String namespaceURI,
            final ClassLoader loader) {
        
        if (xmlName == null || xmlName.length() == 0) {
            String message = "Cannot resolve a null or zero-length class name.";
            LOG.warn(message);
            throw new IllegalArgumentException(message);
        }
        
        // @TODO Joachim 2007-05-05 the class loader is NOT used!
        // get a list of all descriptors with the correct xmlName, regardless of their namespace
        List<ClassDescriptor> possibleMatches = _descriptorCache.getDescriptors(xmlName);
        if (possibleMatches.size() == 0) {
            // nothing matches that XML name
            return null;
        }
        if (possibleMatches.size() == 1) {
            // we have exactly one possible match - that's our result
            // (if it has the right namespace, it's an exact match, if not its
            // the only possible match)
            return (XMLClassDescriptor) possibleMatches.get(0);
        }

        // we have more than one result - only an exact match can be the result
        for (Iterator<ClassDescriptor> i = possibleMatches.iterator(); i.hasNext(); ) {
            XMLClassDescriptor descriptor = (XMLClassDescriptor) i.next();

            if (ResolveHelpers.namespaceEquals(namespaceURI, descriptor.getNameSpaceURI())) {
                return descriptor;
            }
        }

        // no exact match and too many possible matches...
        return null;
    } //-- resolveByXMLName

    /**
     * {@inheritDoc}
     */
    public Iterator<ClassDescriptor> resolveAllByXMLName(final String xmlName, 
            final String namespaceURI, final ClassLoader loader) {
        
        if (xmlName == null || xmlName.length() == 0) {
            String message = "Cannot resolve a null or zero-length xml name.";
            LOG.warn(message);
            throw new IllegalArgumentException(message);
        }

        // get all descriptors with the matching xml name
        return _descriptorCache.getDescriptors(xmlName).iterator();
    } //-- resolveAllByXMLName

    /**
     * {@inheritDoc}
     */
    public void addClass(final String className) throws ResolverException {
        this.resolve(className);
    }

    /**
     * {@inheritDoc}
     */
    public void addClasses(final String[] classNames) throws ResolverException {
        for (int i = 0; i < classNames.length; i++) {
            String className = classNames[i];
            this.addClass(className);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void addClass(final Class<?> clazz) throws ResolverException {
        this.resolve(clazz);
    }

    /**
     * {@inheritDoc}
     */
    public void addClasses(final Class<?>[] clazzes) throws ResolverException {
        for (int i = 0; i < clazzes.length; i++) {
            Class<?> clazz = clazzes[i];
            this.addClass(clazz);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void addPackage(final String packageName) throws ResolverException {
        if (packageName == null || packageName.length() == 0) {
            String message = "Cannot resolve a null or zero-length package name.";
            LOG.warn(message);
            throw new IllegalArgumentException(message);
        }
        
        getResolverStrategy().resolvePackage(_descriptorCache, packageName);
    }

    /**
     * {@inheritDoc}
     */
    public void addPackages(final String[] packageNames) throws ResolverException {
        for (int i = 0; i < packageNames.length; i++) {
            String packageName = packageNames[i];
            this.addPackage(packageName);
        }
    }
       
    /**
     * {@inheritDoc}
     * @deprecated
     */
    public void loadClassDescriptors(final String packageName) throws ResolverException {
        String message = "Already deprecated in the interface!";
        LOG.warn(message);
        throw new UnsupportedOperationException();
    }
    
    /**
     * To set all strategy properties to the values of the attributes of this instance.
     * Only exception is the class loader property which is always set in the resolve method.
     */
    private void setAttributesIntoStrategy() {
        ResolverStrategy strategy = _resolverStrategy;
        strategy.setProperty(
                ResolverStrategy.PROPERTY_LOAD_PACKAGE_MAPPINGS, 
                _loadPackageMappings);
        strategy.setProperty(
                ResolverStrategy.PROPERTY_USE_INTROSPECTION, _useIntrospector);
        strategy.setProperty(
                ResolverStrategy.PROPERTY_MAPPING_LOADER, _mappingLoader);
        strategy.setProperty(
                ResolverStrategy.PROPERTY_INTROSPECTOR, _introspector);
    }

    /**
     * Internal cache for XMLClassDescriptors.<br>
     * <br>
     * The cache maintains all descriptors loaded by its
     * <code>XMLClassDescriptorResolver</code>. It also keeps track of
     * mapping files and CDR lists that have been loaded. Just like the
     * ClassCache it also has a list of missing descriptors to avoid trying to
     * load those descriptors again.
     * 
     * The cached descriptors are available via the name of the classes they
     * describe or via their XML name from a mapping file.
     * 
     * @author <a href="mailto:stevendolg AT gxm DOT at">Steven Dolg</a>
     */
    private static class DescriptorCacheImpl implements ResolverStrategy.ResolverResults {
        /** Logger to be used by DescriptorCache. */
        private static final Log LOG2 = LogFactory.getLog(DescriptorCacheImpl.class);
        /** Some fixed text to detect errors... */
        private static final String INTERNAL_CONTAINER_NAME = "-error-if-this-is-used-";

        /** List of class names a descriptor is not available for. */
        private final List<String> _missingTypes;

        /** Map of cached descriptors with the class names they describe as key. */
        private final Map<String, ClassDescriptor> _typeMap;

        /** Map of cached descriptors with their XML names as key. */
        private final Map<String, List<ClassDescriptor>> _xmlNameMap;

        /** Lock used to isolate write accesses to the caches internal lists and maps. */
        private final ReentrantReadWriteLock _lock;

        /**
         * Default constructor.<br>
         * <br>
         * Initializes all lists and maps.
         */
        public DescriptorCacheImpl() {
            super();
            
            LOG2.debug("New instance!");
            
            _typeMap = new HashMap<String, ClassDescriptor>();
            _xmlNameMap = new HashMap<String, List<ClassDescriptor>>();
            _missingTypes = new ArrayList<String>();
            _lock = new ReentrantReadWriteLock();
        } //--- DescriptorCacheImpl

        /**
         * Adds a descriptor to this caches maps.<br>
         * The descriptor is mapped both with the class name and its XML name.
         * 
         * The descriptor will not be mapped with its XML name is
         * <code>null</code>, the empty string (""), or has the value of the
         * constant INTERNAL_CONTAINER_NAME.
         * 
         * If there already is a descriptor for the given <code>className</code>
         * and/or the descriptor's XML name the previously cached descriptor is
         * replaced.
         * 
         * @param className The class name to be used for mapping the given descriptor.
         * @param descriptor The descriptor to be mapped.
         * @throws InterruptedException 
         * 
         * @see #INTERNAL_CONTAINER_NAME
         */
        public void addDescriptor(final String className, final XMLClassDescriptor descriptor) {
            if ((className == null) || (className.length() == 0)) {
                String message = "Class name to insert ClassDescriptor must not be null";
                LOG2.warn(message);
                throw new IllegalArgumentException(message);
            }

            // acquire write lock first
            _lock.writeLock().lock();
            try {

                if (descriptor == null) {
                    if (LOG2.isDebugEnabled()) {
                        LOG2.debug("Adding class name to missing classes: " + className);
                    }
                    _missingTypes.add(className);
                    return;
                }
                
                if (LOG2.isDebugEnabled()) {
                    LOG2.debug("Adding descriptor class for: " 
                            + className + " descriptor: " + descriptor);
                }
                _typeMap.put(className, descriptor);
    
                String xmlName = descriptor.getXMLName();
                // ignore descriptors with an empty XMLName
                if (xmlName == null || xmlName.length() == 0) {
                    return;
                }
    
                // ignore descriptors with the internal XMLName
                if (INTERNAL_CONTAINER_NAME.equals(xmlName)) {
                    return;
                }
    
                // add new descriptor to the list for the corresponding XML name  
                List<ClassDescriptor> descriptorList = _xmlNameMap.get(xmlName);
                if (descriptorList == null) {
                    descriptorList = new ArrayList<ClassDescriptor>();
                    _xmlNameMap.put(xmlName, descriptorList);
                }
                if (!descriptorList.contains(descriptor)) {
                    descriptorList.add(descriptor);
                }
                
                _missingTypes.remove(className);
            } finally {
                _lock.writeLock().unlock();
            }
        } //-- addDescriptor

        /**
         * Gets the descriptor that is mapped to the given class name.
         * 
         * @param className The class name to get a descriptor for.
         * @return The descriptor mapped to the given name or <code>null</code>
         *         if no descriptor is stored in this cache.
         */
        public XMLClassDescriptor getDescriptor(final String className) {

            // acquire read lock which is released via finally block
            _lock.readLock().lock();
            try {

                if ((className == null) 
                        || ("".equals(className)) 
                        || (_missingTypes.contains(className))) {
                    return null;
                }
                
                XMLClassDescriptor ret = (XMLClassDescriptor) _typeMap.get(className);
                if (LOG2.isDebugEnabled()) {
                    LOG2.debug("Get descriptor for: " + className + " found: " + ret);
                }
                return ret;
            } finally {
                _lock.readLock().unlock();
            }
        } //-- getDescriptor

        /**
         * Gets a list of descriptors that have the given XML name.<br>
         * <br>
         * This method will return all previously cached descriptors with the
         * given XML name regardless of their name space.
         * 
         * @param xmlName The XML name of the descriptors to get.
         * @return A list of descriptors with the given XML name or an empty
         *         list if no such descriptor is stored in this cache. This
         *         method will never return <code>null</code>!
         */
        public List<ClassDescriptor> getDescriptors(final String xmlName) {

            // before accessing XML name map acquire read lock first
            _lock.readLock().lock();
            List<ClassDescriptor> list = _xmlNameMap.get(xmlName);
            _lock.readLock().unlock();

            if (list == null) {

                // return an empty list
                list = new ArrayList<ClassDescriptor>();
            } else {

                // return a copy of the original list
                list = new ArrayList<ClassDescriptor>(list);
            }
            return list;
        } //-- getDescriptorList

        /**
         * Checks whether the given class name is contained in the list of class
         * names the descriptor is found to be missing.<br>
         * <br>
         * NOTE: This does not check whether a descriptor is stored within this
         * cache or not. This rather checks the list of class names the
         * XMLClassDescriptorResolverImpl found to have no descriptor. The cache
         * itself has no means of determining that this is the case and thus
         * will never add/remove class names to/from it.
         * 
         * @param className The class name to be checked.
         * @return <code>true</code> If the given class name was stated to
         *         have no descriptor by a previous call to
         *         <code>addMissingDescriptor</code> with exactly the same
         *         class name. <code>false</code> otherwise.
         * 
         * @see #addMissingDescriptor(String)
         */
        public boolean isMissingDescriptor(final String className) {

            // before accessing list with missing types acquire read lock first
            _lock.readLock().lock();
            try {
                return _missingTypes.contains(className);
            } finally {
                _lock.readLock().unlock();
            }
        } //-- isMissingDescriptor

        /**
         * To add not only a single descriptor but a map of descriptors at once.
         * 
         * @param descriptors a Map of className (String) and XMLClassDescriptor pairs
         */
        public void addAllDescriptors(final Map descriptors) {
            if ((descriptors == null) || (descriptors.isEmpty())) {
                LOG2.debug("Called addAllDescriptors with null or empty descriptor map");
                return;
            }
            
            for (Iterator<String> iter = descriptors.keySet().iterator(); iter.hasNext(); ) {
                String clsName = iter.next();
                this.addDescriptor(clsName, (XMLClassDescriptor) descriptors.get(clsName));
            }
        }
    } // -- DescriptorCacheImpl
    
    /**
     * Cleans the descriptor cache.
     * @see org.exolab.castor.xml.XMLClassDescriptorResolver#cleanDescriptorCache()
     */
    public void cleanDescriptorCache() {
        _descriptorCache = new DescriptorCacheImpl();
    }
} // -- ClassDescriptorResolverImpl
