/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.uima.resource.impl;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.uima.UIMAFramework;
import org.apache.uima.UIMA_IllegalStateException;
import org.apache.uima.analysis_engine.impl.AnalysisEngineImplBase;
import org.apache.uima.analysis_engine.impl.PearAnalysisEngineWrapper;
import org.apache.uima.analysis_engine.impl.PearAnalysisEngineWrapper.StringPair;
import org.apache.uima.internal.util.Class_TCCL;
import org.apache.uima.internal.util.Misc;
import org.apache.uima.internal.util.UIMAClassLoader;
import org.apache.uima.resource.CasManager;
import org.apache.uima.resource.DataResource;
import org.apache.uima.resource.ExternalResourceDependency;
import org.apache.uima.resource.ExternalResourceDescription;
import org.apache.uima.resource.FileResourceSpecifier;
import org.apache.uima.resource.ParameterizedDataResource;
import org.apache.uima.resource.RelativePathResolver;
import org.apache.uima.resource.Resource;
import org.apache.uima.resource.ResourceAccessException;
import org.apache.uima.resource.ResourceInitializationException;
import org.apache.uima.resource.ResourceManager;
import org.apache.uima.resource.ResourceSpecifier;
import org.apache.uima.resource.SharedResourceObject;
import org.apache.uima.resource.metadata.ExternalResourceBinding;
import org.apache.uima.resource.metadata.ResourceManagerConfiguration;
import org.apache.uima.util.Level;
import org.apache.uima.util.XMLizable;

/**
 * Reference implementation of {@link org.apache.uima.resource.ResourceManager}.
 * 
 * 
 */
public class ResourceManager_impl implements ResourceManager {
  private static final AtomicInteger IMPORT_URL_CACHE_WARNING_THROTTLE = new AtomicInteger();

 // @formatter:off
  /**
   * Ties an External Resource instance to
   *   - its description 
   *     -- name
   *     -- textual description
   *     -- a ResourceSpecifier describing how to create it
   *     -- (optional) the String name of the Java class that implements the resource)
   *   - its defining UIMA Context
   *   
   *   These are used to validate multiple declarations, and to get
   *   a resource to tie it to a binding
   */
  // @formatter:on
  static protected class ResourceRegistration { // make protected
                                                // https://issues.apache.org/jira/browse/UIMA-2102
    /**
     * For ParameterizedDataResources or DataResources, is the implementation object, which is an
     * arbitrary Java class implementing SharedDataResource (which has the "load" method)
     * 
     * If the external resource specification omitted the implementation class, a default
     * FileResource
     */
    Object resource;

    ExternalResourceDescription description;

    String definingContext;

    public ResourceRegistration(Object resourceOrImplementation,
            ExternalResourceDescription description, String definingContext) {
      resource = resourceOrImplementation;
      this.description = description;
      this.definingContext = definingContext;
    }
  }

  /**
   * resource bundle for log messages
   */
  protected static final String LOG_RESOURCE_BUNDLE = "org.apache.uima.impl.log_messages";

  protected static final Class<Resource> EMPTY_RESOURCE_CLASS = Resource.class;

  private static final URL[] emptyURLarray = new URL[0];

  private AtomicBoolean isDestroyed = new AtomicBoolean(false);

  /**
   * a monitor lock for synchronizing get/set of casManager ref
   */
  private final Object casManagerMonitor = new Object();

  /**
   * Object used for resolving relative paths. This is built by parsing the data path.
   */
  private final RelativePathResolver mRelativePathResolver;

  /**
   * Map from qualified key names (declared in resource dependency XML) to either DataResource
   * objects or SharedResourceObjects that impl Resource.
   * 
   * This map is many to one (multiple keys may refer to the same Resource object)
   * 
   * Can't be concurrentMap because it (currently) depends on storing nulls
   * 
   * Keeping the Map's 2nd argument as Object, although it could be Resource, for backwards
   * compatibility
   */
  final protected Map<String, Object> mResourceMap;

  /**
   * Internal map from resource names (declared in resource declaration XML) to ResourceRegistration
   * objects. Uses: for detecting if multiple declarations are made for the same resource name
   * .values() for list of all resources that have been registered
   * 
   * This is a one-to-one map.
   */
  final protected Map<String, ResourceRegistration> mInternalResourceRegistrationMap;

