/*
 * 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.util;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.uima.UIMAFramework;
import org.apache.uima.analysis_engine.AnalysisEngineDescription;
import org.apache.uima.analysis_engine.impl.AnalysisEngineImplBase;
import org.apache.uima.analysis_engine.metadata.AnalysisEngineMetaData;
import org.apache.uima.cas.CAS;
import org.apache.uima.cas.CASException;
import org.apache.uima.cas.FSIndex;
import org.apache.uima.cas.Feature;
import org.apache.uima.cas.Type;
import org.apache.uima.cas.TypeSystem;
import org.apache.uima.cas.admin.CASFactory;
import org.apache.uima.cas.admin.CASMgr;
import org.apache.uima.cas.admin.FSIndexComparator;
import org.apache.uima.cas.admin.FSIndexRepositoryMgr;
import org.apache.uima.cas.admin.LinearTypeOrderBuilder;
import org.apache.uima.cas.admin.TypeSystemMgr;
import org.apache.uima.cas.impl.CASImpl;
import org.apache.uima.cas.impl.TypeSystemImpl;
import org.apache.uima.cas_data.CasData;
import org.apache.uima.cas_data.FeatureStructure;
import org.apache.uima.cas_data.PrimitiveValue;
import org.apache.uima.collection.CasConsumerDescription;
import org.apache.uima.collection.CasInitializerDescription;
import org.apache.uima.collection.CollectionReaderDescription;
import org.apache.uima.flow.FlowControllerDescription;
import org.apache.uima.resource.CasDefinition;
import org.apache.uima.resource.Resource;
import org.apache.uima.resource.ResourceInitializationException;
import org.apache.uima.resource.ResourceManager;
import org.apache.uima.resource.ResourceSpecifier;
import org.apache.uima.resource.URISpecifier;
import org.apache.uima.resource.metadata.AllowedValue;
import org.apache.uima.resource.metadata.FeatureDescription;
import org.apache.uima.resource.metadata.FsIndexCollection;
import org.apache.uima.resource.metadata.FsIndexDescription;
import org.apache.uima.resource.metadata.FsIndexKeyDescription;
import org.apache.uima.resource.metadata.MetaDataObject;
import org.apache.uima.resource.metadata.ProcessingResourceMetaData;
import org.apache.uima.resource.metadata.ResourceMetaData;
import org.apache.uima.resource.metadata.TypeDescription;
import org.apache.uima.resource.metadata.TypePriorities;
import org.apache.uima.resource.metadata.TypePriorityList;
import org.apache.uima.resource.metadata.TypeSystemDescription;
import org.apache.uima.resource.metadata.impl.ProcessingResourceMetaData_impl;

/**
 * Utilities for creating and setting up CASes. Also includes utilities for merging CAS type
 * systems.
 */
public class CasCreationUtils {

  private static final AllowedValue[] EMPTY_ALLOWED_VALUE_ARRAY = new AllowedValue[0];

  private static final FeatureDescription[] EMPTY_FEAT_DESC_ARRAY = new FeatureDescription[0];

  /**
   * Creates a new CAS instance.
   * 
   * @return a new CAS instance
   * 
   * @throws ResourceInitializationException
   *           if CAS creation fails
   */
  public static CAS createCas() throws ResourceInitializationException {
    return createCas((TypeSystemDescription) null, null, null);
  }

  /**
   * Creates a new CAS instance. Note this method does not work for Aggregate Analysis Engine
   * descriptors -- use {@link #createCas(AnalysisEngineDescription)} instead.
   * 
   * @param aMetaData
   *          metadata for the analysis engine that will process this CAS. This is used to set up
   *          the CAS's type system and indexes.
   * 
   * @return a new CAS instance
   * 
   * @throws ResourceInitializationException
   *           if CAS creation fails
   */
  public static CAS createCas(AnalysisEngineMetaData aMetaData)
          throws ResourceInitializationException {
    List<AnalysisEngineMetaData> list = new ArrayList<>();
    list.add(aMetaData);
    return createCas(list);
  }

  /**
   * Creates a new CAS instance.
   * 
   * @param aMetaData
   *          metadata for the resource that will process this CAS. This is used to set up the CAS's
   *          type system and indexes.
   * 
   * @return a new CAS instance
   * 
   * @throws ResourceInitializationException
   *           if CAS creation fails
   */
  public static CAS createCas(ProcessingResourceMetaData aMetaData)
          throws ResourceInitializationException {
    List<ProcessingResourceMetaData> list = new ArrayList<>();
    list.add(aMetaData);
    return createCas(list);
  }

  /**
   * Creates a new CAS instance for an Analysis Engine. This works for both primitive and aggregate
   * analysis engines.
   * 
   * @param aDescription
   *          description of the analysis engine that will process this CAS. This is used to set up
   *          the CAS's type system and indexes.
   * 
   * @return a new CAS instance
   * 
   * @throws ResourceInitializationException
   *           if CAS creation fails
   */
  public static CAS createCas(AnalysisEngineDescription aDescription)
          throws ResourceInitializationException {
    return createCas(aDescription, UIMAFramework.getDefaultPerformanceTuningProperties());
  }

  /**
   * Creates a new CAS instance for an Analysis Engine. This works for both primitive and aggregate
   * analysis engines.
   * 
   * @param aDescription
   *          description of the analysis engine that will process this CAS. This is used to set up
   *          the CAS's type system and indexes.
   * @param aPerformanceTuningSettings
   *          Properties object containing framework performance tuning settings using key names
   *          defined on {@link UIMAFramework} interface
   * 
   * @return a new CAS instance
   * 
   * @throws ResourceInitializationException
   *           if CAS creation fails
   */
  public static CAS createCas(AnalysisEngineDescription aDescription,
          Properties aPerformanceTuningSettings) throws ResourceInitializationException {
    List<AnalysisEngineDescription> list = new ArrayList<>();
    list.add(aDescription);
    return createCas(list, aPerformanceTuningSettings);
  }

  /**
   * Creates a new CAS instance for a collection of CAS Processors. This method correctly handles
   * aggregate as well as primitive analysis engines
   * <p>
   * If you pass this method objects of type {@link AnalysisEngineDescription},
   * {@link CollectionReaderDescription}, {@link CasInitializerDescription}, or
   * {@link CasConsumerDescription}, it will not instantiate the components. It will just extract
   * the type system information from the descriptor. For any other kind of
   * {@link ResourceSpecifier}, it will call
   * {@link UIMAFramework#produceResource(org.apache.uima.resource.ResourceSpecifier, Map)}. For
   * example, if a {@link URISpecifier} is passed, a remote connection will be established and the
   * service will be queries for its metadata. An exception will be thrown if the connection can not
   * be opened.
   * 
   * @param aComponentDescriptionsOrMetaData
   *          a collection of {@link ResourceSpecifier}, {@link ProcessingResourceMetaData},
   *          {@link TypeSystemDescription}, {@link FsIndexCollection}, or {@link TypePriorities}
   *          objects.
   * 
   * @return a new CAS instance
   * 
   * @throws ResourceInitializationException
   *           if CAS creation fails
   */
  public static CAS createCas(Collection<? extends MetaDataObject> aComponentDescriptionsOrMetaData)
          throws ResourceInitializationException {
    return createCas(aComponentDescriptionsOrMetaData,
            UIMAFramework.getDefaultPerformanceTuningProperties());
  }

  /**
   * Creates a new CAS instance for a collection of CAS Processors. This method correctly handles
   * aggregate as well as primitive analysis engines
   * <p>
   * If you pass this method objects of type {@link AnalysisEngineDescription},
   * {@link CollectionReaderDescription}, {@link CasInitializerDescription}, or
   * {@link CasConsumerDescription}, it will not instantiate the components. It will just extract
   * the type system information from the descriptor. For any other kind of
   * {@link ResourceSpecifier}, it will call
   * {@link UIMAFramework#produceResource(org.apache.uima.resource.ResourceSpecifier, Map)}. For
   * example, if a {@link URISpecifier} is passed, a remote connection will be established and the
   * service will be queries for its metadata. An exception will be thrown if the connection can not
   * be opened.
   * 
   * @param aComponentDescriptionsOrMetaData
   *          a collection of {@link ResourceSpecifier}, {@link ProcessingResourceMetaData},
   *          {@link TypeSystemDescription}, {@link FsIndexCollection}, or {@link TypePriorities}
   *          objects.
   * @param aPerformanceTuningSettings
   *          Properties object containing framework performance tuning settings using key names
   *          defined on {@link UIMAFramework} interface
   * 
   * @return a new CAS instance
   * 
   * @throws ResourceInitializationException
   *           if CAS creation fails
   */
  public static CAS createCas(Collection<? extends MetaDataObject> aComponentDescriptionsOrMetaData,
          Properties aPerformanceTuningSettings) throws ResourceInitializationException {
    return createCas(aComponentDescriptionsOrMetaData, aPerformanceTuningSettings,
            UIMAFramework.newDefaultResourceManager());
  }

  /**
   * Creates a new CAS instance for a collection of CAS Processors. This method correctly handles
   * aggregate as well as primitive analysis engines
   * <p>
   * If you pass this method objects of type {@link AnalysisEngineDescription},
   * {@link CollectionReaderDescription}, {@link CasInitializerDescription}, or
   * {@link CasConsumerDescription}, it will not instantiate the components. It will just extract
   * the type system information from the descriptor. For any other kind of
   * {@link ResourceSpecifier}, it will call
   * {@link UIMAFramework#produceResource(org.apache.uima.resource.ResourceSpecifier, Map)}. For
   * example, if a {@link URISpecifier} is passed, a remote connection will be established and the
   * service will be queries for its metadata. An exception will be thrown if the connection can not
   * be opened.
   * 
   * @param aComponentDescriptionsOrMetaData
   *          a collection of {@link ResourceSpecifier}, {@link ProcessingResourceMetaData},
   *          {@link TypeSystemDescription}, {@link FsIndexCollection}, or {@link TypePriorities}
   *          objects.
   * @param aPerformanceTuningSettings
   *          Properties object containing framework performance tuning settings using key names
   *          defined on {@link UIMAFramework} interface
   * @param aResourceManager
   *          the resource manager to use to resolve import declarations within the metadata
   * 
   * @return a new CAS instance
   * 
   * @throws ResourceInitializationException
   *           if CAS creation fails
   */
  public static CAS createCas(Collection<? extends MetaDataObject> aComponentDescriptionsOrMetaData,
          Properties aPerformanceTuningSettings, ResourceManager aResourceManager)
          throws ResourceInitializationException {
    // build a list of metadata objects
    List<ProcessingResourceMetaData> mdList = getMetaDataList(aComponentDescriptionsOrMetaData,
            aResourceManager);

    // extract TypeSystems, TypePriorities, and FsIndexes from metadata
    List<TypeSystemDescription> typeSystems = new ArrayList<>();
    List<TypePriorities> typePriorities = new ArrayList<>();
    List<FsIndexCollection> fsIndexes = new ArrayList<>();
    Iterator<ProcessingResourceMetaData> it = mdList.iterator();
    while (it.hasNext()) {
      ProcessingResourceMetaData md = it.next();
      if (md.getTypeSystem() != null)
        typeSystems.add(md.getTypeSystem());
      if (md.getTypePriorities() != null)
        typePriorities.add(md.getTypePriorities());
      if (md.getFsIndexCollection() != null)
        fsIndexes.add(md.getFsIndexCollection());
    }

    // merge
    TypeSystemDescription aggTypeDesc = mergeTypeSystems(typeSystems, aResourceManager);
    TypePriorities aggTypePriorities = mergeTypePriorities(typePriorities, aResourceManager);
    FsIndexCollection aggIndexColl = mergeFsIndexes(fsIndexes, aResourceManager);

    return doCreateCas(null, aggTypeDesc, aggTypePriorities, aggIndexColl.getFsIndexes(),
            aPerformanceTuningSettings, aResourceManager);
  }

