//
//  ========================================================================
//  Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.annotations;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

import javax.annotation.Resource;
import javax.naming.InitialContext;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;

import org.eclipse.jetty.annotations.AnnotationIntrospector.AbstractIntrospectableAnnotationHandler;
import org.eclipse.jetty.plus.annotation.Injection;
import org.eclipse.jetty.plus.annotation.InjectionCollection;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.webapp.MetaData;
import org.eclipse.jetty.webapp.WebAppContext;

public class ResourceAnnotationHandler extends AbstractIntrospectableAnnotationHandler
{
    private static final Logger LOG = Log.getLogger(ResourceAnnotationHandler.class);
    
    protected static final List<Class<?>> ENV_ENTRY_TYPES = 
            Arrays.asList(new Class[] {String.class, Character.class, Integer.class, Boolean.class, Double.class, Byte.class, Short.class, Long.class, Float.class});
            

    protected WebAppContext _context;


    public ResourceAnnotationHandler (WebAppContext wac)
    {
        super(true);
        _context = wac;
    }


    /**
     *  Class level Resource annotations declare a name in the
     *  environment that will be looked up at runtime. They do
     *  not specify an injection.
     */
    @Override
    public void doHandle(Class<?> clazz)
    {
        if (supportsResourceInjection(clazz))
        {
            handleClass(clazz);

            Method[] methods = clazz.getDeclaredMethods();
            for (int i=0; i<methods.length; i++)
                handleMethod(clazz, methods[i]);
            Field[] fields = clazz.getDeclaredFields();
            //For each field, get all of it's annotations
            for (int i=0; i<fields.length; i++)
                handleField(clazz, fields[i]);
        }
    }