  /**
   * Map from String keys to Class objects. For ParameterizedResources only, stores the
   * implementation class (not a Resource) corresponding to each resource name.
   * 
   * These class objects may or may not be Resource instances. They may be arbitrary classes, except
   * that they must implement SharedResourceObject.
   * 
   * This is a many to one map; many keys may refer to the same class
   * 
   * key = aQualifiedContextName + the key name in an external resource binding
   */
  final protected Map<String, Class<?>> mParameterizedResourceImplClassMap;

  /**
   * Internal map from resource names (declared in resource declaration XML) to Class objects for
   * parameterized Resource.
   * 
   * These class objects may or may not be Resource instances. They may be arbitrary classes, except
   * that they must implement SharedResourceObject.
   * 
   * These are "customized" when referenced, by parameter strings (such as language, for a
   * Dictionary resource). Used internally during resource initialization.
   * 
   * key = external resource declared name.
   */
  final protected Map<String, Class<?>> mInternalParameterizedResourceImplClassMap;

  /**
   * Map from ParameterizedResourceKey to SharedResourceObject or DataResource instances.
   * 
   * Note: SharedResourceObjects are not "Resource" instances, but rather arbitrary Java classes
   * that are the "implementations" of Resources. The only requirement on these Java classes is that
   * they implement SharedResourceObject interface.
   * 
   * This map is for ParameterizedResources only, and stores the SharedResourceObjects or
   * DataResource objects that have already been instantiated and loaded.
   */
  final protected Map<List<Object>, Object> mParameterizedResourceInstanceMap;

  /**
   * UIMA extension ClassLoader. ClassLoader is created if an extension classpath is specified at
   * the ResourceManager
   * 
   * volatile might be better than synch sets/gets
   */
  private volatile UIMAClassLoader uimaCL = null;

  /** CasManager - manages creation and pooling of CASes. */
  // volatile to support double-checked locking idiom
  protected volatile CasManager mCasManager = null;

//@formatter:off
  /**
   * Cache of imported descriptors, so that parsed objects can be reused if the same URL is imported
   * more than once.
   * 
   * All callers of this synchronize on the importCache object before doing a 
   *    get
   *    ...
   *    put
   * sequence
   * 
   * Use Case where synchronization is needed:
   *   running multiple instances on multiple threads, sharing a common resource manager,
   *   the initialization that merges typePriorities happens lazily, when using Cas Multipliers,
   *   and occurs when the first getCas call happens on a thread.  Although these calls
   *   are synchronized among themselves, any other use of this map that might occur
   *   simultaneously is not.
   */
//@formatter:on
  // leaving this as a synchronizedMap - for backwards compatibility
  // internal users do sync around get/set pairs anyways, but can't rely on
  // what external users do
  // Because internal users do a sync, only one thread at a time is using this
  // (for internal calls) anyways, so there's no advantage to the extra overhead
  // of making this a ConcurrentHashMap (March 2014)
  private Map<String, XMLizable> importCache = Collections.synchronizedMap(new HashMap<>());

  /**
   * Cache of imported descriptor URLs from which the parsed objects in importCache were created, so
   * that these URLs are not re-parsed if the same URL is imported again.
   */
  private Map<String, Set<String>> importUrlsCache = Collections.synchronizedMap(new HashMap<>());

  /**
   * Creates a new <code>ResourceManager_impl</code>.
   */
  public ResourceManager_impl() {
    mResourceMap = Collections.synchronizedMap(new HashMap<>());
    mInternalResourceRegistrationMap = new ConcurrentHashMap<>();
    mParameterizedResourceImplClassMap = new ConcurrentHashMap<>();
    mInternalParameterizedResourceImplClassMap = new ConcurrentHashMap<>();
    mParameterizedResourceInstanceMap = new ConcurrentHashMap<>();
    mRelativePathResolver = new RelativePathResolver_impl();
  }

  /**
   * Creates a new <code>ResourceManager_impl</code> with a custom ClassLoader to use for locating
   * resources.
   * 
   * @param aClassLoader
   *          -
   */
  public ResourceManager_impl(ClassLoader aClassLoader) {
    mResourceMap = Collections.synchronizedMap(new HashMap<>());
    mInternalResourceRegistrationMap = new ConcurrentHashMap<>();
    mParameterizedResourceImplClassMap = new ConcurrentHashMap<>();
    mInternalParameterizedResourceImplClassMap = new ConcurrentHashMap<>();
    mParameterizedResourceInstanceMap = new ConcurrentHashMap<>();
    mRelativePathResolver = new RelativePathResolver_impl(aClassLoader);
  }