  /**
   * Creates a new CAS instance.
   * 
   * @param aTypeSystem
   *          type system to install in the CAS
   * @param aTypePriorities
   *          type priorities to install in the CAS
   * @param aFsIndexes
   *          indexes to install in the CAS
   * 
   * @return a new CAS instance
   * 
   * @throws ResourceInitializationException
   *           if CAS creation fails
   */
  public static CAS createCas(TypeSystemDescription aTypeSystem, TypePriorities aTypePriorities,
          FsIndexDescription[] aFsIndexes) throws ResourceInitializationException {
    return createCas(aTypeSystem, aTypePriorities, aFsIndexes, null, null);
  }

  /**
   * Creates a new CAS instance.
   * 
   * @param aTypeSystem
   *          type system to install in the CAS
   * @param aTypePriorities
   *          type priorities to install in the CAS
   * @param aFsIndexes
   *          indexes to install in the CAS
   * @param aPerformanceTuningSettings
   *          Properties object containing framework performance tuning settings using key names
   *          defined on {@link UIMAFramework} interface
   * 
   * @return a new CAS instance
   * 
   * @throws ResourceInitializationException
   *           if CAS creation fails
   */
  public static CAS createCas(TypeSystemDescription aTypeSystem, TypePriorities aTypePriorities,
          FsIndexDescription[] aFsIndexes, Properties aPerformanceTuningSettings)
          throws ResourceInitializationException {
    return createCas(aTypeSystem, aTypePriorities, aFsIndexes, aPerformanceTuningSettings, null);
  }

  /**
   * Creates a new CAS instance.
   * 
   * @param aTypeSystem
   *          type system to install in the CAS
   * @param aTypePriorities
   *          type priorities to install in the CAS
   * @param aFsIndexes
   *          indexes to install in the CAS
   * @param aPerformanceTuningSettings
   *          Properties object containing framework performance tuning settings using key names
   *          defined on {@link UIMAFramework} interface
   * @param aResourceManager
   *          the resource manager
   * @return a new CAS instance
   * 
   * @throws ResourceInitializationException
   *           if CAS creation fails
   */
  public static CAS createCas(TypeSystemDescription aTypeSystem, TypePriorities aTypePriorities,
          FsIndexDescription[] aFsIndexes, Properties aPerformanceTuningSettings,
          ResourceManager aResourceManager) throws ResourceInitializationException {
    return doCreateCas(null, aTypeSystem, aTypePriorities, aFsIndexes, aPerformanceTuningSettings,
            aResourceManager);
  }

  /**
   * Creates a new CAS instance for a collection of CAS Processors, which. reuses an existing type
   * system. Using this method allows several CASes to all share the exact same type system object.
   * This method correctly handles aggregate as well as primitive analysis engines.
   * <p>
   * If you pass this method objects of type {@link AnalysisEngineDescription},
   * {@link CollectionReaderDescription}, {@link CasInitializerDescription}, or
   * {@link CasConsumerDescription}, it will not instantiate the components. It will just extract
   * the type system information from the descriptor. For any other kind of
   * {@link ResourceSpecifier}, it will call
   * {@link UIMAFramework#produceResource(org.apache.uima.resource.ResourceSpecifier, Map)}. For
   * example, if a {@link URISpecifier} is passed, a remote connection will be established and the
   * service will be queries for its metadata. An exception will be thrown if the connection can not
   * be opened.
   * 
   * @param aComponentDescriptionsOrMetaData
   *          a collection of {@link ResourceSpecifier}, {@link ProcessingResourceMetaData},
   *          {@link TypeSystemDescription}, {@link FsIndexCollection}, or {@link TypePriorities}
   *          objects.
   * @param aTypeSystem
   *          type system to install in the CAS, null if none
   * @param aPerformanceTuningSettings
   *          Properties object containing framework performance tuning settings using key names
   *          defined on {@link UIMAFramework} interface
   * 
   * @return a new CAS instance
   * 
   * @throws ResourceInitializationException
   *           if CAS creation fails
   */
  public static CAS createCas(Collection<? extends MetaDataObject> aComponentDescriptionsOrMetaData,
          TypeSystem aTypeSystem, Properties aPerformanceTuningSettings)
          throws ResourceInitializationException {
    return createCas(aComponentDescriptionsOrMetaData, aTypeSystem, aPerformanceTuningSettings,
            UIMAFramework.newDefaultResourceManager());
  }

  /**
   * Creates a new CAS instance for a collection of CAS Processors, which. reuses an existing type
   * system. Using this method allows several CASes to all share the exact same type system object.
   * This method correctly handles aggregate as well as primitive analysis engines.
   * <p>
   * If you pass this method objects of type {@link AnalysisEngineDescription},
   * {@link CollectionReaderDescription}, {@link CasInitializerDescription}, or
   * {@link CasConsumerDescription}, it will not instantiate the components. It will just extract
   * the type system information from the descriptor. For any other kind of
   * {@link ResourceSpecifier}, it will call
   * {@link UIMAFramework#produceResource(org.apache.uima.resource.ResourceSpecifier, Map)}. For
   * example, if a {@link URISpecifier} is passed, a remote connection will be established and the
   * service will be queries for its metadata. An exception will be thrown if the connection can not
   * be opened.
   * 
   * @param aComponentDescriptionsOrMetaData
   *          a collection of {@link ResourceSpecifier}, {@link ProcessingResourceMetaData},
   *          {@link TypeSystemDescription}, {@link FsIndexCollection}, or {@link TypePriorities}
   *          objects.
   * @param aTypeSystem
   *          type system to install in the CAS, null if none
   * @param aPerformanceTuningSettings
   *          Properties object containing framework performance tuning settings using key names
   *          defined on {@link UIMAFramework} interface
   * @param aResourceManager
   *          the resource manager to use to resolve import declarations within the metadata, and
   *          also to set the JCas ClassLoader for the new CAS
   * 
   * @return a new CAS instance
   * 
   * @throws ResourceInitializationException
   *           if CAS creation fails
   */
  public static CAS createCas(Collection<? extends MetaDataObject> aComponentDescriptionsOrMetaData,
          TypeSystem aTypeSystem, Properties aPerformanceTuningSettings,
          ResourceManager aResourceManager) throws ResourceInitializationException {
    // build a list of metadata objects
    List<ProcessingResourceMetaData> mdList = getMetaDataList(aComponentDescriptionsOrMetaData,
            aResourceManager);

    // extract TypeSystems, TypePriorities, and FsIndexes from metadata
    List<TypeSystemDescription> typeSystems = new ArrayList<>();
    List<TypePriorities> typePriorities = new ArrayList<>();
    List<FsIndexCollection> fsIndexes = new ArrayList<>();
    Iterator<ProcessingResourceMetaData> it = mdList.iterator();
    while (it.hasNext()) {
      ProcessingResourceMetaData md = it.next();
      if (md.getTypeSystem() != null)
        typeSystems.add(md.getTypeSystem());
      if (md.getTypePriorities() != null)
        typePriorities.add(md.getTypePriorities());
      if (md.getFsIndexCollection() != null)
        fsIndexes.add(md.getFsIndexCollection());
    }

    // merge TypePriorities and FsIndexes
    TypePriorities aggTypePriorities = mergeTypePriorities(typePriorities, aResourceManager);
    FsIndexCollection aggIndexColl = mergeFsIndexes(fsIndexes, aResourceManager);

    if (aTypeSystem != null) // existing type system object was specified; use that
    {
      return doCreateCas(aTypeSystem, null, aggTypePriorities, aggIndexColl.getFsIndexes(),
              aPerformanceTuningSettings, aResourceManager);
    } else {
      // no type system object specified; merge type system descriptions in metadata
      TypeSystemDescription aggTypeDesc = mergeTypeSystems(typeSystems);
      return doCreateCas(null, aggTypeDesc, aggTypePriorities, aggIndexColl.getFsIndexes(),
              aPerformanceTuningSettings, aResourceManager);
    }
  }

  /**
   * Creates a new CAS instance that reuses an existing type system. Using this method allows
   * several CASes to all share the exact same type system object.
   * 
   * @param aTypeSystem
   *          type system to install in the CAS
   * @param aTypePriorities
   *          type priorities to install in the CAS
   * @param aFsIndexes
   *          indexes to install in the CAS
   * @param aPerformanceTuningSettings
   *          the settings for performance tuning
   * @return a new CAS instance
   * 
   * @throws ResourceInitializationException
   *           if CAS creation fails
   */
  public static CAS createCas(TypeSystem aTypeSystem, TypePriorities aTypePriorities,
          FsIndexDescription[] aFsIndexes, Properties aPerformanceTuningSettings)
          throws ResourceInitializationException {
    return createCas(aTypeSystem, aTypePriorities, aFsIndexes, aPerformanceTuningSettings, null);
  }

  /**
   * Creates a new CAS instance that reuses an existing type system. Using this method allows
   * several CASes to all share the exact same type system object.
   * 
   * @param aTypeSystem
   *          type system to install in the CAS
   * @param aTypePriorities
   *          type priorities to install in the CAS
   * @param aFsIndexes
   *          indexes to install in the CAS
   * @param aPerformanceTuningSettings
   *          the settings for performance tuning
   * @param aResourceManager
   *          resource manager, which is used to set the JCas ClassLoader for the new CAS
   * 
   * @return a new CAS instance
   * 
   * @throws ResourceInitializationException
   *           if CAS creation fails
   */
  public static CAS createCas(TypeSystem aTypeSystem, TypePriorities aTypePriorities,
          FsIndexDescription[] aFsIndexes, Properties aPerformanceTuningSettings,
          ResourceManager aResourceManager) throws ResourceInitializationException {
    return doCreateCas(aTypeSystem, null, aTypePriorities, aFsIndexes, aPerformanceTuningSettings,
            aResourceManager);
  }

