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

import jakarta.annotation.Resource;
import jakarta.annotation.Resources;
import jakarta.annotation.security.DeclareRoles;
import jakarta.annotation.security.RunAs;
import jakarta.servlet.ServletSecurityElement;
import jakarta.servlet.annotation.ServletSecurity;

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

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

    /**
     * Constructs a new WebAnnotationSet.
     */
    public WebAnnotationSet() {
    }

    private static final String SEPARATOR = "/";
    private static final String MAPPED_NAME_PROPERTY = "mappedName";


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


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

    /**
     * Process the annotations on a context.
     *
     * @param context The context which will have its annotations processed
     */
    public static void loadApplicationAnnotations(Context context) {
        if (!context.getMetadataComplete()) {
            loadApplicationListenerAnnotations(context);
            loadApplicationFilterAnnotations(context);
            loadApplicationServletAnnotations(context);
        }
    }


    // ------------------------------------------------------- Protected Methods

    /**
     * Process the annotations for the listeners.
     *
     * @param context The context which will have its annotations processed
     */
    protected static void loadApplicationListenerAnnotations(Context context) {
        String[] applicationListeners = context.findApplicationListeners();
        for (String className : applicationListeners) {
            Class<?> clazz = Introspection.loadClass(context, className);
            if (clazz == null) {
                continue;
            }

            loadClassAnnotation(context, clazz);
            loadFieldsAnnotation(context, clazz);
            loadMethodsAnnotation(context, clazz);
        }
    }


    /**
     * Process the annotations for the filters.
     *
     * @param context The context which will have its annotations processed
     */
    protected static void loadApplicationFilterAnnotations(Context context) {
        FilterDef[] filterDefs = context.findFilterDefs();
        for (FilterDef filterDef : filterDefs) {
            Class<?> clazz = Introspection.loadClass(context, filterDef.getFilterClass());
            if (clazz == null) {
                continue;
            }

            loadClassAnnotation(context, clazz);
            loadFieldsAnnotation(context, clazz);
            loadMethodsAnnotation(context, clazz);
        }
    }


    /**
     * Process the annotations for the servlets.
     *
     * @param context The context which will have its annotations processed
     */
    protected static void loadApplicationServletAnnotations(Context context) {

        Container[] children = context.findChildren();
        for (Container child : children) {
            if (child instanceof Wrapper wrapper) {

                if (wrapper.getServletClass() == null) {
                    continue;
                }

                Class<?> clazz = Introspection.loadClass(context, wrapper.getServletClass());
                if (clazz == null) {
                    continue;
                }

                loadClassAnnotation(context, clazz);
                loadFieldsAnnotation(context, clazz);
                loadMethodsAnnotation(context, clazz);

                /*
                 * Process RunAs annotation which can be only on servlets. Ref JSR 250, equivalent to the run-as element
                 * in the deployment descriptor
                 */
                RunAs runAs = clazz.getAnnotation(RunAs.class);
                if (runAs != null) {
                    wrapper.setRunAs(runAs.value());
                }

                // Process ServletSecurity annotation
                ServletSecurity servletSecurity = clazz.getAnnotation(ServletSecurity.class);
                if (servletSecurity != null) {
                    context.addServletSecurity(new ApplicationServletRegistration(wrapper, context),
                            new ServletSecurityElement(servletSecurity));
                }
            }
        }
    }


    /**
     * Process the annotations on a context for a given className.
     *
     * @param context The context which will have its annotations processed
     * @param clazz   The class to examine for Servlet annotations
     */
    protected static void loadClassAnnotation(Context context, Class<?> clazz) {
        /*
         * Process Resource annotation. Ref JSR 250
         */
        Resource resourceAnnotation = clazz.getAnnotation(Resource.class);
        if (resourceAnnotation != null) {
            addResource(context, resourceAnnotation);
        }
        /*
         * Process Resources annotation. Ref JSR 250
         */
        Resources resourcesAnnotation = clazz.getAnnotation(Resources.class);
        if (resourcesAnnotation != null && resourcesAnnotation.value() != null) {
            for (Resource resource : resourcesAnnotation.value()) {
                addResource(context, resource);
            }
        }
        /*
         * Process DeclareRoles annotation. Ref JSR 250, equivalent to the security-role element in the deployment
         * descriptor
         */
        DeclareRoles declareRolesAnnotation = clazz.getAnnotation(DeclareRoles.class);
        if (declareRolesAnnotation != null && declareRolesAnnotation.value() != null) {
            for (String role : declareRolesAnnotation.value()) {
                context.addSecurityRole(role);
            }
        }
    }


    /**
     * Processes @Resource annotations on fields of the given class.
     *
     * @param context The context to add resources to
     * @param clazz   The class to scan for field annotations
     */
    protected static void loadFieldsAnnotation(Context context, Class<?> clazz) {
        // Initialize the annotations
        for (Field field : clazz.getDeclaredFields()) {
            Resource annotation = field.getAnnotation(Resource.class);
            if (annotation != null) {
                String defaultName = clazz.getName() + SEPARATOR + field.getName();
                Class<?> defaultType = field.getType();
                addResource(context, annotation, defaultName, defaultType);
            }
        }
    }


    /**
     * Processes @Resource annotations on methods of the given class.
     *
     * @param context The context to add resources to
     * @param clazz   The class to scan for method annotations
     */
    protected static void loadMethodsAnnotation(Context context, Class<?> clazz) {
        // Initialize the annotations
        for (Method method : clazz.getDeclaredMethods()) {
            Resource annotation = method.getAnnotation(Resource.class);
            if (annotation != null) {
                if (!Introspection.isValidSetter(method)) {
                    throw new IllegalArgumentException(sm.getString("webAnnotationSet.invalidInjection"));
                }

                String defaultName = clazz.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.
     *
     * @param context    The context which will have its annotations processed
     * @param annotation The annotation that was found
     */
    protected static void addResource(Context context, Resource annotation) {
        addResource(context, annotation, null, null);
    }


    /**
     * Processes a Resource annotation with default name and type.
     *
     * @param context      The context to add resources to
     * @param annotation   The annotation
     * @param defaultName  The default resource name
     * @param defaultType  The default resource type
     */
    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-entry element
            ContextEnvironment resource = new ContextEnvironment();

            resource.setName(name);
            resource.setType(type);
            resource.setDescription(annotation.description());
            resource.setProperty(MAPPED_NAME_PROPERTY, annotation.mappedName());
            resource.setLookupName(annotation.lookup());

            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());
            service.setLookupName(annotation.lookup());

            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("jakarta.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(MAPPED_NAME_PROPERTY, annotation.mappedName());
            resource.setDescription(annotation.description());
            resource.setLookupName(annotation.lookup());

            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());
            resource.setLookupName(annotation.lookup());

            context.getNamingResources().addMessageDestinationRef(resource);

        } else {
            /*
             * General case. Also used for: - javax.resource.cci.InteractionSpec - jakarta.transaction.UserTransaction
             */

            // resource-env-ref
            ContextResourceEnvRef resource = new ContextResourceEnvRef();

            resource.setName(name);
            resource.setType(type);
            resource.setProperty(MAPPED_NAME_PROPERTY, annotation.mappedName());
            resource.setDescription(annotation.description());
            resource.setLookupName(annotation.lookup());

            context.getNamingResources().addResourceEnvRef(resource);
        }
    }


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


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