/*

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

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import java.io.InputStream;

import java.lang.annotation.Annotation;

import java.lang.reflect.Constructor;

import java.lang.reflect.Method;

import java.util.ArrayList;

import java.util.List;

/**

 * Utility method library used to conveniently interact with <code>Class</code>es, such as acquiring them from the

 * application <code>ClassLoader</code>s and instantiating Objects from them.

 *

 * @since 0.1

 */

public class ClassUtils {

    //TODO - complete JavaDoc

    /**

     * Private internal log instance.

     */

    private static final Logger log = LoggerFactory.getLogger(ClassUtils.class);

    /**

     * @since 1.0

     */

    private static final ClassLoaderAccessor THREAD_CL_ACCESSOR = new ExceptionIgnoringAccessor() {

        @Override

        protected ClassLoader doGetClassLoader() throws Throwable {

            return Thread.currentThread().getContextClassLoader();

        }

    };

    /**

     * @since 1.0

     */

    private static final ClassLoaderAccessor CLASS_CL_ACCESSOR = new ExceptionIgnoringAccessor() {

        @Override

        protected ClassLoader doGetClassLoader() throws Throwable {

            return ClassUtils.class.getClassLoader();

        }

    };

    /**

     * @since 1.0

     */

    private static final ClassLoaderAccessor SYSTEM_CL_ACCESSOR = new ExceptionIgnoringAccessor() {

        @Override

        protected ClassLoader doGetClassLoader() throws Throwable {

            return ClassLoader.getSystemClassLoader();

        }

    };

    /**

     * Returns the specified resource by checking the current thread's

     * {@link Thread#getContextClassLoader() context class loader}, then the

     * current ClassLoader (<code>ClassUtils.class.getClassLoader()</code>), then the system/application

     * ClassLoader (<code>ClassLoader.getSystemClassLoader()</code>, in that order, using

     * {@link ClassLoader#getResourceAsStream(String) getResourceAsStream(name)}.

     *

     * @param name the name of the resource to acquire from the classloader(s).

     * @return the InputStream of the resource found, or <code>null</code> if the resource cannot be found from any

     *         of the three mentioned ClassLoaders.

     * @since 0.9

     */

    public static InputStream getResourceAsStream(String name) {

        InputStream is = THREAD_CL_ACCESSOR.getResourceStream(name);

        if (is == null) {

            if (log.isTraceEnabled()) {

                log.trace("Resource [" + name + "] was not found via the thread context ClassLoader.  Trying the " +

                        "current ClassLoader...");

            }

            is = CLASS_CL_ACCESSOR.getResourceStream(name);

        }

        if (is == null) {

            if (log.isTraceEnabled()) {

                log.trace("Resource [" + name + "] was not found via the current class loader.  Trying the " +

                        "system/application ClassLoader...");

            }

            is = SYSTEM_CL_ACCESSOR.getResourceStream(name);

        }

        if (is == null && log.isTraceEnabled()) {

            log.trace("Resource [" + name + "] was not found via the thread context, current, or " +

                    "system/application ClassLoaders.  All heuristics have been exhausted.  Returning null.");

        }

        return is;

    }

    /**

     * Attempts to load the specified class name from the current thread's

     * {@link Thread#getContextClassLoader() context class loader}, then the

     * current ClassLoader (<code>ClassUtils.class.getClassLoader()</code>), then the system/application

     * ClassLoader (<code>ClassLoader.getSystemClassLoader()</code>, in that order.  If any of them cannot locate

     * the specified class, an <code>UnknownClassException</code> is thrown (our RuntimeException equivalent of

     * the JRE's <code>ClassNotFoundException</code>.

     *

     * @param fqcn the fully qualified class name to load

     * @return the located class

     * @throws UnknownClassException if the class cannot be found.

     */

    public static Class forName(String fqcn) throws UnknownClassException {

        Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn);

        if (clazz == null) {

            if (log.isTraceEnabled()) {

                log.trace("Unable to load class named [" + fqcn +

                        "] from the thread context ClassLoader.  Trying the current ClassLoader...");

            }

            clazz = CLASS_CL_ACCESSOR.loadClass(fqcn);

        }