  /**
   * Method that does the work for creating a new CAS instance. Other createCas methods in this
   * class should all eventually call this method, so that the critical code is not duplicated in
   * more than one place.
   * 
   * @param aTypeSystem
   *          an existing type system to reuse in this CAS, null if none.
   * @param aTypeSystemDescription
   *          description of type system to use for this CAS. This is only used if aTypeSystem is
   *          null.
   * @param aTypePriorities
   *          type priorities to install in the CAS
   * @param aFsIndexes
   *          indexes to install in the CAS
   * @param aPerformanceTuningSettings
   *          Properties object containing framework performance tuning settings using key names
   *          defined on {@link UIMAFramework} interface
   * 
   * @return a new CAS instance
   * 
   * @throws ResourceInitializationException
   *           if CAS creation fails
   */
  private static CAS doCreateCas(TypeSystem aTypeSystem, TypeSystemDescription aTypeSystemDesc,
          TypePriorities aTypePriorities, FsIndexDescription[] aFsIndexes,
          Properties aPerformanceTuningSettings, ResourceManager aResourceManager)
          throws ResourceInitializationException {
    if (aResourceManager == null) {
      aResourceManager = UIMAFramework.newDefaultResourceManager();
    }

    // resolve imports
    try {
      if (aTypeSystemDesc != null) {
        aTypeSystemDesc.resolveImports(aResourceManager);
        // even though there's only one Type System, we still need to do a merge, to handle the
        // case where this TypeSystem defines the same type more than once (or has imports that do)
        List<TypeSystemDescription> tsList = new ArrayList<>();
        tsList.add(aTypeSystemDesc);
        aTypeSystemDesc = mergeTypeSystems(tsList, aResourceManager, null);
      }
      if (aTypePriorities != null) {
        aTypePriorities.resolveImports(aResourceManager);
      }
    } catch (InvalidXMLException e) {
      throw new ResourceInitializationException(e);
    }

    // get initial heap size
    String initialHeapSizeStr = null;
    if (aPerformanceTuningSettings != null) {
      initialHeapSizeStr = aPerformanceTuningSettings
              .getProperty(UIMAFramework.CAS_INITIAL_HEAP_SIZE);
    }

    int initialHeapSize = (null == initialHeapSizeStr) ? CASImpl.DEFAULT_INITIAL_HEAP_SIZE
            : Integer.parseInt(initialHeapSizeStr);

    // Check Jcas cache performance setting. Defaults to true.
    // in v3, JCasCache is ignored
    boolean useJcasCache = true;
    if (aPerformanceTuningSettings != null) {
      String useJcasCacheString = aPerformanceTuningSettings
              .getProperty(UIMAFramework.JCAS_CACHE_ENABLED, "true");
      if ("false".equalsIgnoreCase(useJcasCacheString)) {
        useJcasCache = false;
      }
    }

    // create CAS using either aTypeSystem or aTypeSystemDesc
    CASMgr casMgr;
    if (aTypeSystem != null) {
      casMgr = CASFactory.createCAS(initialHeapSize, aTypeSystem, useJcasCache);

      // Set JCas ClassLoader - before setupTypeSystem
      if (aResourceManager.getExtensionClassLoader() != null) {
        casMgr.setJCasClassLoader(aResourceManager.getExtensionClassLoader());
      }

    } else // no TypeSystem to reuse - create a new one
    {
      boolean skip_loading_user_jcas = false;
      if (aPerformanceTuningSettings != null) {
        String v = aPerformanceTuningSettings.getProperty(UIMAFramework.SKIP_USER_JCAS_LOADING,
                "false");
        skip_loading_user_jcas = "true".equalsIgnoreCase(v);
      }

      casMgr = CASFactory.createCAS();

      if (aResourceManager.getExtensionClassLoader() != null) {
        casMgr.setJCasClassLoader(aResourceManager.getExtensionClassLoader());
      }

      // install type system
      setupTypeSystem(casMgr, aTypeSystemDesc);
      // Commit the type system
      ((CASImpl) casMgr).commitTypeSystem(skip_loading_user_jcas);
    }

    try {
      // install TypePriorities into CAS
      setupTypePriorities(casMgr, aTypePriorities);

      // install Built-in indexes into CAS
      casMgr.initCASIndexes();
    } catch (CASException e) {
      throw new ResourceInitializationException(e);
    }

    // install AnalysisEngine's custom indexes into CAS
    setupIndexes(casMgr, aFsIndexes);

    // Commit the index repository
    casMgr.getIndexRepositoryMgr().commit();

    return casMgr.getCAS().getView(CAS.NAME_DEFAULT_SOFA);
  }

  /**
   * Create a CAS from a CAS Definition.
   * 
   * In V3, creating the type system is expensive (due to loading and setting up of JCas classes),
   * so we do the type system creation once per CasDefinition and store it with the CAS definition
   * 
   * @param casDef
   *          completely describes the CAS to be created
   * @param performanceTuningSettings
   *          Properties object containing framework performance tuning settings using key names
   *          defined on {@link UIMAFramework} interface
   * 
   * @return a new CAS matching the given CasDefinition
   * @throws ResourceInitializationException
   *           if CAS creation fails
   * 
   */
  public static CAS createCas(CasDefinition casDef, Properties performanceTuningSettings)
          throws ResourceInitializationException {
    TypeSystemImpl tsi = casDef.getTypeSystemImpl();
    CAS cas;
    if (null == tsi) {
      synchronized (casDef) {
        if (null == tsi) { // retest under sync lock
          cas = createCas(casDef.getTypeSystemDescription(), casDef.getTypePriorities(),
                  casDef.getFsIndexDescriptions(), performanceTuningSettings,
                  casDef.getResourceManager());
          casDef.setTypeSystemImpl((TypeSystemImpl) cas.getTypeSystem());
          return cas;
        }
      }
    }

    return doCreateCas(tsi, casDef.getTypeSystemDescription(), casDef.getTypePriorities(),
            casDef.getFsIndexDescriptions(), performanceTuningSettings,
            casDef.getResourceManager());
  }

  /**
   * Create a CAS from a CAS Definition, but reuse the provided TypeSystem object.
   * 
   * @param casDef
   *          completely describes the CAS to be created
   * @param performanceTuningSettings
   *          Properties object containing framework performance tuning settings using key names
   *          defined on {@link UIMAFramework} interface
   * @param typeSystem
   *          type system object to reuse
   * 
   * @return a new CAS matching the given CasDefinition
   * @throws ResourceInitializationException
   *           if CAS creation fails
   */
  public static CAS createCas(CasDefinition casDef, Properties performanceTuningSettings,
          TypeSystem typeSystem) throws ResourceInitializationException {
    return createCas(typeSystem, casDef.getTypePriorities(), casDef.getFsIndexDescriptions(),
            performanceTuningSettings, casDef.getResourceManager());
  }

  /**
   * Installs a TypeSystem in a CAS.
   * 
   * @param aCASMgr
   *          the <code>CASMgr</code> object whose type system is to be modified.
   * @param aTypeSystem
   *          description of type system to install
   * 
   * @throws ResourceInitializationException
   *           if an error occurs during modification of the type system
   */
  public static void setupTypeSystem(CASMgr aCASMgr, TypeSystemDescription aTypeSystem)
          throws ResourceInitializationException {
    TypeSystemMgr typeSystemMgr = aCASMgr.getTypeSystemMgr();
    if (aTypeSystem != null) {
      TypeDescription[] types = aTypeSystem.getTypes();
      if (types != null) {
        // add all Types first (so that we can handle forward references) - note
        // that it isn't guaranteed that a supertype will occur in the Types list
        // before its subtype.

        // Build a linked list of type descriptions. We will make multiple passes
        // over this, adding types to the CAS and removing them from the linked
        // list. We continue until the list is empty or we cannot make any
        // progress.
        LinkedList<TypeDescription> typeList = new LinkedList<>();
        typeList.addAll(Arrays.asList(types));
        int numTypes = typeList.size();
        int lastNumTypes;
        List<TypeDescription> typesInOrderOfCreation = new LinkedList<>();
        do {
          lastNumTypes = numTypes;
          Iterator<TypeDescription> it = typeList.iterator();
          while (it.hasNext()) {
            TypeDescription curTypeDesc = it.next();
            String typeName = curTypeDesc.getName();
            // type does not exist - add it under the appropriate supertype
            String superTypeName = curTypeDesc.getSupertypeName();
            if (superTypeName == null) {
              throw new ResourceInitializationException(
                      ResourceInitializationException.NO_SUPERTYPE,
                      new Object[] { typeName, curTypeDesc.getSourceUrlString() });
            }
            // Check if it's a built-in type: must not change supertype!
            Type builtIn = typeSystemMgr.getType(typeName);
            if (builtIn != null) {
              if (!superTypeName.equals(typeSystemMgr.getParent(builtIn).getName())) {
                throw new ResourceInitializationException(
                        ResourceInitializationException.REDEFINING_BUILTIN_TYPE,
                        new Object[] { typeSystemMgr.getParent(builtIn), typeName, superTypeName,
                            curTypeDesc.getSourceUrlString() });
              }
            }
            Type supertype = typeSystemMgr.getType(superTypeName);
            if (supertype != null) {
              // supertype is defined, so add to CAS type system
              // check for special "enumerated types" that extend String
              if (curTypeDesc.getSupertypeName().equals(CAS.TYPE_NAME_STRING)) {
                AllowedValue[] vals = curTypeDesc.getAllowedValues();
                if (vals == null) {
                  throw new ResourceInitializationException(
                          ResourceInitializationException.MISSING_ALLOWED_VALUES,
                          new Object[] { typeName, curTypeDesc.getSourceUrlString() });
                }
                String[] valStrs = new String[vals.length];
                for (int i = 0; i < valStrs.length; i++) {
                  valStrs[i] = vals[i].getString();
                }
                typeSystemMgr.addStringSubtype(typeName, valStrs);
              } else // a "normal" type
              {
                // make sure that allowed values are NOT specified for non-string subtypes
                if (curTypeDesc.getAllowedValues() != null
                        && curTypeDesc.getAllowedValues().length > 0) {
                  throw new ResourceInitializationException(
                          ResourceInitializationException.ALLOWED_VALUES_ON_NON_STRING_TYPE,
                          new Object[] { typeName, curTypeDesc.getSourceUrlString() });
                }
                typeSystemMgr.addType(typeName, supertype);
              }
              // remove from list of type descriptions and add it to the typesInOrderOfCreation list
              // for later processing
              it.remove();
              typesInOrderOfCreation.add(curTypeDesc);
            }
          }
          numTypes = typeList.size();
        } while (numTypes > 0 && numTypes != lastNumTypes);
        // we quit the above loop either when we've added all types or when
        // we went through the entire list without successfully finding any
        // supertypes. In the latter case, throw an exception. Since there
        // can be more than one such type, we look for one that does not have
        // ancestor in the list as it is the likely cause of the issue. The
        // implementation of this is not as efficient as it could be but avoids
        // issues with cyclic definitions.
        for (int i = 0; i < typeList.size(); i++) {
          TypeDescription td_i = typeList.get(i);
          boolean foundSuperType = false;
          for (int j = 0; j < typeList.size(); j++) {
            if (i == j) {
              continue;
            }
            TypeDescription td_j = typeList.get(j);
            if (td_j.getName().equals(td_i.getSupertypeName())) {
              foundSuperType = true;
              break;
            }
          }
          if (!foundSuperType) {
            throw new ResourceInitializationException(
                    ResourceInitializationException.UNDEFINED_SUPERTYPE, new Object[] {
                        td_i.getSupertypeName(), td_i.getName(), td_i.getSourceUrlString() });
          }
        }

        if (numTypes > 0) {
          // We get here in either of two cases: there was only one problematic
          // type definition, or there was a cycle.
          TypeDescription firstFailed = typeList.getFirst();
          throw new ResourceInitializationException(
                  ResourceInitializationException.UNDEFINED_SUPERTYPE,
                  new Object[] { firstFailed.getSupertypeName(), firstFailed.getName(),
                      firstFailed.getSourceUrlString() });
        }

        // now for each type, add its features. We add features to supertypes before subtypes. This
        // is done so that
        // if we have a duplicate feature name on both a supertype and a subtype, it is added to the
        // supertype and then
        // ignored when we get to the subtype. Although this is a dubious type system, we support it
        // for backwards
        // compatibility (but we might want to think about generating a warning).
        Iterator<TypeDescription> typeIter = typesInOrderOfCreation.iterator();
        while (typeIter.hasNext()) {
          TypeDescription typeDesc = typeIter.next();
          Type type = typeSystemMgr.getType(typeDesc.getName());
          // assert type != null;

          FeatureDescription[] features = typeDesc.getFeatures();
          if (features != null) {
            for (int j = 0; j < features.length; j++) {
              String featName = features[j].getName();
              String rangeTypeName = features[j].getRangeTypeName();
              Type rangeType = typeSystemMgr.getType(rangeTypeName);
              if (rangeType == null) {
                throw new ResourceInitializationException(
                        ResourceInitializationException.UNDEFINED_RANGE_TYPE,
                        new Object[] { rangeTypeName, featName, typeDesc.getName(),
                            features[j].getSourceUrlString() });
              }
              if (rangeType.isArray()) // TODO: also List?
              {
                // if an element type is specified, get the specific
                // array subtype for that element type
                String elementTypeName = features[j].getElementType();
                if (elementTypeName != null && elementTypeName.length() > 0) {
                  Type elementType = typeSystemMgr.getType(elementTypeName);
                  if (elementType == null) {
                    throw new ResourceInitializationException(
                            ResourceInitializationException.UNDEFINED_RANGE_TYPE,
                            new Object[] { elementTypeName, featName, typeDesc.getName(),
                                features[j].getSourceUrlString() });
                  }
                  rangeType = typeSystemMgr.getArrayType(elementType);
                }
              }
              Boolean multiRefAllowed = features[j].getMultipleReferencesAllowed();
              if (multiRefAllowed == null) {
                multiRefAllowed = Boolean.FALSE; // default to false if unspecified
              }
              typeSystemMgr.addFeature(featName, type, rangeType, multiRefAllowed);
            }
          }
        }
      }
    }
  }

