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

import java.lang.reflect.Field;
import java.lang.reflect.Method;

import javax.annotation.Resource;
import javax.annotation.Resources;
import javax.annotation.security.DeclareRoles;
import javax.annotation.security.RunAs;

import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.deploy.ContextEnvironment;
import org.apache.catalina.deploy.ContextResource;
import org.apache.catalina.deploy.ContextResourceEnvRef;
import org.apache.catalina.deploy.ContextService;
import org.apache.catalina.deploy.FilterDef;
import org.apache.catalina.deploy.MessageDestinationRef;
import org.apache.catalina.util.Introspection;
import org.apache.tomcat.util.res.StringManager;

/**
 * <p><strong>AnnotationSet</strong> for processing the annotations of the web application
 * classes (<code>/WEB-INF/classes</code> and <code>/WEB-INF/lib</code>).</p>
 *
 * @author Fabien Carrion
 */
public class WebAnnotationSet {

    private static final String SEPARATOR = "/";

    /**
     * The string resources for this package.
     */
    protected static final StringManager sm =
        StringManager.getManager(Constants.Package);


    // --------------------------------------------------------- Public Methods

    /**
     * Process the annotations on a context.
     */
    public static void loadApplicationAnnotations(Context context) {
        
        loadApplicationListenerAnnotations(context);
        loadApplicationFilterAnnotations(context);
        loadApplicationServletAnnotations(context);
        
        
    }
    
    
    // -------------------------------------------------------- protected Methods
    
    
    /**
     * Process the annotations for the listeners.
     */
    protected static void loadApplicationListenerAnnotations(Context context) {
        Class<?> classClass = null;
        String[] applicationListeners =
                context.findApplicationListeners();
        for (int i = 0; i < applicationListeners.length; i++) {
            classClass = Introspection.loadClass(context,
                    applicationListeners[i]);
            if (classClass == null) {
                continue;
            }

            loadClassAnnotation(context, classClass);
            loadFieldsAnnotation(context, classClass);
            loadMethodsAnnotation(context, classClass);
        }
    }
    
    
    /**
     * Process the annotations for the filters.
     */
    protected static void loadApplicationFilterAnnotations(Context context) {
        Class<?> classClass = null;
        FilterDef[] filterDefs = context.findFilterDefs();
        for (int i = 0; i < filterDefs.length; i++) {
            classClass = Introspection.loadClass(context,
                    (filterDefs[i]).getFilterClass());
            if (classClass == null) {
                continue;
            }

            loadClassAnnotation(context, classClass);
            loadFieldsAnnotation(context, classClass);
            loadMethodsAnnotation(context, classClass);
        }
    }
    
    
    /**
     * Process the annotations for the servlets.
     */
    protected static void loadApplicationServletAnnotations(Context context) {
        
        Wrapper wrapper = null;
        Class<?> classClass = null;
        
        Container[] children = context.findChildren();
        for (int i = 0; i < children.length; i++) {
            if (children[i] instanceof Wrapper) {
                
                wrapper = (Wrapper) children[i];
                if (wrapper.getServletClass() == null) {
                    continue;
                }
                
                classClass = Introspection.loadClass(context,
                        wrapper.getServletClass());
                if (classClass == null) {
                    continue;
                }
                
                loadClassAnnotation(context, classClass);
                loadFieldsAnnotation(context, classClass);
                loadMethodsAnnotation(context, classClass);

                /* Process RunAs annotation which can be only on servlets.
                 * Ref JSR 250, equivalent to the run-as element in
                 * the deployment descriptor
                 */
                RunAs annotation = classClass.getAnnotation(RunAs.class);
                if (annotation != null) {
                    wrapper.setRunAs(annotation.value());
                }
            }
        }
        
        
    }
    
    
    /**
     * Process the annotations on a context for a given className.
     */
    protected static void loadClassAnnotation(Context context,
            Class<?> classClass) {
        /* Process Resource annotation.
         * Ref JSR 250
         */
        {
            Resource annotation = classClass.getAnnotation(Resource.class);
            if (annotation != null) {
                addResource(context, annotation);
            }
        }
        /* Process Resources annotation.
         * Ref JSR 250
         */
        {
            Resources annotation = classClass.getAnnotation(Resources.class);
            if (annotation != null && annotation.value() != null) {
                for (Resource resource : annotation.value()) {
                    addResource(context, resource);
                }
            }
        }
        /* Process EJB annotation.
         * Ref JSR 224, equivalent to the ejb-ref or ejb-local-ref
         * element in the deployment descriptor.
        {
            EJB annotation = classClass.getAnnotation(EJB.class);
            if (annotation != null) {

                if ((annotation.mappedName().length() == 0)
                        || annotation.mappedName().equals("Local")) {

                    ContextLocalEjb ejb = new ContextLocalEjb();

                    ejb.setName(annotation.name());
                    ejb.setType(annotation.beanInterface().getCanonicalName());
                    ejb.setDescription(annotation.description());

                    ejb.setHome(annotation.beanName());

                    context.getNamingResources().addLocalEjb(ejb);

                } else if (annotation.mappedName().equals("Remote")) {

                    ContextEjb ejb = new ContextEjb();

                    ejb.setName(annotation.name());
                    ejb.setType(annotation.beanInterface().getCanonicalName());
                    ejb.setDescription(annotation.description());

                    ejb.setHome(annotation.beanName());

                    context.getNamingResources().addEjb(ejb);

                }
            }
        }
        */
        /* Process WebServiceRef annotation.
         * Ref JSR 224, equivalent to the service-ref element in 
         * the deployment descriptor.
         * The service-ref registration is not implemented
        {
            WebServiceRef annotation = classClass
                    .getAnnotation(WebServiceRef.class);
            if (annotation != null) {
                ContextService service = new ContextService();

                service.setName(annotation.name());
                service.setWsdlfile(annotation.wsdlLocation());

                service.setType(annotation.type().getCanonicalName());

                if (annotation.value() == null)
                    service.setServiceinterface(annotation.type()
                            .getCanonicalName());

                if (annotation.type().getCanonicalName().equals("Service"))
                    service.setServiceinterface(annotation.type()
                            .getCanonicalName());

                if (annotation.value().getCanonicalName().equals("Endpoint"))
                    service.setServiceendpoint(annotation.type()
                            .getCanonicalName());

                service.setPortlink(annotation.type().getCanonicalName());

                context.getNamingResources().addService(service);
            }
        }
        */
        /* Process DeclareRoles annotation.
         * Ref JSR 250, equivalent to the security-role element in
         * the deployment descriptor
         */
        {
            DeclareRoles annotation = classClass
                    .getAnnotation(DeclareRoles.class);
            if (annotation != null && annotation.value() != null) {
                for (String role : annotation.value()) {
                    context.addSecurityRole(role);
                }
            }
        }
    }


