/*
 * 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.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import org.apache.uima.ResourceFactory;
import org.apache.uima.UIMAFramework;
import org.apache.uima.resource.Resource;
import org.apache.uima.resource.ResourceInitializationException;
import org.apache.uima.resource.ResourceSpecifier;

/**
 * A simple implementation of a {@link org.apache.uima.ResourceFactory}. This implementation
 * maintains a Map between the {@link ResourceSpecifier} sub-interface name (e.g.
 * <code>AnalysisEngineDescription</code>) and the class name of the resource to be constructed from
 * specifiers of that type.
 * <p>
 * UIMA developers who introduce new types of {@link Resource}s or {@link ResourceSpecifier}s may
 * create an instance of this class and use the {@link #addMapping(Class,Class)} method to register
 * a mapping between the ResourceSpecifier interface and the Class of the Resource that is to be
 * constructed from it. The <code>SimpleResourceFactory</code> should then be registered with the
 * framework by calling
 * <code>{@link UIMAFramework#getResourceFactory()}.{@link org.apache.uima.CompositeResourceFactory#registerFactory(Class,ResourceFactory) registerFactory(Class,ResourceFactory)};</code>
 * 
 * 
 */
public class SimpleResourceFactory implements ResourceFactory {
  /**
   * resource bundle for log messages
   */
  private static final String LOG_RESOURCE_BUNDLE = "org.apache.uima.impl.log_messages";

  /**
   * current class
   */
  private static final Class<SimpleResourceFactory> CLASS_NAME = SimpleResourceFactory.class;

  /**
   * Map from ResourceSpecifier Class to List of Resource Classes. Resource initialization is
   * attempted in reverse order through this List, so more recently registered classes are tried
   * first.
   */
  protected Map<Class<? extends ResourceSpecifier>, List<Class<? extends Resource>>> mClassMap = Collections
          .synchronizedMap(new HashMap<>());

  /**
   * Produces an appropriate <code>Resource</code> instance from a <code>ResourceSpecifier</code>.
   * 
   * @param aResourceClass
   *          the interface of the resource to be produced. This is intended to be a standard UIMA
   *          interface such as <code>TextAnalysisEngine</code> or <code>ASB</code>.
   * @param aSpecifier
   *          an object that specifies how to acquire an instance of a <code>Resource</code>.
   * @param aAdditionalParams
   *          a Map containing additional parameters to pass to the
   *          {@link Resource#initialize(ResourceSpecifier,Map)} method. May be <code>null</code> if
   *          there are no parameters.
   * 
   * @return a <code>Resource</code> instance. Returns <code>null</code> if this factory does not
   *         know how to create a Resource from the <code>ResourceSpecifier</code> provided.
   * 
   * @throws ResourceInitializationException
   *           if a failure occurred during production of the resource
   * 
   * @see org.apache.uima.ResourceFactory#produceResource(Class, ResourceSpecifier,Map)
   */
  @Override
  public Resource produceResource(Class<? extends Resource> aResourceClass,
          ResourceSpecifier aSpecifier, Map<String, Object> aAdditionalParams)
          throws ResourceInitializationException {
    ResourceInitializationException lastException = null;

    // get all interfaces implemented by aSpecifier
    Class<?>[] interfaces = aSpecifier.getClass().getInterfaces();

    // look up class mapping
    List<Class<? extends Resource>> resourceClasses = null;
    for (int i = 0; i < interfaces.length; i++) {
      resourceClasses = mClassMap.get(interfaces[i]);
      if (resourceClasses != null)
        break;
    }

    if (resourceClasses != null) {
      // iterate backwards through the elements of the list, so that
      // we attempt to initialize the most recently registered Resource
      // classes first
      ListIterator<Class<? extends Resource>> i = resourceClasses
              .listIterator(resourceClasses.size());
      while (i.hasPrevious()) {
        Class<? extends Resource> currentClass = i.previous();
        ResourceInitializationException currentException = null;
        try {
          // check to see if this is a subclass of aResourceClass
          if (aResourceClass.isAssignableFrom(currentClass)) {
            // instantiate this Resource Class
            Resource resource = currentClass.newInstance();
            // attempt to initialize it
            UIMAFramework.getLogger(CLASS_NAME).logrb(Level.CONFIG, CLASS_NAME.getName(),
                    "produceResource", LOG_RESOURCE_BUNDLE, "UIMA_trying_resource_class__CONFIG",
                    currentClass.getName());

            if (resource.initialize(aSpecifier, aAdditionalParams)) {
              // success!
              return resource;
            }
          }
        }
        // if an exception occurs, log it but do not throw it... yet
        catch (IllegalAccessException e) {
          currentException = new ResourceInitializationException(
                  ResourceInitializationException.COULD_NOT_INSTANTIATE,
                  new Object[] { currentClass.getName(), aSpecifier.getSourceUrlString() }, e);
        } catch (InstantiationException e) {
          currentException = new ResourceInitializationException(
                  ResourceInitializationException.COULD_NOT_INSTANTIATE,
                  new Object[] { currentClass.getName(), aSpecifier.getSourceUrlString() }, e);
        } catch (Throwable t) {
          currentException = new ResourceInitializationException(
                  ResourceInitializationException.ERROR_INITIALIZING_FROM_DESCRIPTOR,
                  new Object[] { currentClass.getName(), aSpecifier.getSourceUrlString() }, t);
        } finally {
          if (currentException != null) {
            currentException.fillInStackTrace();
            // UIMAFramework.getLogger().logException(currentException);
            // store this exception
            lastException = currentException;
          }
        }
        // try again
      }
    }

    // No resource could be created. If an exception occurred,
    // throw it. Otherwise, return null.
    if (lastException != null) {
      throw lastException;
    } else {
      return null;
    }
  }

  /**
   * Configures this <code>SimpleResourceFactory</code> by adding a new mapping between a
   * <code>ResourceSpecifier</code> class and a <code>Resource</code> class.
   * 
   * @param aSpecifierInterface
   *          the subinterface of <code>ResourceSpecifier</code>.
   * @param aResourceClass
   *          a subclass of <code>Resource</code> that is to be instantiated from resource
   *          specifiers of the given class.
   */
  public void addMapping(Class<? extends ResourceSpecifier> aSpecifierInterface,
          Class<? extends Resource> aResourceClass) {
    List<Class<? extends Resource>> mappingList = mClassMap.computeIfAbsent(aSpecifierInterface,
            k -> new ArrayList<>());
    // No mapping exists. Create a new list and put it in the map.

    // add the new Resource Class to the end of the mapping list
    mappingList.add(aResourceClass);
  }

  /**
   * Configures this <code>SimpleResourceFactory</code> by adding a new mapping between a
   * <code>ResourceSpecifier</code> class and a <code>Resource</code> class.
   * 
   * @param aSpecifierInterfaceName
   *          name of the subinterface of <code>ResourceSpecifier</code>.
   * @param aResourceClassName
   *          the name of a subclass of <code>Resource</code> that is to be instantiated from
   *          resource specifiers of the given class.
   * @throws ClassNotFoundException
   *           -
   */
  @SuppressWarnings("unchecked")
  public void addMapping(String aSpecifierInterfaceName, String aResourceClassName)
          throws ClassNotFoundException {
    addMapping((Class<? extends ResourceSpecifier>) Class.forName(aSpecifierInterfaceName),
            (Class<? extends Resource>) Class.forName(aResourceClassName));
  }
}