     public void handleClass (Class<?> clazz)
     {
         Resource resource = (Resource)clazz.getAnnotation(Resource.class);
         if (resource != null)
         {
             String name = resource.name();
             String mappedName = resource.mappedName();

             if (name==null || name.trim().equals(""))
                 throw new IllegalStateException ("Class level Resource annotations must contain a name (Common Annotations Spec Section 2.3)");

             try
             {
                 if (!org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_context, name,mappedName))
                     if (!org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_context.getServer(), name,mappedName))
                         throw new IllegalStateException("No resource at "+(mappedName==null?name:mappedName));
             }
             catch (NamingException e)
             {
                 LOG.warn(e);
             }
         }
    }

    public void handleField(Class<?> clazz, Field field)
    {
        Resource resource = (Resource)field.getAnnotation(Resource.class);
        if (resource != null)
        {
            //JavaEE Spec 5.2.3: Field cannot be static
            if (Modifier.isStatic(field.getModifiers()))
            {
                LOG.warn("Skipping Resource annotation on "+clazz.getName()+"."+field.getName()+": cannot be static");
                return;
            }

            //JavaEE Spec 5.2.3: Field cannot be final
            if (Modifier.isFinal(field.getModifiers()))
            {
                LOG.warn("Skipping Resource annotation on "+clazz.getName()+"."+field.getName()+": cannot be final");
                return;
            }

            //work out default name
            String name = clazz.getCanonicalName()+"/"+field.getName();

            //allow @Resource name= to override the field name
            name = (resource.name()!=null && !resource.name().trim().equals("")? resource.name(): name);
            String mappedName = (resource.mappedName()!=null && !resource.mappedName().trim().equals("")?resource.mappedName():null);
            //get the type of the Field
            Class<?> type = field.getType();

            //Servlet Spec 3.0 p. 76
            //If a descriptor has specified at least 1 injection target for this
            //resource, then it overrides this annotation
            MetaData metaData = _context.getMetaData();
            if (metaData.getOriginDescriptor("resource-ref."+name+".injection") != null)
            {
                //at least 1 injection was specified for this resource by a descriptor, so
                //it overrides this annotation
                return;
            }

            //No injections for this resource in any descriptors, so we can add it
            //Does the injection already exist?
            InjectionCollection injections = (InjectionCollection)_context.getAttribute(InjectionCollection.INJECTION_COLLECTION);
            if (injections == null)
            {
                injections = new InjectionCollection();
                _context.setAttribute(InjectionCollection.INJECTION_COLLECTION, injections);
            }
            Injection injection = injections.getInjection(name, clazz, field);
            if (injection == null)
            {
                //No injection has been specified, add it
                try
                {
                    boolean bound = org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_context, name, mappedName);
                    if (!bound)
                        bound = org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_context.getServer(), name, mappedName);
                    if (!bound)
                        bound =  org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(null, name, mappedName);
                    if (!bound)
                    {
                        //see if there is an env-entry value been bound
                        try
                        {
                            InitialContext ic = new InitialContext();
                            String nameInEnvironment = (mappedName!=null?mappedName:name);
                            ic.lookup("java:comp/env/"+nameInEnvironment);
                            bound = true;
                        }
                        catch (NameNotFoundException e)
                        {
                            bound = false;
                        }
                    }
                    //Check there is a JNDI entry for this annotation
                    if (bound)
                    {
                        LOG.debug("Bound "+(mappedName==null?name:mappedName) + " as "+ name);
                        //   Make the Injection for it if the binding succeeded
                        injection = new Injection();
                        injection.setTarget(clazz, field, type);
                        injection.setJndiName(name);
                        injection.setMappingName(mappedName);
                        injections.add(injection);

                        //TODO - an @Resource is equivalent to a resource-ref, resource-env-ref, message-destination
                        metaData.setOrigin("resource-ref."+name+".injection",resource,clazz);
                    }
                    else if (!isEnvEntryType(type))
                    {
                        //if this is an env-entry type resource and there is no value bound for it, it isn't
                        //an error, it just means that perhaps the code will use a default value instead
                        // JavaEE Spec. sec 5.4.1.3

                        throw new IllegalStateException("No resource at "+(mappedName==null?name:mappedName));
                    }
                }
                catch (NamingException e)
                {
                    //if this is an env-entry type resource and there is no value bound for it, it isn't
                    //an error, it just means that perhaps the code will use a default value instead
                    // JavaEE Spec. sec 5.4.1.3
                    if (!isEnvEntryType(type))
                        throw new IllegalStateException(e);
                }
            }
        }
    }


    /**
     * Process a Resource annotation on a Method.
     * <p>
     * This will generate a JNDI entry, and an Injection to be
     * processed when an instance of the class is created.
     * 
     * @param clazz the class to process 
     * @param method the method to process
     */
    public void handleMethod(Class<?> clazz, Method method)
    {

        Resource resource = (Resource)method.getAnnotation(Resource.class);
        if (resource != null)
        {
            /*
             * Commons Annotations Spec 2.3
             * " The Resource annotation is used to declare a reference to a resource.
             *   It can be specified on a class, methods or on fields. When the
             *   annotation is applied on a field or method, the container will
             *   inject an instance of the requested resource into the application
             *   when the application is initialized... Even though this annotation
             *   is not marked Inherited, if used all superclasses MUST be examined
             *   to discover all uses of this annotation. All such annotation instances
             *   specify resources that are needed by the application. Note that this
             *   annotation may appear on private fields and methods of the superclasses.
             *   Injection of the declared resources needs to happen in these cases as
             *   well, even if a method with such an annotation is overridden by a subclass."
             *
             *  Which IMHO, put more succinctly means "If you find a @Resource on any method
             *  or field, inject it!".
             */
            //JavaEE Spec 5.2.3: Method cannot be static
            if (Modifier.isStatic(method.getModifiers()))
            {
                LOG.warn("Skipping Resource annotation on "+clazz.getName()+"."+method.getName()+": cannot be static");
                return;
            }

            // Check it is a valid javabean: must be void return type, the name must start with "set" and it must have
            // only 1 parameter
            if (!method.getName().startsWith("set"))
            {
                LOG.warn("Skipping Resource annotation on "+clazz.getName()+"."+method.getName()+": invalid java bean, does not start with 'set'");
                return;
            }

            if (method.getParameterCount() != 1)
            {
                LOG.warn("Skipping Resource annotation on "+clazz.getName()+"."+method.getName()+": invalid java bean, not single argument to method");
                return;
            }

            if (Void.TYPE != method.getReturnType())
            {
                LOG.warn("Skipping Resource annotation on "+clazz.getName()+"."+method.getName()+": invalid java bean, not void");
                return;
            }


            //default name is the javabean property name
            String name = method.getName().substring(3);
            name = name.substring(0,1).toLowerCase(Locale.ENGLISH)+name.substring(1);
            name = clazz.getCanonicalName()+"/"+name;

            name = (resource.name()!=null && !resource.name().trim().equals("")? resource.name(): name);
            String mappedName = (resource.mappedName()!=null && !resource.mappedName().trim().equals("")?resource.mappedName():null);
            Class<?> paramType = method.getParameterTypes()[0];

            Class<?> resourceType = resource.type();

            //Servlet Spec 3.0 p. 76
            //If a descriptor has specified at least 1 injection target for this
            //resource, then it overrides this annotation
            MetaData metaData = _context.getMetaData();
            if (metaData.getOriginDescriptor("resource-ref."+name+".injection") != null)
            {
                //at least 1 injection was specified for this resource by a descriptor, so
                //it overrides this annotation
                return;
            }

            //check if an injection has already been setup for this target by web.xml
            InjectionCollection injections = (InjectionCollection)_context.getAttribute(InjectionCollection.INJECTION_COLLECTION);
            if (injections == null)
            {
                injections = new InjectionCollection();
                _context.setAttribute(InjectionCollection.INJECTION_COLLECTION, injections);
            }
            Injection injection = injections.getInjection(name, clazz, method, paramType);
            if (injection == null)
            {
                try
                {
                    //try binding name to environment
                    //try the webapp's environment first
                    boolean bound = org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_context, name, mappedName);

                    //try the server's environment
                    if (!bound)
                        bound = org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_context.getServer(), name, mappedName);

                    //try the jvm's environment
                    if (!bound)
                        bound = org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(null, name, mappedName);

                    //TODO if it is an env-entry from web.xml it can be injected, in which case there will be no
                    //NamingEntry, just a value bound in java:comp/env
                    if (!bound)
                    {
                        try
                        {
                            InitialContext ic = new InitialContext();
                            String nameInEnvironment = (mappedName!=null?mappedName:name);
                            ic.lookup("java:comp/env/"+nameInEnvironment);
                            bound = true;
                        }
                        catch (NameNotFoundException e)
                        {
                            bound = false;
                        }
                    }

                    if (bound)
                    {
                        LOG.debug("Bound "+(mappedName==null?name:mappedName) + " as "+ name);
                        //   Make the Injection for it
                        injection = new Injection();
                        injection.setTarget(clazz, method,paramType,resourceType);
                        injection.setJndiName(name);
                        injection.setMappingName(mappedName);
                        injections.add(injection);
                        //TODO - an @Resource is equivalent to a resource-ref, resource-env-ref, message-destination
                        metaData.setOrigin("resource-ref."+name+".injection",resource,clazz);
                    }
                    else if (!isEnvEntryType(paramType))
                    {

                        //if this is an env-entry type resource and there is no value bound for it, it isn't
                        //an error, it just means that perhaps the code will use a default value instead
                        // JavaEE Spec. sec 5.4.1.3
                        throw new IllegalStateException("No resource at "+(mappedName==null?name:mappedName));
                    }
                }
                catch (NamingException e)
                {
                    //if this is an env-entry type resource and there is no value bound for it, it isn't
                    //an error, it just means that perhaps the code will use a default value instead
                    // JavaEE Spec. sec 5.4.1.3
                    if (!isEnvEntryType(paramType))
                        throw new IllegalStateException(e);
                }
            }

        }
    }
    
    /**
     * Check if the given Class is one that the specification allows to have a Resource annotation.
     * 
     * @param c the class
     * @return true if Resource annotation permitted, false otherwise
     */
    public boolean supportsResourceInjection (Class<?> c)
    {
        if (javax.servlet.Servlet.class.isAssignableFrom(c) ||
                javax.servlet.Filter.class.isAssignableFrom(c) || 
                javax.servlet.ServletContextListener.class.isAssignableFrom(c) ||
                javax.servlet.ServletContextAttributeListener.class.isAssignableFrom(c) ||
                javax.servlet.ServletRequestListener.class.isAssignableFrom(c) ||
                javax.servlet.ServletRequestAttributeListener.class.isAssignableFrom(c) ||
                javax.servlet.http.HttpSessionListener.class.isAssignableFrom(c) ||
                javax.servlet.http.HttpSessionAttributeListener.class.isAssignableFrom(c) ||
                javax.servlet.http.HttpSessionIdListener.class.isAssignableFrom(c) ||
                javax.servlet.AsyncListener.class.isAssignableFrom(c) ||
                javax.servlet.http.HttpUpgradeHandler.class.isAssignableFrom(c))
            return true;
        
        return false;
    }
    
    
    /**
     * Check if the class is one of the basic java types permitted as 
     * env-entries.
     * @param clazz the class to check
     * @return true if class is permitted by the spec to be an env-entry value
     */
    public boolean isEnvEntryType (Class<?> clazz)
    {
        return ENV_ENTRY_TYPES.contains(clazz);
    }
}