        if (clazz == null) {

            if (log.isTraceEnabled()) {

                log.trace("Unable to load class named [" + fqcn + "] from the current ClassLoader.  " +

                        "Trying the system/application ClassLoader...");

            }

            clazz = SYSTEM_CL_ACCESSOR.loadClass(fqcn);

        }

        if (clazz == null) {

            String msg = "Unable to load class named [" + fqcn + "] from the thread context, current, or " +

                    "system/application ClassLoaders.  All heuristics have been exhausted.  Class could not be found.";

            throw new UnknownClassException(msg);

        }

        return clazz;

    }

    public static boolean isAvailable(String fullyQualifiedClassName) {

        try {

            forName(fullyQualifiedClassName);

            return true;

        } catch (UnknownClassException e) {

            return false;

        }

    }

    public static Object newInstance(String fqcn) {

        return newInstance(forName(fqcn));

    }

    public static Object newInstance(String fqcn, Object... args) {

        return newInstance(forName(fqcn), args);

    }

    public static Object newInstance(Class clazz) {

        if (clazz == null) {

            String msg = "Class method parameter cannot be null.";

            throw new IllegalArgumentException(msg);

        }

        try {

            return clazz.newInstance();

        } catch (Exception e) {

            throw new InstantiationException("Unable to instantiate class [" + clazz.getName() + "]", e);

        }

    }

    public static Object newInstance(Class clazz, Object... args) {

        Class[] argTypes = new Class[args.length];

        for (int i = 0; i < args.length; i++) {

            argTypes[i] = args[i].getClass();

        }

        Constructor ctor = getConstructor(clazz, argTypes);

        return instantiate(ctor, args);

    }

    public static Constructor getConstructor(Class clazz, Class... argTypes) {

        try {

            return clazz.getConstructor(argTypes);

        } catch (NoSuchMethodException e) {

            throw new IllegalStateException(e);

        }

    }

    public static Object instantiate(Constructor ctor, Object... args) {

        try {

            return ctor.newInstance(args);

        } catch (Exception e) {

            String msg = "Unable to instantiate Permission instance with constructor [" + ctor + "]";

            throw new InstantiationException(msg, e);

        }

    }

    /**

     *

     * @param type

     * @param annotation

     * @return

     * @since 1.3

     */

    public static List<Method> getAnnotatedMethods(final Class<?> type, final Class<? extends Annotation> annotation) {

        final List<Method> methods = new ArrayList<Method>();

        Class<?> clazz = type;

        while (!Object.class.equals(clazz)) {

            Method[] currentClassMethods = clazz.getDeclaredMethods();

            for (final Method method : currentClassMethods) {

                if (annotation == null || method.isAnnotationPresent(annotation)) {

                    methods.add(method);

                }

            }

            // move to the upper class in the hierarchy in search for more methods

            clazz = clazz.getSuperclass();

        }

        return methods;

    }

    /**

     * @since 1.0

     */

    private static interface ClassLoaderAccessor {

        Class loadClass(String fqcn);

        InputStream getResourceStream(String name);

    }

    /**

     * @since 1.0

     */

    private static abstract class ExceptionIgnoringAccessor implements ClassLoaderAccessor {

        public Class loadClass(String fqcn) {

            Class clazz = null;

            ClassLoader cl = getClassLoader();

            if (cl != null) {

                try {

                    clazz = cl.loadClass(fqcn);

                } catch (ClassNotFoundException e) {

                    if (log.isTraceEnabled()) {

                        log.trace("Unable to load clazz named [" + fqcn + "] from class loader [" + cl + "]");

                    }

                }

            }

            return clazz;

        }

        public InputStream getResourceStream(String name) {

            InputStream is = null;

            ClassLoader cl = getClassLoader();

            if (cl != null) {

                is = cl.getResourceAsStream(name);

            }

            return is;

        }

        protected final ClassLoader getClassLoader() {

            try {

                return doGetClassLoader();

            } catch (Throwable t) {

                if (log.isDebugEnabled()) {

                    log.debug("Unable to acquire ClassLoader.", t);

                }

            }

            return null;

        }

        protected abstract ClassLoader doGetClassLoader() throws Throwable;

    }

}