  /**
   * Adds TypePriorities to a CAS.
   * 
   * @param aCASMgr
   *          the <code>CASMgr</code> object to be modified
   * @param aTypePriorities
   *          description of the type priorities to add
   * 
   * @throws ResourceInitializationException
   *           if an error occurs during type priority setup
   */
  public static void setupTypePriorities(CASMgr aCASMgr, TypePriorities aTypePriorities)
          throws ResourceInitializationException {
    if (aTypePriorities != null) {
      LinearTypeOrderBuilder typeOrderBuilder = aCASMgr.getIndexRepositoryMgr()
              .getDefaultOrderBuilder();
      TypePriorityList[] priorityLists = aTypePriorities.getPriorityLists();
      for (int i = 0; i < priorityLists.length; i++) {
        // check that all types exist. This error would be caught in
        // typeOrderBuilder.getOrder(), but that's too late to indicate
        // the location of the faulty descriptor in the error message.
        String[] typeList = priorityLists[i].getTypes();
        for (int j = 0; j < typeList.length; j++) {
          if (aCASMgr.getTypeSystemMgr().getType(typeList[j]) == null) {
            throw new ResourceInitializationException(
                    ResourceInitializationException.UNDEFINED_TYPE_FOR_PRIORITY_LIST,
                    new Object[] { typeList[j], priorityLists[i].getSourceUrlString() });
          }
        }
        try {
          typeOrderBuilder.add(priorityLists[i].getTypes());
        } catch (CASException e) {
          // typically caused by a cycle in the priorities - the caused-by message
          // will clarify.
          throw new ResourceInitializationException(
                  ResourceInitializationException.INVALID_TYPE_PRIORITIES,
                  new Object[] { priorityLists[i].getSourceUrlString() }, e);
        }
      }
    }
  }

  /**
   * Adds FeatureStructure indexes to a CAS.
   * 
   * @param aCASMgr
   *          the <code>CASMgr</code> object to be modified
   * @param aIndexes
   *          descriptions of the indexes to add
   * 
   * @throws ResourceInitializationException
   *           if an error occurs during index creation
   */
  public static void setupIndexes(CASMgr aCASMgr, FsIndexDescription[] aIndexes)
          throws ResourceInitializationException {
    if (aIndexes != null) {
      TypeSystemMgr tsm = aCASMgr.getTypeSystemMgr();
      FSIndexRepositoryMgr irm = aCASMgr.getIndexRepositoryMgr();

      for (int i = 0; i < aIndexes.length; i++) {
        int kind = FSIndex.SORTED_INDEX;
        String kindStr = aIndexes[i].getKind();
        if (kindStr != null) {
          if (kindStr.equals(FsIndexDescription.KIND_BAG))
            kind = FSIndex.BAG_INDEX;
          else if (kindStr.equals(FsIndexDescription.KIND_SET))
            kind = FSIndex.SET_INDEX;
          else if (kindStr.equals(FsIndexDescription.KIND_SORTED))
            kind = FSIndex.SORTED_INDEX;
        }

        Type type = tsm.getType(aIndexes[i].getTypeName());
        if (type == null) {
          throw new ResourceInitializationException(
                  ResourceInitializationException.UNDEFINED_TYPE_FOR_INDEX,
                  new Object[] { aIndexes[i].getTypeName(), aIndexes[i].getLabel(),
                      aIndexes[i].getSourceUrlString() });
        }
        FSIndexComparator comparator = irm.createComparator();
        comparator.setType(type);

        FsIndexKeyDescription[] keys = aIndexes[i].getKeys();
        if (keys != null) {
          for (int j = 0; j < keys.length; j++) {
            if (keys[j].isTypePriority()) {
              comparator.addKey(irm.getDefaultTypeOrder(), FSIndexComparator.STANDARD_COMPARE);
            } else {
              Feature feature = type.getFeatureByBaseName(keys[j].getFeatureName());
              if (feature == null) {
                throw new ResourceInitializationException(
                        ResourceInitializationException.INDEX_KEY_FEATURE_NOT_FOUND,
                        new Object[] { keys[j].getFeatureName(), aIndexes[i].getLabel(),
                            aIndexes[i].getSourceUrlString() });
              }
              comparator.addKey(feature, keys[j].getComparator());
            }
          }
        }

        irm.createIndex(comparator, aIndexes[i].getLabel(), kind);
      }
    }
  }

  /**
   * Extracts a TypeSystem definition from a CasData.
   * 
   * @param aCasData
   *          the CAS Data from which to extract the type system
   * 
   * @return a description of a TypeSystem to which the CAS Data conforms
   */
  public static TypeSystemDescription convertData2TypeSystem(CasData aCasData) {
    TypeSystemDescription result = UIMAFramework.getResourceSpecifierFactory()
            .createTypeSystemDescription();
    Iterator<FeatureStructure> iter = aCasData.getFeatureStructures();
    List<TypeDescription> typesArr = new ArrayList<>();
    while (iter.hasNext()) {
      FeatureStructure casFS = iter.next();
      TypeDescription newType = UIMAFramework.getResourceSpecifierFactory().createTypeDescription();
      newType.setName(casFS.getType());
      newType.setSupertypeName("uima.tcas.annotation");
      newType.setDescription("CasData Type");
      String features[] = casFS.getFeatureNames();
      if (features != null) {
        for (int i = 0; i < features.length; i++) {
          String featName = features[i];
          String rangeName = "";
          String description = "";
          PrimitiveValue pVal = (PrimitiveValue) casFS.getFeatureValue(featName);
          if (pVal.get().getClass().getName().equals("java.lang.String")) {
            System.out.println(" the feature is a String ");
            rangeName = "uima.cas.String";
            description = " featue of the casDataType";
          }
          newType.addFeature(featName, description, rangeName);
        }
      }
      typesArr.add(newType);
    }
    TypeDescription td[] = new TypeDescription[typesArr.size()];
    for (int j = 0; j < typesArr.size(); j++) {
      td[j] = typesArr.get(j);
    }
    result.setTypes(td);
    return result;
  }

  /**
   * Merges several TypeSystemDescriptions into one. Also resolves imports in the
   * TypeSystemDescription objects.
   * 
   * @param aTypeSystems
   *          a collection of TypeSystems to be merged
   * 
   * @return a new TypeSystemDescription that is the result of merging all of the type systems
   *         together
   * 
   * @throws ResourceInitializationException
   *           if an incompatibility exists or if an import could not be resolved
   */
  public static TypeSystemDescription mergeTypeSystems(
          Collection<? extends TypeSystemDescription> aTypeSystems)
          throws ResourceInitializationException {
    return mergeTypeSystems(aTypeSystems, UIMAFramework.newDefaultResourceManager());
  }

  /**
   * Merges several TypeSystemDescriptions into one. Also resolves imports in the
   * TypeSystemDescription objects.
   * 
   * @param aTypeSystems
   *          a collection of TypeSystems to be merged
   * @param aResourceManager
   *          Resource Manager to use to locate type systems imported by name
   * 
   * @return a new TypeSystemDescription that is the result of merging all of the type systems
   *         together
   * 
   * @throws ResourceInitializationException
   *           if an incompatibility exists or if an import could not be resolved
   */
  public static TypeSystemDescription mergeTypeSystems(
          Collection<? extends TypeSystemDescription> aTypeSystems,
          ResourceManager aResourceManager) throws ResourceInitializationException {
    return mergeTypeSystems(aTypeSystems, aResourceManager, null);
  }