    protected static void loadFieldsAnnotation(Context context,
            Class<?> classClass) {
        // Initialize the annotations
        Field[] fields = Introspection.getDeclaredFields(classClass);
        if (fields != null && fields.length > 0) {
            for (Field field : fields) {
                Resource annotation = field.getAnnotation(Resource.class);
                if (annotation != null) {
                    String defaultName = classClass.getName() + SEPARATOR + field.getName();
                    Class<?> defaultType = field.getType();
                    addResource(context, annotation, defaultName, defaultType);
                }
            }
        }
    }
    
    
    protected static void loadMethodsAnnotation(Context context,
            Class<?> classClass) {
        // Initialize the annotations
        Method[] methods = Introspection.getDeclaredMethods(classClass);
        if (methods != null && methods.length > 0) {
            for (Method method : methods) {
                Resource annotation = method.getAnnotation(Resource.class);
                if (annotation != null) {
                    if (!Introspection.isValidSetter(method)) {
                        throw new IllegalArgumentException(sm.getString(
                                "webAnnotationSet.invalidInjection"));
                    }

                    String defaultName = classClass.getName() + SEPARATOR +
                            Introspection.getPropertyName(method);

                    Class<?> defaultType =
                            (method.getParameterTypes()[0]);
                    addResource(context, annotation, defaultName, defaultType);
                }
            }
        }
    }

