/*
 * 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$
 */
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$ $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.isEmpty()) {
      // 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