  /**
   * Merges several TypeSystemDescriptions into one. Also resolves imports in the
   * TypeSystemDescription objects.
   * <p>
   * This version of this method takes an argument <code>aOutputMergedTypes</code>, which this
   * method will populate with the names and descriptor locations of any types whose definitions
   * have been merged from multiple non-identical sources. That is, types that are declared more
   * than once, with different (but compatible) sets of features in each declaration, or with
   * different (but compatible) supertypes.
   * 
   * @param aTypeSystems
   *          a collection of TypeSystems to be merged
   * @param aResourceManager
   *          Resource Manager to use to locate type systems imported by name
   * @param aOutputMergedTypes
   *          A Map that this method will populate with information about the set of types whose
   *          definitions were merged from multiple non-identical sources. The keys in the Map will
   *          be the type names (Strings) and the values will be {link Set}s containing Descriptor
   *          URLs (Strings) where those types are declared. You may pass null if you are not
   *          interested in this information.
   * 
   * @return a new TypeSystemDescription that is the result of merging all of the type systems
   *         together
   * 
   * @throws ResourceInitializationException
   *           if an incompatibility exists or if an import could not be resolved
   */
  public static TypeSystemDescription mergeTypeSystems(
          Collection<? extends TypeSystemDescription> aTypeSystems,
          ResourceManager aResourceManager, Map<String, Set<String>> aOutputMergedTypes)
          throws ResourceInitializationException {
    // also build a Map from Type names to Types. Use a TreeMap so we get a consistent ordering of
    // types.
    Map<String, TypeDescription> typeNameMap = new TreeMap<>();

    // Iterate through all type systems and add types to the merged TypeSystem.
    // If a type is defined more than once, we need to check if the superType
    // declarations are compatible (one inherits from another), and merge the
    // features.

    // In order to properly handle the supertype merging, we need to make sure
    // that we process the supertype definitions before the subtypes. To do this,
    // we build a linked list of type descriptions, and make multiple passes
    // over this, adding types to the merged type system when their supertypes
    // become defined. We continue until the list is empty or we cannot make any
    // progress.
    LinkedList<TypeDescription> typeList = new LinkedList<>();
    Iterator<? extends TypeSystemDescription> it = aTypeSystems.iterator();
    while (it.hasNext()) {
      TypeSystemDescription ts = it.next();
      if (ts != null) {
        try {
          ts.resolveImports(aResourceManager);
        } catch (InvalidXMLException e) {
          throw new ResourceInitializationException(e);
        }
        TypeDescription[] types = ts.getTypes();
        typeList.addAll(Arrays.asList(types));
      }
    }
    int lastNumTypes;
    do {
      lastNumTypes = typeList.size();
      Iterator<TypeDescription> typeIter = typeList.iterator();
      while (typeIter.hasNext()) {
        TypeDescription type = typeIter.next();
        String supertypeName = type.getSupertypeName();
        if (supertypeName.startsWith("uima.cas") || supertypeName.startsWith("uima.tcas")
                || typeNameMap.containsKey(supertypeName)) {
          // supertype is defined, ok to proceed
          // check if type is already defined
          addTypeToMergedTypeSystem(aOutputMergedTypes, typeNameMap, type);
          typeIter.remove();
        }
      }
    } while (typeList.size() > 0 && typeList.size() != lastNumTypes);

    // At this point, if the typeList is not empty, then we either have a type with an undefined
    // supertype, or a cycle.
    // We go ahead and merge the type definitions anyway - these problems will be caught at CAS
    // creation time. Undefined supertypes
    // may be OK at this stage - this type system will have to be further merged before it can be
    // used.
    Iterator<TypeDescription> typeIter = typeList.iterator();
    while (typeIter.hasNext()) {
      TypeDescription type = typeIter.next();
      addTypeToMergedTypeSystem(aOutputMergedTypes, typeNameMap, type);
    }

    // create the type system and populate from the typeNamesMap
    TypeSystemDescription result = UIMAFramework.getResourceSpecifierFactory()
            .createTypeSystemDescription();
    TypeDescription[] types = new TypeDescription[typeNameMap.values().size()];
    typeNameMap.values().toArray(types);
    result.setTypes(types);
    return result;
  }

  private static void addTypeToMergedTypeSystem(Map<String, Set<String>> aOutputMergedTypes,
          Map<String, TypeDescription> typeNameMap, TypeDescription type)
          throws ResourceInitializationException {
    String typeName = type.getName();
    String supertypeName = type.getSupertypeName();
    TypeDescription existingType = typeNameMap.get(typeName);
    if (existingType == null) {
      // create new type
      existingType = UIMAFramework.getResourceSpecifierFactory().createTypeDescription();
      existingType.setName(typeName);
      existingType.setDescription(type.getDescription());
      existingType.setSupertypeName(supertypeName);
      existingType.setAllowedValues(type.getAllowedValues());
      existingType.setSourceUrl(type.getSourceUrl());
      typeNameMap.put(type.getName(), existingType);
      FeatureDescription[] features = type.getFeatures();
      if (features != null) {
        mergeFeatures(existingType, type.getFeatures());
      }
    } else {
      // type already existed - check that supertypes are compatible
      String existingSupertypeName = existingType.getSupertypeName();
      if (!existingSupertypeName.equals(supertypeName)) {
        // supertypes are not identical - check if one subsumes the other
        if (subsumes(existingSupertypeName, supertypeName, typeNameMap)) {
          // existing supertype subsumes newly specified supertype -
          // reset supertype to the new, more specific type
          existingType.setSupertypeName(supertypeName);
          // report that a merge occurred
          reportMerge(aOutputMergedTypes, type, existingType);
        } else if (subsumes(supertypeName, existingSupertypeName, typeNameMap)) {
          // newly specified supertype subsumes old type, this is OK and we don't
          // need to do anything except report this
          reportMerge(aOutputMergedTypes, type, existingType);
        } else {
          // error
          throw new ResourceInitializationException(
                  ResourceInitializationException.INCOMPATIBLE_SUPERTYPES, new Object[] { typeName,
                      supertypeName, existingSupertypeName, type.getSourceUrlString() });
        }
      }
      // merge features or check string allowed values are the same
      if (supertypeName.equals("uima.cas.String")) {
        AllowedValue[] av1 = getAllowedValues(type);
        AllowedValue[] av2 = getAllowedValues(existingType);
        if (!isAllowedValuesMatch(av1, av2)) {
          throw new ResourceInitializationException(
                  ResourceInitializationException.ALLOWED_VALUES_NOT_IDENTICAL, new Object[] {
                      typeName, avAsString(av1), avAsString(av2), type.getSourceUrlString() });
        }
      } else {
        int prevNumFeatures = existingType.getFeatures().length;
        FeatureDescription[] features = type.getFeatures();
        if (features != null) {
          mergeFeatures(existingType, type.getFeatures());
          // if feature-merged occurred, the number of features on the type will have
          // changed. Report this by adding to the aOutputMergedTypeNames collection.
          if (existingType.getFeatures().length != prevNumFeatures) {
            reportMerge(aOutputMergedTypes, type, existingType);
          }
        }
      }
    }
  }

  private static boolean isAllowedValuesMatch(AllowedValue[] av1, AllowedValue[] av2) {
    if (av1.length != av2.length) {
      return false;
    }

    Set<String> s1 = new HashSet<>(av1.length);
    Set<String> s2 = new HashSet<>(av1.length);

    for (AllowedValue av : av1) {
      s1.add(av.getString());
    }

    for (AllowedValue av : av2) {
      s2.add(av.getString());
    }

    return s1.equals(s2);
  }

  private static String avAsString(AllowedValue[] av) {
    StringBuilder sb = new StringBuilder("{");
    for (int i = 0; i < av.length; i++) {
      sb.append(av[i].getString());
      if (i < av.length - 1) {
        sb.append(", ");
      }
    }
    sb.append('}');
    return sb.toString();
  }

  private static AllowedValue[] getAllowedValues(TypeDescription type) {
    AllowedValue[] r = type.getAllowedValues();
    if (r == null) {
      return EMPTY_ALLOWED_VALUE_ARRAY;
    }
    return r;
  }

  /**
   * Utility method for populating the aOutputMergedTypes argument in the mergeTypeSystems method.
   * 
   * @param aOutputMergedTypes
   *          Map to populate
   * @param currentType
   *          TypeDescription currently being processed
   * @param existingType
   *          TypeDescription that already existed for the same name
   */
  private static void reportMerge(Map<String, Set<String>> aOutputMergedTypes,
          TypeDescription currentType, TypeDescription existingType) {
    if (aOutputMergedTypes != null) {
      String typeName = currentType.getName();
      Set<String> descriptorUrls = aOutputMergedTypes.get(typeName);
      if (descriptorUrls == null) {
        descriptorUrls = new TreeSet<>();
        descriptorUrls.add(existingType.getSourceUrlString());
        descriptorUrls.add(currentType.getSourceUrlString());
        aOutputMergedTypes.put(typeName, descriptorUrls);
      } else {
        descriptorUrls.add(currentType.getSourceUrlString());
      }
    }
  }

  /**
   * Merges the Type Systems of each component within an aggregate Analysis Engine, producing a
   * single combined Type System.
   * 
   * @param aAggregateDescription
   *          an aggregate Analysis Engine description
   * 
   * @return a new TypeSystemDescription that is the result of merging all of the delegate AE type
   *         systems together
   * 
   * @throws ResourceInitializationException
   *           if an incompatibility exists or if an import could not be resolved
   */
  public static TypeSystemDescription mergeDelegateAnalysisEngineTypeSystems(
          AnalysisEngineDescription aAggregateDescription) throws ResourceInitializationException {
    return mergeDelegateAnalysisEngineTypeSystems(aAggregateDescription,
            UIMAFramework.newDefaultResourceManager());
  }

  /**
   * Merges the Type Systems of each component within an aggregate Analysis Engine, producing a
   * single combined Type System.
   * <p>
   * This version of this method takes an argument <code>aOutputMergedTypeNames</code>, to which
   * this method will add the names of any types whose definitions have been merged from multiple
   * non-identical sources. That is, types that are declared more than once, with different (but
   * compatible) sets of features in each declaration, or with different (but compatible)
   * supertypes.
   * 
   * @param aAggregateDescription
   *          an aggregate Analysis Engine description
   * @param aResourceManager
   *          ResourceManager instance used to resolve imports
   * 
   * @return a new TypeSystemDescription that is the result of merging all of the delegate AE type
   *         systems together
   * 
   * @throws ResourceInitializationException
   *           if an incompatibility exists or if an import could not be resolved
   */
  public static TypeSystemDescription mergeDelegateAnalysisEngineTypeSystems(
          AnalysisEngineDescription aAggregateDescription, ResourceManager aResourceManager)
          throws ResourceInitializationException {
    return mergeDelegateAnalysisEngineTypeSystems(aAggregateDescription, aResourceManager, null);
  }