    /**
     * Process a Resource annotation to set up a Resource.
     * Ref JSR 250, equivalent to the resource-ref,
     * message-destination-ref, env-ref, resource-env-ref
     * or service-ref element in the deployment descriptor.
     */
    protected static void addResource(Context context, Resource annotation) {
        addResource(context, annotation, null, null);
    }

    protected static void addResource(Context context, Resource annotation,
            String defaultName, Class<?> defaultType) {
        String name = getName(annotation, defaultName);
        String type = getType(annotation, defaultType);
            
        if (type.equals("java.lang.String") ||
                type.equals("java.lang.Character") ||
                type.equals("java.lang.Integer") ||
                type.equals("java.lang.Boolean") ||
                type.equals("java.lang.Double") ||
                type.equals("java.lang.Byte") ||
                type.equals("java.lang.Short") ||
                type.equals("java.lang.Long") ||
                type.equals("java.lang.Float")) {

            // env-ref element
            ContextEnvironment resource = new ContextEnvironment();
            
            resource.setName(name);
            resource.setType(type);
            
            resource.setDescription(annotation.description());
            
            resource.setValue(annotation.mappedName());
            
            context.getNamingResources().addEnvironment(resource);
            
        } else if (type.equals("javax.xml.rpc.Service")) {
            
            // service-ref element
            ContextService service = new ContextService();
            
            service.setName(name);
            service.setWsdlfile(annotation.mappedName());
            
            service.setType(type);
            service.setDescription(annotation.description());
            
            context.getNamingResources().addService(service);
            
        } else if (type.equals("javax.sql.DataSource") ||
                type.equals("javax.jms.ConnectionFactory") ||
                type.equals("javax.jms.QueueConnectionFactory") ||
                type.equals("javax.jms.TopicConnectionFactory") ||
                type.equals("javax.mail.Session") ||
                type.equals("java.net.URL") ||
                type.equals("javax.resource.cci.ConnectionFactory") ||
                type.equals("org.omg.CORBA_2_3.ORB") ||
                type.endsWith("ConnectionFactory")) {
            
            // resource-ref element
            ContextResource resource = new ContextResource();
            
            resource.setName(name);
            resource.setType(type);
            
            if (annotation.authenticationType()
                    == Resource.AuthenticationType.CONTAINER) {
                resource.setAuth("Container");
            } else if (annotation.authenticationType()
                    == Resource.AuthenticationType.APPLICATION) {
                resource.setAuth("Application");
            }
            
            resource.setScope(annotation.shareable() ? "Shareable" : "Unshareable");
            resource.setProperty("mappedName", annotation.mappedName());
            resource.setDescription(annotation.description());
            
            context.getNamingResources().addResource(resource);
            
        } else if (type.equals("javax.jms.Queue") ||
                type.equals("javax.jms.Topic")) {
            
            // message-destination-ref
            MessageDestinationRef resource = new MessageDestinationRef();
            
            resource.setName(name);
            resource.setType(type);
            
            resource.setUsage(annotation.mappedName());
            resource.setDescription(annotation.description());
            
            context.getNamingResources().addMessageDestinationRef(resource);
            
        } else if (type.equals("javax.resource.cci.InteractionSpec") ||
                type.equals("javax.transaction.UserTransaction") ||
                true) {
            
            // resource-env-ref
            ContextResourceEnvRef resource = new ContextResourceEnvRef();
            
            resource.setName(name);
            resource.setType(type);
            
            resource.setProperty("mappedName", annotation.mappedName());
            resource.setDescription(annotation.description());
            
            context.getNamingResources().addResourceEnvRef(resource);
            
        }
    }


    private static String getType(Resource annotation, Class<?> defaultType) {
        Class<?> type = annotation.type();
        if (type == null || type.equals(Object.class)) {
            if (defaultType != null) {
                type = defaultType;
            }
        }
        return Introspection.convertPrimitiveType(type).getCanonicalName();
    }


    private static String getName(Resource annotation, String defaultName) {
        String name = annotation.name();
        if (name == null || name.equals("")) {
            if (defaultName != null) {
                name = defaultName;
            }
        }
        return name;
    }
}