  /*
   * Version for Pear wrapper
   */
  public ResourceManager_impl(Map<String, Object> resourceMap,
          Map<String, ResourceRegistration> internalResourceRegistrationMap,
          Map<String, Class<?>> parameterizedResourceImplClassMap,
          Map<String, Class<?>> internalParameterizedResourceImplClassMap,
          Map<List<Object>, Object> parameterizedResourceInstanceMap) {
    mResourceMap = resourceMap;
    mInternalResourceRegistrationMap = internalResourceRegistrationMap;
    mParameterizedResourceImplClassMap = parameterizedResourceImplClassMap;
    mInternalParameterizedResourceImplClassMap = internalParameterizedResourceImplClassMap;
    mParameterizedResourceInstanceMap = parameterizedResourceInstanceMap;
    mRelativePathResolver = new RelativePathResolver_impl();
  }

  public ResourceManager_impl copy() {
    ResourceManager_impl rm = new ResourceManager_impl(mResourceMap,
            mInternalResourceRegistrationMap, mParameterizedResourceImplClassMap,
            mInternalParameterizedResourceImplClassMap,
            mParameterizedResourceInstanceMap);
    // non-final fields init
    rm.uimaCL = uimaCL;
    rm.importCache = importCache;
    rm.importUrlsCache = importUrlsCache;
    return rm;
  }

  /**
   * Support reusing UIMA Class Loader instances to speed up things including the Component
   * Description Editor when obtaining info from CustomResourceSpecifiers
   * https://issues.apache.org/jira/browse/UIMA-1722
   * 
   * @param uimaCL
   *          -
   * @param resolveResource
   *          -
   */
  public synchronized void setExtensionClassPath(UIMAClassLoader uimaCL, boolean resolveResource) {
    this.uimaCL = uimaCL;

    if (resolveResource) {
      // set UIMA extension ClassLoader also to resolve resources
      getRelativePathResolver().setPathResolverClassLoader(uimaCL);
    }
  }

  /**
   * 
   * /**
   * 
   * @see org.apache.uima.resource.ResourceManager#setExtensionClassPath(java.lang.String, boolean)
   */
  @Override
  public synchronized void setExtensionClassPath(String classpath, boolean resolveResource)
          throws MalformedURLException {
    // create UIMA extension ClassLoader with the given classpath
    uimaCL = new UIMAClassLoader(classpath, Class_TCCL.get_parent_cl());

    if (resolveResource) {
      // set UIMA extension ClassLoader also to resolve resources
      getRelativePathResolver().setPathResolverClassLoader(uimaCL);
    }
  }

  /**
   * @see org.apache.uima.resource.ResourceManager#setExtensionClassPath(ClassLoader,java.lang.String,
   *      boolean)
   */
  @Override
  public synchronized void setExtensionClassPath(ClassLoader parent, String classpath,
          boolean resolveResource) throws MalformedURLException {
    // create UIMA extension ClassLoader with the given classpath
    uimaCL = new UIMAClassLoader(classpath, parent);

    if (resolveResource) {
      // set UIMA extension ClassLoader also to resolve resources
      getRelativePathResolver().setPathResolverClassLoader(uimaCL);
    }
  }

  // https://issues.apache.org/jira/browse/UIMA-5553
  // https://issues.apache.org/jira/browse/UIMA-5609
  // synchronized because the other methods that set the extension class loader are.
  public synchronized void setExtensionClassLoaderImpl(ClassLoader classLoader,
          boolean resolveResource) {
    uimaCL = (classLoader instanceof UIMAClassLoader) ? ((UIMAClassLoader) classLoader)
            : new UIMAClassLoader(emptyURLarray, classLoader);
    if (resolveResource) {
      // set UIMA extension ClassLoader also to resolve resources
      getRelativePathResolver().setPathResolverClassLoader(uimaCL);
    }
  }

  /**
   * @see org.apache.uima.resource.ResourceManager#getExtensionClassLoader()
   */
  @Override
  public ClassLoader getExtensionClassLoader() {
    return uimaCL;
  }

  /**
   * @see org.apache.uima.resource.ResourceManager#getDataPath()
   */
  @Override
  public String getDataPath() {
    return getRelativePathResolver().getDataPath();
  }

  @Override
  public List<String> getDataPathElements() {
    return getRelativePathResolver().getDataPathElements();
  }