  /**
   * Merges the Type Systems of each component within an aggregate Analysis Engine, producing a
   * single combined Type System.
   * <p>
   * This version of this method takes an argument <code>aOutputMergedTypes</code>, which this
   * method will populate with the names and descriptor locations of any types whose definitions
   * have been merged from multiple non-identical sources. That is, types that are declared more
   * than once, with different (but compatible) sets of features in each declaration, or with
   * different (but compatible) supertypes.
   * 
   * @param aAggregateDescription
   *          an aggregate Analysis Engine description
   * @param aResourceManager
   *          ResourceManager instance used to resolve imports
   * @param aOutputMergedTypes
   *          A Map that this method will populate with information about the set of types whose
   *          definitions were merged from multiple non-identical sources. The keys in the Map will
   *          be the type names (Strings) and the values will be {link Set}s containing Descriptor
   *          URLs (Strings) where those types are declared. You may pass null if you are not
   *          interested in this information. *
   * @return a new TypeSystemDescription that is the result of merging all of the delegate AE type
   *         systems together
   * 
   * @throws ResourceInitializationException
   *           if an incompatibility exists or if an import could not be resolved
   */
  public static TypeSystemDescription mergeDelegateAnalysisEngineTypeSystems(
          AnalysisEngineDescription aAggregateDescription, ResourceManager aResourceManager,
          Map<String, Set<String>> aOutputMergedTypes) throws ResourceInitializationException {
    // expand the aggregate AE description into the individual delegates
    List<AnalysisEngineDescription> l = new ArrayList<>();
    l.add(aAggregateDescription);
    List<ProcessingResourceMetaData> mdList = getMetaDataList(l, aResourceManager);

    // extract type systems and merge
    List<TypeSystemDescription> typeSystems = new ArrayList<>();
    Iterator<ProcessingResourceMetaData> it = mdList.iterator();
    while (it.hasNext()) {
      ProcessingResourceMetaData md = it.next();
      if (md.getTypeSystem() != null)
        typeSystems.add(md.getTypeSystem());
    }
    return mergeTypeSystems(typeSystems, aResourceManager, aOutputMergedTypes);
  }

  /**
   * Merges a List of FsIndexCollections into a single FsIndexCollection object.
   * 
   * @param aFsIndexCollections
   *          list of FsIndexCollection objects
   * @param aResourceManager
   *          ResourceManager instance to use to resolve imports
   * 
   * @return a merged FsIndexCollection object
   * @throws ResourceInitializationException
   *           if an incompatibility exists or if an import could not be resolved
   */
  public static FsIndexCollection mergeFsIndexes(
          List<? extends FsIndexCollection> aFsIndexCollections, ResourceManager aResourceManager)
          throws ResourceInitializationException {
    Map<String, FsIndexDescription> aggIndexes = new HashMap<>();
    Iterator<? extends FsIndexCollection> it = aFsIndexCollections.iterator();
    while (it.hasNext()) {
      FsIndexCollection indexColl = it.next();

      if (indexColl != null) {
        try {
          indexColl.resolveImports(aResourceManager);
        } catch (InvalidXMLException e) {
          throw new ResourceInitializationException(e);
        }
        FsIndexDescription[] indexes = indexColl.getFsIndexes();
        for (int i = 0; i < indexes.length; i++) {
          // does an index with this label already exist?
          FsIndexDescription duplicateIndex = aggIndexes.get(indexes[i].getLabel());
          if (duplicateIndex == null) {
            // no, so add it
            aggIndexes.put(indexes[i].getLabel(), indexes[i]);
          } else if (!duplicateIndex.equals(indexes[i])) {
            // index with same label exists, they better be equal!
            throw new ResourceInitializationException(
                    ResourceInitializationException.DUPLICATE_INDEX_NAME,
                    new Object[] { duplicateIndex.getLabel(), duplicateIndex.getSourceUrlString(),
                        indexes[i].getSourceUrlString() });
          }
        }
      }
    }

    // convert index map to FsIndexCollection
    FsIndexCollection aggIndexColl = UIMAFramework.getResourceSpecifierFactory()
            .createFsIndexCollection();
    Collection<FsIndexDescription> indexes = aggIndexes.values();
    FsIndexDescription[] indexArray = new FsIndexDescription[indexes.size()];
    indexes.toArray(indexArray);
    aggIndexColl.setFsIndexes(indexArray);
    return aggIndexColl;
  }

  /**
   * Merges the FS Index Collections of each component within an aggregate Analysis Engine,
   * producing a single combined FS Index Collection.
   * 
   * @param aAggregateDescription
   *          an aggregate Analysis Engine description
   * 
   * @return a new FsIndexCollection that is the result of merging all of the delegate AE
   *         FsIndexCollections together
   * 
   * @throws ResourceInitializationException
   *           if an incompatibility exists or if an import could not be resolved
   */
  public static FsIndexCollection mergeDelegateAnalysisEngineFsIndexCollections(
          AnalysisEngineDescription aAggregateDescription) throws ResourceInitializationException {
    return mergeDelegateAnalysisEngineFsIndexCollections(aAggregateDescription,
            UIMAFramework.newDefaultResourceManager());
  }

  /**
   * Merges the FS Index Collections of each component within an aggregate Analysis Engine,
   * producing a single combined FS Index Collection.
   * 
   * @param aAggregateDescription
   *          an aggregate Analysis Engine description
   * @param aResourceManager
   *          ResourceManager instance used to resolve imports
   * 
   * @return a new FsIndexCollection that is the result of merging all of the delegate AE
   *         FsIndexCollections together
   * 
   * @throws ResourceInitializationException
   *           if an incompatibility exists or if an import could not be resolved
   */
  public static FsIndexCollection mergeDelegateAnalysisEngineFsIndexCollections(
          AnalysisEngineDescription aAggregateDescription, ResourceManager aResourceManager)
          throws ResourceInitializationException {
    // expand the aggregate AE description into the individual delegates
    List<AnalysisEngineDescription> l = new ArrayList<>();
    l.add(aAggregateDescription);
    List<ProcessingResourceMetaData> mdList = getMetaDataList(l, aResourceManager);

    // extract FsIndexCollections and merge
    List<FsIndexCollection> fsIndexes = new ArrayList<>();
    Iterator<ProcessingResourceMetaData> it = mdList.iterator();
    while (it.hasNext()) {
      ProcessingResourceMetaData md = it.next();
      if (md.getFsIndexCollection() != null)
        fsIndexes.add(md.getFsIndexCollection());
    }
    return mergeFsIndexes(fsIndexes, aResourceManager);
  }

  /**
   * Merges a List of TypePriorities into a single TypePriorities object.
   * 
   * @param aTypePriorities
   *          list of TypePriorities objects
   * @param aResourceManager
   *          ResourceManager instance to use to resolve imports
   * 
   * @return a merged TypePriorities object
   * @throws ResourceInitializationException
   *           if an import could not be resolved
   */
  public static TypePriorities mergeTypePriorities(List<? extends TypePriorities> aTypePriorities,
          ResourceManager aResourceManager) throws ResourceInitializationException {
    TypePriorities aggTypePriorities = UIMAFramework.getResourceSpecifierFactory()
            .createTypePriorities();
    Iterator<? extends TypePriorities> it = aTypePriorities.iterator();
    while (it.hasNext()) {
      TypePriorities tp = it.next();
      try {
        tp.resolveImports(aResourceManager);
      } catch (InvalidXMLException e) {
        throw new ResourceInitializationException(e);
      }
      TypePriorityList[] pls = tp.getPriorityLists();
      if (pls != null) {
        for (int i = 0; i < pls.length; i++) {
          aggTypePriorities.addPriorityList(pls[i]);
        }
      }
    }
    return aggTypePriorities;
  }

  /**
   * Merges the Type Priorities of each component within an aggregate Analysis Engine, producing a
   * single combined TypePriorities object.
   * 
   * @param aAggregateDescription
   *          an aggregate Analysis Engine description
   * 
   * @return a new TypePriorities object that is the result of merging all of the delegate AE
   *         TypePriorities together
   * 
   * @throws ResourceInitializationException
   *           if an incompatibility exists
   */
  public static TypePriorities mergeDelegateAnalysisEngineTypePriorities(
          AnalysisEngineDescription aAggregateDescription) throws ResourceInitializationException {
    return mergeDelegateAnalysisEngineTypePriorities(aAggregateDescription,
            UIMAFramework.newDefaultResourceManager());
  }

  /**
   * Merges the Type Priorities of each component within an aggregate Analysis Engine, producing a
   * single combined TypePriorities object.
   * 
   * @param aAggregateDescription
   *          an aggregate Analysis Engine description
   * @param aResourceManager
   *          ResourceManager instance used to resolve imports
   * 
   * @return a new TypePriorities object that is the result of merging all of the delegate AE
   *         TypePriorities together
   * 
   * @throws ResourceInitializationException
   *           if an incompatibility exists
   */
  public static TypePriorities mergeDelegateAnalysisEngineTypePriorities(
          AnalysisEngineDescription aAggregateDescription, ResourceManager aResourceManager)
          throws ResourceInitializationException {
    // expand the aggregate AE description into the individual delegates
    ArrayList<AnalysisEngineDescription> l = new ArrayList<>();
    l.add(aAggregateDescription);
    List<ProcessingResourceMetaData> mdList = getMetaDataList(l, aResourceManager);

    // extract TypePriorities and merge
    List<TypePriorities> typePriorities = new ArrayList<>();
    Iterator<ProcessingResourceMetaData> it = mdList.iterator();
    while (it.hasNext()) {
      ProcessingResourceMetaData md = it.next();
      if (md.getTypePriorities() != null)
        typePriorities.add(md.getTypePriorities());
    }
    return mergeTypePriorities(typePriorities, aResourceManager);
  }

  /**
   * Merges the Type Systems, Type Priorities, and FS Indexes of each component within an aggregate
   * Analysis Engine.
   * <p>
   * This version of this method takes an argument <code>aOutputMergedTypes</code>, which this
   * method will populate with the names and descriptor locations of any types whose definitions
   * have been merged from multiple non-identical sources. That is, types that are declared more
   * than once, with different (but compatible) sets of features in each declaration, or with
   * different (but compatible) supertypes.
   * 
   * @param aAggregateDescription
   *          an aggregate Analysis Engine description
   * @param aResourceManager
   *          ResourceManager instance used to resolve imports
   * @param aOutputMergedTypes
   *          A Map that this method will populate with information about the set of types whose
   *          definitions were merged from multiple non-identical sources. That is, types that are
   *          declared more than once, with different (but compatible) sets of features in each
   *          declaration, or with different (but compatible) supertypes. The keys in the Map will
   *          be the type names (Strings) and the values will be {link Set}s containing Descriptor
   *          URLs (Strings) where those types are declared. You may pass null if you are not
   *          interested in this information.
   * @param aOutputFailedRemotes
   *          If this parameter is non-null, and if a remote AE could not be contacted, then an
   *          entry will be added to this map. The key will be the context name (e.g.,
   *          /myDelegate1/nestedRemoteDelegate) of the failed remote, and the value will be the
   *          Exception that occurred. If this parameter is null, an exception will be thrown if a
   *          remote AE could not be contacted.
   * 
   * @return an object containing the merged TypeSystem, TypePriorities, and FS Index definitions.
   * 
   * @throws ResourceInitializationException
   *           if an incompatibility exists or if an import could not be resolved
   */
  public static ProcessingResourceMetaData mergeDelegateAnalysisEngineMetaData(
          AnalysisEngineDescription aAggregateDescription, ResourceManager aResourceManager,
          Map<String, Set<String>> aOutputMergedTypes,
          Map<String, ? super Exception> aOutputFailedRemotes)
          throws ResourceInitializationException {
    // expand the aggregate AE description into the individual delegates
    ArrayList<AnalysisEngineDescription> l = new ArrayList<>();
    l.add(aAggregateDescription);
    List<ProcessingResourceMetaData> mdList = getMetaDataList(l, aResourceManager,
            aOutputFailedRemotes);

    ProcessingResourceMetaData result = UIMAFramework.getResourceSpecifierFactory()
            .createProcessingResourceMetaData();

    // extract type systems and merge
    List<TypeSystemDescription> typeSystems = new ArrayList<>();
    Iterator<ProcessingResourceMetaData> it = mdList.iterator();
    while (it.hasNext()) {
      ProcessingResourceMetaData md = it.next();
      if (md.getTypeSystem() != null)
        typeSystems.add(md.getTypeSystem());
    }
    result.setTypeSystem(mergeTypeSystems(typeSystems, aResourceManager, aOutputMergedTypes));

    // extract TypePriorities and merge
    List<TypePriorities> typePriorities = new ArrayList<>();
    it = mdList.iterator();
    while (it.hasNext()) {
      ProcessingResourceMetaData md = it.next();
      if (md.getTypePriorities() != null)
        typePriorities.add(md.getTypePriorities());
    }
    result.setTypePriorities(mergeTypePriorities(typePriorities, aResourceManager));

    // extract FsIndexCollections and merge
    List<FsIndexCollection> fsIndexes = new ArrayList<>();
    it = mdList.iterator();
    while (it.hasNext()) {
      ProcessingResourceMetaData md = it.next();
      if (md.getFsIndexCollection() != null)
        fsIndexes.add(md.getFsIndexCollection());
    }
    result.setFsIndexCollection(mergeFsIndexes(fsIndexes, aResourceManager));

    return result;
  }

