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

import java.net.URL;

//@formatter:off
/**
 * Class Loader for loading localized messages
 * See https://issues.apache.org/jira/browse/UIMA-1714
 * Delegates to other ClassLoaders, in the following order
 *   the class loader that loaded the 2nd previous caller
 *   the class loader that loaded the 3rd previous caller
 *   etc.
 *   and finally, the thread local context loader, if it exists UIMA-3692
 * Note: the caller of this method is presumed to be framework code
 *   that was, in turn, called to perform some logging or whatever,
 *   so we skip the 1st previous caller.
 * 
 * Note that each of these class loaders will, in turn, delegate if they are set up to do so
 * 
 * Note: if a caller's class loader is the same as the previously tried one, we skip it (simple
 * speed optimization, and may avoid some kind of strange loop)
 */
//@formatter:on
public class MsgLocalizationClassLoader {

  /**
   * this inner class only for purposes of getting access to the protected method to get the call
   * stack
   */
  static class CallStack extends SecurityManager {
    Class<?>[] getCallStack() {
      return getClassContext();
    }
  }

  // static final CallStack csi = new CallStack(); // not used 2/2018

  /**
   * One instance of this class made Must be thread-safe
   */
  static class CallClimbingClassLoader extends ClassLoader {

    /**
     * This value is set / cleared in a try / finally block, from the value of
     * originalContextClassLoader in the two classes
     * org.apache.uima.InternationalizedRuntimeException and
     * org.apache.uima.InternationalizedException
     * 
     * The purpose is to enable using the class loader from the context where the exception was
     * created.
     */
    static final ThreadLocal<ClassLoader> original_thread_context_class_loader = new ThreadLocal<>();

    /*
     * Try to load the class itself before delegate the class loading to its parent
     */
    @Override
    protected synchronized Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
      // First, check if the class has already been loaded
      Class<?> c = findLoadedClass(name);
      if (c == null) {
        // try to load class
        c = findClass(name); // may throw; if so, don't delegate, delegation has already been done
      }
      if (resolve) {
        resolveClass(c);
      }
      return c;
    }

    /**
     * Called after findLoadedClass has returned null Delegates loading in specific order
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

      // UIMA-3692, UIMA-4793 try the thread context class loader
      // if not found, will return class not found exception

      ClassLoader cl = original_thread_context_class_loader.get();
      if (cl != null) {
        try {
          return cl.loadClass(name);
        } catch (ClassNotFoundException e) {
        }
      }

      // 2nd try: the current thread context class loader
      ClassLoader context_classLoader = Thread.currentThread().getContextClassLoader();
      if (null != context_classLoader && cl != context_classLoader) {
        try {
          return context_classLoader.loadClass(name);
        } catch (ClassNotFoundException e) {
        }
      }

      // get the call stack; "new" is needed to get the current context call stack
      ClassLoader[] cls = Misc.getCallingClass_classLoaders();

      for (ClassLoader ccl : cls) {
        if (ccl == cl || ccl == context_classLoader) {
          continue; // alread tried
        }
        Class<?> c = null;
        try {
          c = ccl.loadClass(name); // include delegation
          return c;
        } catch (ClassNotFoundException e) {
        }
      } // end of for loop, looking through all the super classes

      throw new ClassNotFoundException(name);
    }

    @Override
    public URL getResource(String name) {

      // UIMA-3692, UIMA-4793 try the thread context class loader
      // if not found, will return class not found exception

      ClassLoader cl = original_thread_context_class_loader.get();
      if (cl != null) {
        URL c = cl.getResource(name);
        if (null != c) {
          return c;
        }
      }

      // 2nd try: the current thread context class loader
      ClassLoader context_classLoader = Thread.currentThread().getContextClassLoader();
      if (null != context_classLoader && cl != context_classLoader) {
        URL c = context_classLoader.getResource(name);
        if (null != c) {
          return c;
        }
      }

      ClassLoader[] cls = Misc.getCallingClass_classLoaders();

      for (ClassLoader ccl : cls) {
        if (ccl == cl || ccl == context_classLoader) {
          continue; // alread tried
        }
        URL c = ccl.getResource(name);
        if (null != c) {
          return c;
        }
      } // end of for loop, looking through all the super classes

      return null;

      // Map<ClassLoader,ClassLoader> alreadySearched = new IdentityHashMap<>(7);
      // // get the call stack
      // Class<?>[] cs = new CallStack().getCallStack();
      // // start at the caller of the caller's class loader
      // // cs[0] is getClassContext
      // // cs[1] is getCallStack
      // // cs[2] is this method, find class
      // for (int i = 3; i < cs.length; i++) {
      // Class<?> callingClass = cs[i];
      // ClassLoader cl = callingClass.getClassLoader();
      // if (null == cl) { // means system class loader
      // cl = ClassLoader.getSystemClassLoader();
      // }
      // if (null != alreadySearched.get(cl)) {
      // continue;
      // }
      // alreadySearched.put(cl, cl);
      //
      // URL c = cl.getResource(name); // include delegation
      // if (null != c) {
      // return c;
      // }
      // }
      // // UIMA-3692, UIMA-4793 try the thread context class loader
      // // if not found, will return class not found exception
      // ClassLoader cl = original_thread_context_class_loader.get();
      // if (cl != null) {
      // URL c = cl.getResource(name);
      // if (null != c) {
      // return c;
      // }
      // }
      // return Thread.currentThread().getContextClassLoader().getResource(name);
      // }
    }
  }

  private static final CallClimbingClassLoader MSG_LOCALIZATION_CLASS_LOADER = new CallClimbingClassLoader();

  public static URL getResource(String name) {
    return MSG_LOCALIZATION_CLASS_LOADER.getResource(name);
  }

  public static ClassLoader getMsgLocalizationClassLoader() {
    return MSG_LOCALIZATION_CLASS_LOADER;
  }

  public static Class<?> loadClass(String name) throws ClassNotFoundException {
    return MSG_LOCALIZATION_CLASS_LOADER.loadClass(name);
  }
}