  /**
   * @see org.apache.uima.resource.ResourceManager#setDataPath(String)
   */
  @Override
  public void setDataPath(String aPath) throws MalformedURLException {
    getRelativePathResolver().setDataPath(aPath);
  }

  @Override
  public void setDataPathElements(String... aElements) throws MalformedURLException {
    getRelativePathResolver().setDataPathElements(aElements);
  }

  @Override
  public void setDataPathElements(File... aElements) throws MalformedURLException {
    getRelativePathResolver().setDataPathElements(aElements);
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.uima.resource.ResourceManager#resolveRelativePath(java.lang.String)
   */
  @Override
  public URL resolveRelativePath(String aRelativePath) throws MalformedURLException {
    URL relativeUrl;
    try {
      relativeUrl = new URL(aRelativePath);
    } catch (MalformedURLException e) {
      relativeUrl = new URL("file", "", aRelativePath);
    }
    return getRelativePathResolver().resolveRelativePath(relativeUrl);
  }

  private void checkDestroyed() {
    if (isDestroyed.get()) {
      throw new IllegalStateException("ResourceManager is destroyed");
    }
  }

  /**
   * @see org.apache.uima.resource.ResourceManager#getResource(String)
   */
  @Override
  public Object getResource(String aName) throws ResourceAccessException {
    checkDestroyed();
    Object r = mResourceMap.get(aName);
    // if this is a ParameterizedDataResource, it is an error
    if (r instanceof ParameterizedDataResource) {
      throw new ResourceAccessException(ResourceAccessException.PARAMETERS_REQUIRED,
              new Object[] { aName });
    }
    return r;
  }

  /**
   * @see org.apache.uima.resource.ResourceManager#getResource(java.lang.String, java.lang.String[])
   */
  @Override
  public Object getResource(String aName, String[] aParams) throws ResourceAccessException {
   // @formatter:off
    /* Multi-core design
     *   This may be called by user code sharing the same Resource Manager, and / or the same 
     *     uima context object.
     *   Do double-checked idiom to avoid locking where resource is already available, loaded   
     */
   // @formatter:on
    checkDestroyed();
    Object r = mResourceMap.get(aName);

    // if no resource found, return null
    if (r == null) {
      return null;
    }
    // if not a ParameterizedDataResource, it is an error
    if (!(r instanceof ParameterizedDataResource)) {
      throw new ResourceAccessException(ResourceAccessException.PARAMETERS_NOT_ALLOWED,
              new Object[] { aName });
    }
    ParameterizedDataResource pdr = (ParameterizedDataResource) r;

    // get a particular DataResource instance for the specified parameters
    DataResource dr;
    try {
      dr = pdr.getDataResource(aParams);
    } catch (ResourceInitializationException e) {
      throw new ResourceAccessException(e);
    }

    // see if we've already encountered this DataResource under this resource name
    List<Object> nameAndResource = new ArrayList<>(2);
    nameAndResource.add(aName);
    nameAndResource.add(dr);
    Object resourceInstance = mParameterizedResourceInstanceMap.get(nameAndResource);
    if (resourceInstance != null) {
      return resourceInstance;
    }
    synchronized (mParameterizedResourceInstanceMap) {
      // double-check idiom
      resourceInstance = mParameterizedResourceInstanceMap.get(nameAndResource);
      if (resourceInstance != null) {
        return resourceInstance;
      }
      // We haven't encountered this before. See if we need to instantiate a
      // SharedResourceObject
      Class<?> sharedResourceObjectClass = mParameterizedResourceImplClassMap.get(aName);
      if (sharedResourceObjectClass != EMPTY_RESOURCE_CLASS) {
        try {
          SharedResourceObject sro = (SharedResourceObject) sharedResourceObjectClass.newInstance();
          sro.load(dr);
          mParameterizedResourceInstanceMap.put(nameAndResource, sro);
          return sro;
        } catch (InstantiationException e) {
          throw new ResourceAccessException(e);
        } catch (IllegalAccessException e) {
          throw new ResourceAccessException(e);
        } catch (ResourceInitializationException e) {
          throw new ResourceAccessException(e);
        }
      } else
      // no impl. class - just return the DataResource
      {
        mParameterizedResourceInstanceMap.put(nameAndResource, dr);
        return dr;
      }
    }
  }

  /**
   * @see org.apache.uima.resource.ResourceManager#getResourceClass(java.lang.String)
   */
  @Override
  @SuppressWarnings("unchecked")
  public Class<?> getResourceClass(String aName) {
    checkDestroyed();
    Object r = mResourceMap.get(aName);
    if (r == null) // no such resource
    {
      return null;
    }

    // if this is a ParameterizedDataResource, look up its class
    if (r instanceof ParameterizedDataResource) {
      Class<?> customResourceClass = mParameterizedResourceImplClassMap.get(aName);
      if (customResourceClass == EMPTY_RESOURCE_CLASS) {
        // return the default class
        return DataResource_impl.class;
      }
      return customResourceClass;
    } else {
      // return r's Class
      // could be, for
      return (Class<? extends Resource>) r.getClass();
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.uima.resource.ResourceManager#getResourceAsStream(java.lang.String,
   * java.lang.String[])
   */
  @Override
  public InputStream getResourceAsStream(String aKey, String[] aParams)
          throws ResourceAccessException {
    return getResourceAsStreamCommon(getResource(aKey, aParams));
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.uima.resource.ResourceManager#getResourceAsStream(java.lang.String)
   */
  @Override
  public InputStream getResourceAsStream(String aKey) throws ResourceAccessException {
    return getResourceAsStreamCommon(getResource(aKey));
  }

  private InputStream getResourceAsStreamCommon(Object resource) throws ResourceAccessException {
    checkDestroyed();
    try {
      if (resource != null && resource instanceof DataResource) {
        return ((DataResource) resource).getInputStream();
      } else {
        return null;
      }
    } catch (IOException e) {
      throw new ResourceAccessException(e);
    }
  }

  private URL getResourceAsStreamCommonUrl(Object resource) {
    if (resource != null && resource instanceof DataResource) {
      return ((DataResource) resource).getUrl();
    } else {
      return null;
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.uima.resource.ResourceManager#getResourceURL(java.lang.String,
   * java.lang.String[])
   */
  @Override
  public URL getResourceURL(String aKey, String[] aParams) throws ResourceAccessException {
    return getResourceAsStreamCommonUrl(getResource(aKey, aParams));
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.uima.resource.ResourceManager#getResourceURL(java.lang.String)
   */
  @Override
  public URL getResourceURL(String aKey) throws ResourceAccessException {
    return getResourceAsStreamCommonUrl(getResource(aKey));
  }

//@formatter:off
  /*
   * (non-Javadoc)
   * 
   * This method is called during Resource Initialization, 
   *   - only for resources which are "local", that is, instances of ResourceCreationSpecifier
   *   - and therefore might have external resource declarations 
   * 
   * Compare with resolveAndValidateResourceDependencies, called for resource binding resolution.
   * 
   * @see ResourceManager#initializeExternalResources(ResourceManagerConfiguration, String,
   * Map<String, Object>)
   */
//@formatter:on
  @Override
  public synchronized void initializeExternalResources(ResourceManagerConfiguration aConfiguration,
          String aQualifiedContextName, Map<String, Object> aAdditionalParams)
          throws ResourceInitializationException {
    // register resources
    checkDestroyed();

    // set up aAdditionalParams to have this resource manager if not already set
    // so that External Resource instances created from this use this creating/owning
    // resource manager as the value returned from their getResourceManager call
    // see https://issues.apache.org/jira/browse/UIMA-5148
    if (null == aAdditionalParams) {
      aAdditionalParams = new HashMap<>();
    }
    if (!aAdditionalParams.containsKey(Resource.PARAM_RESOURCE_MANAGER)) {
      aAdditionalParams.put(Resource.PARAM_RESOURCE_MANAGER, this);
    }

    // set up aAdditionalParams to have this resource manager if not already set
    // so that External Resource instances created from this use this creating/owning
    // UIMA Context so that getResourceManager works
    // see https://issues.apache.org/jira/browse/UIMA-5153

    ExternalResourceDescription[] resources = aConfiguration.getExternalResources();
    for (int i = 0; i < resources.length; i++) {
      String name = resources[i].getName();

      // check for existing resource registration under this name
      ResourceRegistration registration = mInternalResourceRegistrationMap.get(name);
      if (registration == null) {
        registerResource(name, resources[i], aQualifiedContextName, aAdditionalParams);
      } else {
        // log a message if the resource definitions are not identical
        if (!registration.description.equals(resources[i])) {
          // if the resource was overridden in an enclosing aggregate, use an INFO level message.
          // if not (e.g. sibling annotators declare the same resource name), it's a WARNING.
          if (aQualifiedContextName.startsWith(registration.definingContext)) {
            UIMAFramework.getLogger().logrb(Level.CONFIG, ResourceManager_impl.class.getName(),
                    "initializeExternalResources", LOG_RESOURCE_BUNDLE,
                    "UIMA_overridden_resource__CONFIG",
                    new Object[] { name, aQualifiedContextName, registration.definingContext });
          } else {
            UIMAFramework.getLogger().logrb(Level.WARNING, ResourceManager_impl.class.getName(),
                    "initializeExternalResources", LOG_RESOURCE_BUNDLE,
                    "UIMA_duplicate_resource_name__WARNING",
                    new Object[] { name, registration.definingContext, aQualifiedContextName });
          }
        }
      }
    }
    // apply bindings
    ExternalResourceBinding[] bindings = aConfiguration.getExternalResourceBindings();
    for (int i = 0; i < bindings.length; i++) {
      ResourceRegistration registration = mInternalResourceRegistrationMap
              .get(bindings[i].getResourceName());
      if (registration == null) {
        throw new ResourceInitializationException(
                ResourceInitializationException.UNKNOWN_RESOURCE_NAME,
                new Object[] { bindings[i].getResourceName(), bindings[i].getSourceUrlString() });
      }
      mResourceMap.put(aQualifiedContextName + bindings[i].getKey(), registration.resource);
      // record the link from key to resource class (for parameterized resources only)
      Class<?> impl = mInternalParameterizedResourceImplClassMap.get(bindings[i].getResourceName());
      mParameterizedResourceImplClassMap.put(aQualifiedContextName + bindings[i].getKey(),
              (impl == null) ? EMPTY_RESOURCE_CLASS : impl);
    }
  }

  /*
   * (non-Javadoc)
   * 
   * Called during resource initialization, when the resource has external resource bindings, to
   * resolve those bindings
   * 
   * @see ResourceManager#resolveAndValidateResourceDependencies(ExternalResourceDependency[],
   * String)
   * 
   * Multi-threaded. Partial avoidance of re-resolving, but if a resource fails to resolve, it will
   * be reattempted on every call
   */
  @Override
  public synchronized void resolveAndValidateResourceDependencies(
          ExternalResourceDependency[] aDependencies, String aQualifiedContextName)
          throws ResourceInitializationException {
    checkDestroyed();
    for (int i = 0; i < aDependencies.length; i++) {
      // get resource
      String qname = aQualifiedContextName + aDependencies[i].getKey();
      Object resourceImpl = mResourceMap.get(qname); // may or may not implement Resource, may
                                                     // implement SharedResourceObject

      if (resourceImpl == null) {
        // no resource found
        // try to look up in classpath/datapath
        URL relativeUrl;
        try {
          relativeUrl = new URL("file", "", aDependencies[i].getKey());
        } catch (MalformedURLException e) {
          throw new ResourceInitializationException(e);
        }
        URL absUrl = getRelativePathResolver().resolveRelativePath(relativeUrl);
        if (absUrl != null) {
          // found - create a DataResource object and store it in the mResourceMap
          FileResourceSpecifier spec = new FileResourceSpecifier_impl();
          spec.setFileUrl(absUrl.toString());
          // produces an instance of DataResourceImpl
          resourceImpl = UIMAFramework.produceResource(spec, null);
          mResourceMap.put(qname, resourceImpl);
        }
      }

      if (resourceImpl == null) { // still no resource found - throw exception if required

        if (!aDependencies[i].isOptional()) {
          throw new ResourceInitializationException(
                  ResourceInitializationException.RESOURCE_DEPENDENCY_NOT_SATISFIED, new Object[] {
                      aDependencies[i].getKey(), aDependencies[i].getSourceUrlString() });
        }

      } else { // resource not null

        // make sure resource exists and implements the correct interface
        try {
          String name = aDependencies[i].getInterfaceName();
          if (name != null && name.length() > 0) {
            Class<?> theInterface = loadUserClass(name);

            Class<?> resourceClass = getResourceClass(qname);
            if (!theInterface.isAssignableFrom(resourceClass)) {
              throw new ResourceInitializationException(
                      ResourceInitializationException.RESOURCE_DOES_NOT_IMPLEMENT_INTERFACE,
                      new Object[] { qname, aDependencies[i].getInterfaceName(),
                          aDependencies[i].getSourceUrlString() });
            }
          }
        } catch (ClassNotFoundException e) {
          throw new ResourceInitializationException(ResourceInitializationException.CLASS_NOT_FOUND,
                  new Object[] { aDependencies[i].getInterfaceName(),
                      aDependencies[i].getSourceUrlString() });
        }
      }
    }
  }

  /**
   * Instantiates a resource and inserts it in the internal resource map.
   */
  private void registerResource(String aName, ExternalResourceDescription aResourceDescription,
          String aDefiningContext, Map<String, Object> aResourceInitParams)
          throws ResourceInitializationException {
    // add the relative path resolver to the resource init. params
    Map<String, Object> initParams = (aResourceInitParams == null) ? new HashMap<>()
            : new HashMap<>(aResourceInitParams);
    initParams.put(DataResource.PARAM_RELATIVE_PATH_RESOLVER, getRelativePathResolver());

    // determine if verification mode is on. If so, we don't want to load the resource data
    boolean verificationMode = initParams
            .containsKey(AnalysisEngineImplBase.PARAM_VERIFICATION_MODE);

    // create the initial resource using the resource factory
    Resource r = UIMAFramework.produceResource(aResourceDescription.getResourceSpecifier(),
            initParams);

    // load implementation class (if any) and ensure that it implements
    // SharedResourceObject
    String implementationName = aResourceDescription.getImplementationName();
    Class<?> implClass = null; // might or might not impl Resource
    Object implInstance = r; // what will be registered, might be the Resource, or its
                             // implementation
    if (implementationName != null && implementationName.length() > 0) {
      try {
        implClass = loadUserClass(implementationName);
      } catch (ClassNotFoundException e) {
        throw new ResourceInitializationException(ResourceInitializationException.CLASS_NOT_FOUND,
                new Object[] { implementationName, aResourceDescription.getSourceUrlString() }, e);
      }

      if (!SharedResourceObject.class.isAssignableFrom(implClass)) {
        throw new ResourceInitializationException(
                ResourceInitializationException.NOT_A_SHARED_RESOURCE_OBJECT,
                new Object[] { implementationName, aResourceDescription.getSourceUrlString() });
      }
    }

    // is this a DataResource?
    if (r instanceof DataResource) {
      // instantiate and load the resource object if there is one
      if (implClass != null) {
        try {
          SharedResourceObject sro = (SharedResourceObject) implClass.newInstance();
          if (!verificationMode) {
            sro.load((DataResource) r);
          }
          implInstance = sro; // so the implementation is registered, for DataResources
        } catch (InstantiationException e) {
          throw new ResourceInitializationException(
                  ResourceInitializationException.COULD_NOT_INSTANTIATE,
                  new Object[] { implClass.getName(), aResourceDescription.getSourceUrlString() },
                  e);
        } catch (IllegalAccessException e) {
          throw new ResourceInitializationException(
                  ResourceInitializationException.COULD_NOT_INSTANTIATE,
                  new Object[] { implClass.getName(), aResourceDescription.getSourceUrlString() },
                  e);
        }
      }
    }
    // is it a ParameterizedDataResource?
    else if (r instanceof ParameterizedDataResource) {
      // we can't load the SharedResourceObject now, but we need to remember
      // which class it is for later when we get a request with parameters
      mInternalParameterizedResourceImplClassMap.put(aName,
              (null == implClass) ? EMPTY_RESOURCE_CLASS : implClass);
    } else {
      // it is some other type of Resource
      // it is an error to specify an implementation class in this case
      if (implClass != null) {
        throw new ResourceInitializationException(
                ResourceInitializationException.NOT_A_DATA_RESOURCE,
                new Object[] { implClass.getName(), aName, r.getClass().getName(),
                    aResourceDescription.getSourceUrlString() });
      }
    }

    // put resource or its implementation (for DataResources) in internal map for later retrieval
    ResourceRegistration registration = new ResourceRegistration(implInstance, aResourceDescription,
            aDefiningContext);
    mInternalResourceRegistrationMap.put(aName, registration);
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.uima.resource.ResourceManager#getCasManager()
   */
  @Override
  public CasManager getCasManager() {
    // Optimization for case where mCasManager already created
    // Some sync contention was observed - this makes it less. UIMA-4012
    if (mCasManager != null) {
      return mCasManager;
    }
    synchronized (casManagerMonitor) {
      if (mCasManager == null) {
        mCasManager = new CasManager_impl(this);
      }
      return mCasManager;
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see
   * org.apache.uima.resource.ResourceManager#setCasManager(org.apache.uima.resource.CasManager)
   */
  @Override
  public void setCasManager(CasManager aCasManager) {
    synchronized (casManagerMonitor) {
      if (mCasManager == null) {
        mCasManager = aCasManager;
      } else {
        throw new UIMA_IllegalStateException(UIMA_IllegalStateException.CANNOT_SET_CAS_MANAGER,
                new Object[0]);
      }
    }
  }

  // This method overridden by subclass for pear wrapper
  protected RelativePathResolver getRelativePathResolver() {
    return mRelativePathResolver;
  }

  @Override
  public Map<String, XMLizable> getImportCache() {
    return importCache;
  }

  /**
   * Was used during import resolving until UIMA 3.2.0. Is no longer used since since import
   * resolving has been migrated to the
   * {@code org.apache.uima.resource.metadata.impl.ImportResolver}.
   * 
   * @deprecated No longer used. Scheduled for removal in UIMA 4.0.
   */
  @Deprecated
  public Map<String, Set<String>> getImportUrlsCache() {
    Misc.decreasingWithTrace(IMPORT_URL_CACHE_WARNING_THROTTLE,
            "ResourceManager_impl.getImportUrlsCache() should not be called. It is no longer "
                    + "filled during import resolving. The method will be removed in a future UIMA "
                    + "version.",
            UIMAFramework.getLogger());
    return importUrlsCache;
  }

  @Override
  public <N> Class<N> loadUserClass(String name) throws ClassNotFoundException {
    return Class_TCCL.forName(name, this, true);
  }

  public static Class<?> loadUserClass(String name, ResourceManager rm)
          throws ClassNotFoundException {
    return Class_TCCL.forName(name, rm, true);
  }

  public static Class<?> loadUserClassOrThrow(String name, ResourceManager rm,
          ResourceSpecifier aSpecifier) throws ResourceInitializationException {
    try {
      return Class_TCCL.forName(name, rm, true);
    } catch (ClassNotFoundException e) {
      throw new ResourceInitializationException(ResourceInitializationException.CLASS_NOT_FOUND,
              new Object[] { name, aSpecifier.getSourceUrlString() }, e);
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.uima.resource.ResourceManager#destroy()
   */
  @Override
  public void destroy() {
    boolean alreadyDestroyed = isDestroyed.getAndSet(true);
    if (alreadyDestroyed) {
      return;
    }

    for (ResourceRegistration rr : mInternalResourceRegistrationMap.values()) {
      if (rr.resource instanceof Resource) {
        ((Resource) rr.resource).destroy();
      }
    }

    for (Object r : mParameterizedResourceInstanceMap.values()) {
      if (r instanceof Resource) {
        ((Resource) r).destroy();
      }
    }

    if (uimaCL != null) {
      try {
        uimaCL.close();
      } catch (IOException e) {
        UIMAFramework.getLogger().logrb(Level.WARNING, ResourceManager_impl.class.getName(),
                "destroy", LOG_RESOURCE_BUNDLE, "UIMA_Classloader_close_exception", e);
      }
    }

    // https://issues.apache.org/jira/browse/UIMA-5935
    Map<ResourceManager, Map<PearAnalysisEngineWrapper.StringPair, ResourceManager>> cachedResourceManagers = PearAnalysisEngineWrapper
            .getCachedResourceManagers();
    synchronized (cachedResourceManagers) {
      Map<StringPair, ResourceManager> c1 = cachedResourceManagers.get(this);
      if (c1 != null) {
        for (ResourceManager rm : c1.values()) {
          rm.destroy();
        }
      }
    }

    // not clearing mResourcMap, mInternalResourceRegistrationMap,
    // mParameterizedResourceImplClassMap,
    // mInternalParameterizedResourceImplClassMap, mParameterizedResourceInstanceMap
    // because these could be shared with other resource managers
    // not clearing importCache, importUrlsCache - might be in used by other Resource Managers
    // (shared)

    // no destroy of caspool at this time

  }

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.uima.resource.ResourceManager#getExternalResources()
   */
  @Override
  public List<Object> getExternalResources() {

    List<Object> rs = new ArrayList<>();
    for (ResourceRegistration r : mInternalResourceRegistrationMap.values()) {
      if (!(r instanceof ParameterizedDataResource)) {
        rs.add(r.resource);
      }
    }

    for (Object r : mParameterizedResourceInstanceMap.values()) {
      rs.add(r);
    }

    return rs;
  }

}