  /**
   * Determines whether one type subsumes another.
   * 
   * @param aType1Name
   *          name of first type
   * @param aType2Name
   *          name of second type
   * @param aNameMap
   *          Map from type names to TypeDescriptions
   * 
   * @return true if and only if the type named <code>aType1Name</code> subsumes the type named
   *         <code>aType2Name</code> according to the information given in the
   *         <code>aNameMap</code>.
   */
  protected static boolean subsumes(String aType1Name, String aType2Name,
          Map<String, ? extends TypeDescription> aNameMap) {
    // Top type subsumes everything
    if (CAS.TYPE_NAME_TOP.equals(aType1Name)) {
      return true;
    }

    // "walk up the tree" from aType2Name until we reach aType1Name or null
    String current = aType2Name;
    while (current != null && !current.equals(aType1Name)) {
      TypeDescription curType = aNameMap.get(current);
      if (curType == null)
        current = null;
      else
        current = curType.getSupertypeName();
    }

    return (current != null);
  }

  /**
   * Merges features into a TypeDescription.
   * 
   * @param aType
   *          TypeDescription into which to merge the features
   * @param aFeatures
   *          array of features to merge
   * 
   * @throws ResourceInitializationException
   *           if an incompatibility exists
   */
  protected static void mergeFeatures(TypeDescription aType, FeatureDescription[] aFeatures)
          throws ResourceInitializationException {
    FeatureDescription[] existingFeatures = aType.getFeatures();
    if (existingFeatures == null) {
      existingFeatures = EMPTY_FEAT_DESC_ARRAY;
    }

    for (int i = 0; i < aFeatures.length; i++) {
      String featName = aFeatures[i].getName();
      String rangeTypeName = aFeatures[i].getRangeTypeName();
      String elementTypeName = aFeatures[i].getElementType();
      Boolean multiRefsAllowed = aFeatures[i].getMultipleReferencesAllowed();

      // see if a feature already exists with this name
      FeatureDescription feat = null;
      for (int j = 0; j < existingFeatures.length; j++) {
        if (existingFeatures[j].getName().equals(featName)) {
          feat = existingFeatures[j];
          break;
        }
      }

      if (feat == null) {
        // doesn't exist; add it
        FeatureDescription featDesc = aType.addFeature(featName, aFeatures[i].getDescription(),
                rangeTypeName, elementTypeName, multiRefsAllowed);
        featDesc.setSourceUrl(aFeatures[i].getSourceUrl());
      } else {// feature does exist
        // check that the range types match
        if (!feat.getRangeTypeName().equals(rangeTypeName)) {
          throw new ResourceInitializationException(
                  ResourceInitializationException.INCOMPATIBLE_RANGE_TYPES,
                  new Object[] { aType.getName() + ":" + feat.getName(), rangeTypeName,
                      feat.getRangeTypeName(), aType.getSourceUrlString() });
        }
        Boolean mra1 = feat.getMultipleReferencesAllowed();
        Boolean mra2 = multiRefsAllowed;

        // the logic here:
        // OK if both null
        // OK if both not-null, and are equals()
        // OK if one is null, the other has boolean-value of false (false is the default)
        // not ok otherwise

        if (!(((mra1 == null) && (mra2 == null)) || ((mra1 != null) && mra1.equals(mra2))
                || ((mra1 == null) && !mra2) || ((mra2 == null) && !mra1))) {
          throw new ResourceInitializationException(
                  ResourceInitializationException.INCOMPATIBLE_MULTI_REFS, new Object[] {
                      aType.getName() + ":" + feat.getName(), aType.getSourceUrlString() });
        }

        if (!elementTypesCompatible(feat.getElementType(), elementTypeName)) {
          throw new ResourceInitializationException(
                  ResourceInitializationException.INCOMPATIBLE_ELEMENT_RANGE_TYPES,
                  new Object[] { aType.getName() + TypeSystem.FEATURE_SEPARATOR + feat.getName(),
                      elementTypeName, feat.getElementType(), aType.getSourceUrlString() });
        }
      }
    }
  }

  /**
   * Compare element type names for array-like features
   * 
   * @param o1
   *          name of first element type
   * @param o2
   *          name of second element type
   * @return true if elements are compatible for merging features
   */
  private static boolean elementTypesCompatible(String o1, String o2) {
    return ((null == o1) && (null == o2)) || ((null != o1) && o1.equals(o2)) ||
    // allow missing types to be equal to TOP
            (o1 != null && o1.equals(CAS.TYPE_NAME_TOP) && o2 == null)
            || (o2 != null && o2.equals(CAS.TYPE_NAME_TOP) && o1 == null);
  }

//@formatter:off
  /*************************************************************************************************
   * Caching of getMeta info that requires producing the resource                                  *
   *   - done because producing the resource can be very expensive                                 *                        
   *     including accessing remote things on the network                                          *
   * Cache is cleared approximately every 30 seconds because remote resource's statuses may change *
   *                                                                                               *
   * Cache key is the ResourceSpecifier's class loaders and the ResourceManager                    *
   *   Both the DataPath and the uima extension class loader are used as part of the key           *
   *   because differences in these could cause different metadata to be loaded                    *
   *************************************************************************************************/
//@formatter:on
  private static class MetaDataCacheKey {
    final ResourceSpecifier resourceSpecifier;
    final ClassLoader rmClassLoader;
    final String rmDataPath;

    MetaDataCacheKey(ResourceSpecifier resourceSpecifier, ResourceManager resourceManager) {
      this.resourceSpecifier = resourceSpecifier;
      rmClassLoader = (null == resourceManager) ? null : resourceManager.getExtensionClassLoader(); // can
                                                                                                    // be
                                                                                                    // null
      rmDataPath = (null == resourceManager) ? null : resourceManager.getDataPath();
    }

    @Override
    public int hashCode() {
      return ((rmClassLoader == null) ? 0 : rmClassLoader.hashCode())
              + ((rmDataPath == null) ? 0 : rmDataPath.hashCode()) + resourceSpecifier.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
      if (null == obj) {
        return false;
      }
      MetaDataCacheKey k = (MetaDataCacheKey) obj;
      if (rmDataPath == null) {
        if (k.rmDataPath != null) {
          return false;
        }
        return resourceSpecifier.equals(k.resourceSpecifier) && rmClassLoader == k.rmClassLoader;
      }
      return resourceSpecifier.equals(k.resourceSpecifier) && rmClassLoader == k.rmClassLoader
              && rmDataPath.equals(k.rmDataPath);
    }

    @Override
    public String toString() {
      return this.getClass().getSimpleName() + " [resourceSpecifier=" + resourceSpecifier
              + ", rmClassLoader=" + rmClassLoader + ", rmDataPath=" + rmDataPath + "]";
    }
  }

  private static final boolean cacheDebug = false; // set true for debugging info
  private static final int HOLD_TIME = 30000; // keep cache for 30 seconds, approx., in case a
                                              // remote resource changes state

  /**
   * This is the cache. All references to it are synchronized, using it as the object.
   */
  private static final transient Map<MetaDataCacheKey, MetaDataCacheEntry> metaDataCache = new HashMap<>();

  /**
   * This holds an instance of a Timer object This object is nulled out and gets gc'd when it's
   * timertask finishes, when the cache is empty.
   * 
   * All references to it are synchronized under the metaDataCache.
   */
  private static Timer cleanupTimer = null;

  /**
   * This class holds the processing Resource Metadata, or null if there is none, and a timestamp
   * when the metadata was obtained.
   */
  private static class MetaDataCacheEntry {
    ProcessingResourceMetaData processingResourceMetaData;
    long creationTime;

    MetaDataCacheEntry(ResourceMetaData resourceMetaData) {
      processingResourceMetaData = (resourceMetaData instanceof ProcessingResourceMetaData)
              ? (ProcessingResourceMetaData) resourceMetaData
              : null;
      creationTime = System.currentTimeMillis();
      if (null == cleanupTimer) {
        if (cacheDebug) {
          System.err.format("GetMetaDataCache: Scheduling new cleanup task%n");
        }

        cleanupTimer = new Timer("metaDataCacheCleanup", true); // run as daemon
        // create a new instance of the timer task, because a previous one may
        // still be running
        TimerTask metaDataCacheCleanupTask = new TimerTask() {
          @Override
          public void run() {
            synchronized (metaDataCache) {
              long now = System.currentTimeMillis();
              if (cacheDebug) {
                System.err.format("GetMetaDataCache: cleanup task running%n");
              }
              for (Iterator<Entry<MetaDataCacheKey, MetaDataCacheEntry>> it = metaDataCache
                      .entrySet().iterator(); it.hasNext();) {
                Entry<MetaDataCacheKey, MetaDataCacheEntry> e = it.next();
                if (e.getValue().creationTime + HOLD_TIME < now) {
                  if (cacheDebug) {
                    System.err.format("GetMetaDataCache: cleanup task removing entry %s%n",
                            e.getKey().toString());
                  }
                  it.remove();
                }
              }
              if (metaDataCache.size() == 0) {
                if (cacheDebug) {
                  System.err.format("GetMetaDataCache: cleanup task terminating, cache empty%n");
                }
                cancel();
                cleanupTimer.cancel(); // probably not needed, but for safety ...
                cleanupTimer = null;
              }
              if (cacheDebug) {
                System.err.format("GetMetaDataCache: cleanup task finished a cycle%n");
              }
            }
          }
        };
        cleanupTimer.schedule(metaDataCacheCleanupTask, HOLD_TIME, HOLD_TIME);
      }
    }
  }

  /**
   * Gets a list of ProcessingResourceMetadata objects from a list containing either
   * ResourceSpecifiers, ProcessingResourceMetadata objects, or subparts of
   * ProcessingResourceMetadata objects (type systems, indexes, or type priorities). Subparts will
   * be wrapped inside a ProcessingResourceMetadata object. All objects will be cloned, so that
   * further processing (such as import resolution) does not affect the caller.
   * <p>
   * If you pass this method objects of type {@link AnalysisEngineDescription},
   * {@link CollectionReaderDescription}, {@link CasInitializerDescription}, or
   * {@link CasConsumerDescription}, it will not instantiate the components. It will just extract
   * the type system information from the descriptor. For any other kind of
   * {@link ResourceSpecifier}, it will call
   * {@link UIMAFramework#produceResource(org.apache.uima.resource.ResourceSpecifier, Map)}. For
   * example, if a {@link URISpecifier} is passed, a remote connection will be established and the
   * service will be queries for its metadata. An exception will be thrown if the connection can not
   * be opened.
   * 
   * Note that this last kind of lookup may be expensive (calling produceResource, which in turn may
   * query remote connections etc.). Because of this, a cache is maintained for these, (because some
   * scenarios end up requesting the same metadata multiple times, in rapid succession).
   * 
   * Because remote resource may become available, the cache entries are removed 30 seconds after
   * they are created. This also reclaims space from the cache.
   * 
   * @param aComponentDescriptionOrMetaData
   *          a collection of {@link ResourceSpecifier}, {@link ProcessingResourceMetaData},
   *          {@link TypeSystemDescription}, {@link FsIndexCollection}, or {@link TypePriorities}
   *          objects.
   * @param aResourceManager
   *          used to resolve delegate analysis engine imports
   * @param aOutputFailedRemotes
   *          If this parameter is non-null, and if a remote AE could not be contacted, then the
   *          context name (e.g. /myDelegate1/nestedRemoteDelegate) of the failed remote will be
   *          added to this collection. If this parameter is null, an exception will be thrown if a
   *          remote AE could not be contacted.
   * 
   * @return a List containing the ProcessingResourceMetaData objects containing all of the
   *         information in all of the objects in <code>aComponentDescriptionOrMetaData</code>
   *         (including all components of aggregate AnalysisEngines)
   * 
   * @throws ResourceInitializationException
   *           if a failure occurs because an import could not be resolved
   */
  public static List<ProcessingResourceMetaData> getMetaDataList(
          Collection<? extends MetaDataObject> aComponentDescriptionOrMetaData,
          ResourceManager aResourceManager, Map<String, ? super Exception> aOutputFailedRemotes)
          throws ResourceInitializationException {
    return getMetaDataList(aComponentDescriptionOrMetaData, aResourceManager, aOutputFailedRemotes,
            "");
  }

  private static List<ProcessingResourceMetaData> getMetaDataList(
          Collection<? extends MetaDataObject> aComponentDescriptionOrMetaData,
          ResourceManager aResourceManager, Map<String, ? super Exception> aOutputFailedRemotes,
          String aContextName) throws ResourceInitializationException {

    List<ProcessingResourceMetaData> mdList = new ArrayList<>();
    if (null == aComponentDescriptionOrMetaData) {
      return mdList;
    }
    Iterator<? extends MetaDataObject> iter = aComponentDescriptionOrMetaData.iterator();
    while (iter.hasNext()) {
      Object current = iter.next();
      if (current instanceof ProcessingResourceMetaData) {
        mdList.add((ProcessingResourceMetaData) ((ProcessingResourceMetaData) current).clone());
      } else if (current instanceof AnalysisEngineDescription) {
        AnalysisEngineDescription aeDesc = (AnalysisEngineDescription) current;
        mdList.add((ProcessingResourceMetaData) aeDesc.getAnalysisEngineMetaData().clone());
        // expand aggregate
        if (!aeDesc.isPrimitive()) {
          Map<String, ResourceSpecifier> delegateMap;
          try {
            delegateMap = aeDesc.getAllComponentSpecifiers(aResourceManager);
          } catch (InvalidXMLException e) {
            throw new ResourceInitializationException(e);
          }
          Iterator<Map.Entry<String, ResourceSpecifier>> delIter = delegateMap.entrySet()
                  .iterator();
          while (delIter.hasNext()) {
            Map.Entry<String, ResourceSpecifier> delEntry = delIter.next();
            List<ResourceSpecifier> tempList = new ArrayList<>();
            tempList.add(delEntry.getValue());
            mdList.addAll(getMetaDataList(tempList, aResourceManager, aOutputFailedRemotes,
                    aContextName + "/" + delEntry.getKey()));
          }
        }
      } else if (current instanceof CollectionReaderDescription) {
        mdList.add((ProcessingResourceMetaData) ((CollectionReaderDescription) current)
                .getCollectionReaderMetaData().clone());
      } else if (current instanceof CasInitializerDescription) {
        mdList.add((ProcessingResourceMetaData) ((CasInitializerDescription) current)
                .getCasInitializerMetaData().clone());
      } else if (current instanceof CasConsumerDescription) {
        mdList.add((ProcessingResourceMetaData) ((CasConsumerDescription) current)
                .getCasConsumerMetaData().clone());
      } else if (current instanceof FlowControllerDescription) {
        mdList.add((ProcessingResourceMetaData) ((FlowControllerDescription) current)
                .getFlowControllerMetaData().clone());
      } else if (current instanceof TypeSystemDescription) {
        ProcessingResourceMetaData md = new ProcessingResourceMetaData_impl();
        md.setTypeSystem((TypeSystemDescription) current);
        mdList.add(md);
      } else if (current instanceof FsIndexCollection) {
        ProcessingResourceMetaData md = new ProcessingResourceMetaData_impl();
        md.setFsIndexCollection((FsIndexCollection) current);
        mdList.add(md);
      } else if (current instanceof TypePriorities) {
        ProcessingResourceMetaData md = new ProcessingResourceMetaData_impl();
        md.setTypePriorities((TypePriorities) current);
        mdList.add(md);
      } else if (current instanceof ResourceSpecifier) {

        // first try the cache
        MetaDataCacheKey metaDataCacheKey = new MetaDataCacheKey((ResourceSpecifier) current,
                aResourceManager);
        synchronized (metaDataCache) {
          MetaDataCacheEntry metaData = metaDataCache.get(metaDataCacheKey);
          if (null != metaData) {
            if (cacheDebug) {
              System.err.format("GetMetaDataCache: using cached entry%n");
            }
            if (null != metaData.processingResourceMetaData) {
              mdList.add(metaData.processingResourceMetaData);
            }
            continue;
          }
        }

        // try to instantiate the resource

        Resource resource = null;
        Map<String, Object> prParams = new HashMap<>();
        if (aResourceManager != null) {
          prParams.put(Resource.PARAM_RESOURCE_MANAGER, aResourceManager);
        }
        prParams.put(AnalysisEngineImplBase.PARAM_VERIFICATION_MODE, Boolean.TRUE);
        try {
          resource = UIMAFramework.produceResource((ResourceSpecifier) current, prParams);
          // (null == aResourceManager) ? Collections.<String, Object>emptyMap() :
          // resourceMgrInMap);
        } catch (Exception e) {
          // record failure, so we don't ask for this again, for a while
          synchronized (metaDataCache) {
            if (cacheDebug) {
              System.err.format("GetMetaDataCache: saving entry in cache%n");
            }
            metaDataCache.put(metaDataCacheKey, new MetaDataCacheEntry(null));
          }
          // failed. If aOutputFailedRemotes is non-null, add an entry to it to it, else throw the
          // exception.
          if (aOutputFailedRemotes != null) {
            aOutputFailedRemotes.put(aContextName, e);
          } else {
            if (e instanceof ResourceInitializationException)
              throw (ResourceInitializationException) e;
            else if (e instanceof RuntimeException)
              throw (RuntimeException) e;
            else
              throw new RuntimeException(e);
          }
        }
        ResourceMetaData metadata = (resource == null) ? null : resource.getMetaData();

        synchronized (metaDataCache) {
          if (cacheDebug) {
            System.err.format("GetMetaDataCache: saving entry in cache%n");
          }
          metaDataCache.put(metaDataCacheKey, new MetaDataCacheEntry(metadata));
        }

        if (resource != null) {
          if (metadata instanceof ProcessingResourceMetaData) {
            mdList.add((ProcessingResourceMetaData) metadata);
          }
          resource.destroy();
        }
      } else {
        throw new ResourceInitializationException(
                ResourceInitializationException.UNSUPPORTED_OBJECT_TYPE_IN_CREATE_CAS,
                new Object[] { current.getClass().getName() });
      }
    }

    return mdList;
  }

  /**
   * Gets a list of ProcessingResourceMetadata objects from a list containing either
   * ResourceSpecifiers, ProcessingResourceMetadata objects, or subparts of
   * ProcessingResourceMetadata objects (type systems, indexes, or type priorities). Subparts will
   * be wrapped inside a ProcessingResourceMetadata object. All objects will be cloned, so that
   * further processing (such as import resolution) does not affect the caller.
   * <p>
   * If you pass this method objects of type {@link AnalysisEngineDescription},
   * {@link CollectionReaderDescription}, {@link CasInitializerDescription}, or
   * {@link CasConsumerDescription}, it will not instantiate the components. It will just extract
   * the type system information from the descriptor. For any other kind of
   * {@link ResourceSpecifier}, it will call
   * {@link UIMAFramework#produceResource(org.apache.uima.resource.ResourceSpecifier, Map)}. For
   * example, if a {@link URISpecifier} is passed, a remote connection will be established and the
   * service will be queries for its metadata. An exception will be thrown if the connection can not
   * be opened.
   * 
   * @param aComponentDescriptionOrMetaData
   *          a collection of {@link ResourceSpecifier}, {@link ProcessingResourceMetaData},
   *          {@link TypeSystemDescription}, {@link FsIndexCollection}, or {@link TypePriorities}
   *          objects.
   * @param aResourceManager
   *          used to resolve delegate analysis engine imports
   * 
   * @return a List containing the ProcessingResourceMetaData objects containing all of the
   *         information in all of the objects in <code>aComponentDescriptionOrMetaData</code>
   *         (including all components of aggregate AnalysisEngines)
   * 
   * @throws ResourceInitializationException
   *           if a failure occurs because an import could not be resolved
   */
  public static List<ProcessingResourceMetaData> getMetaDataList(
          Collection<? extends MetaDataObject> aComponentDescriptionOrMetaData,
          ResourceManager aResourceManager) throws ResourceInitializationException {
    return getMetaDataList(aComponentDescriptionOrMetaData, aResourceManager, null);
  }

}
