/*
 * 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.openejb.config.rules;

import org.apache.openejb.OpenEJBException;
import org.apache.openejb.OpenEJBRuntimeException;
import org.apache.openejb.config.EjbModule;
import org.apache.openejb.dyni.DynamicSubclass;
import org.apache.openejb.jee.EnterpriseBean;
import org.apache.openejb.jee.EntityBean;
import org.apache.openejb.jee.Interceptor;
import org.apache.openejb.jee.RemoteBean;
import org.apache.openejb.jee.SessionBean;
import org.apache.openejb.util.Messages;
import org.apache.openejb.util.Strings;
import org.apache.openejb.util.proxy.DynamicProxyImplFactory;
import org.apache.xbean.finder.ClassFinder;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import javax.annotation.Resources;
import javax.annotation.security.DeclareRoles;
import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.annotation.security.RunAs;
import javax.ejb.EJB;
import javax.ejb.EJBHome;
import javax.ejb.EJBLocalHome;
import javax.ejb.EJBLocalObject;
import javax.ejb.EJBObject;
import javax.ejb.EJBs;
import javax.ejb.Init;
import javax.ejb.Local;
import javax.ejb.PostActivate;
import javax.ejb.PrePassivate;
import javax.ejb.Remote;
import javax.ejb.Remove;
import javax.ejb.Timeout;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionManagement;
import javax.jws.WebService;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import static java.lang.reflect.Modifier.isAbstract;

/**
 * @version $Rev$ $Date$
 */
public class CheckClasses extends ValidationBase {

    private static final List<Class<? extends Annotation>> beanOnlyAnnotations = new ArrayList<Class<? extends Annotation>>();

    static {
        beanOnlyAnnotations.add(PostConstruct.class);
        beanOnlyAnnotations.add(PreDestroy.class);
        beanOnlyAnnotations.add(Resource.class);
        beanOnlyAnnotations.add(Resources.class);
        beanOnlyAnnotations.add(DeclareRoles.class);
        beanOnlyAnnotations.add(DenyAll.class);
        beanOnlyAnnotations.add(PermitAll.class);
        beanOnlyAnnotations.add(RolesAllowed.class);
        beanOnlyAnnotations.add(RunAs.class);

        beanOnlyAnnotations.add(EJB.class);
        beanOnlyAnnotations.add(EJBs.class);
        beanOnlyAnnotations.add(Init.class);
        beanOnlyAnnotations.add(PostActivate.class);
        beanOnlyAnnotations.add(PrePassivate.class);
        beanOnlyAnnotations.add(Remove.class);
        beanOnlyAnnotations.add(Timeout.class);
        beanOnlyAnnotations.add(TransactionAttribute.class);
        beanOnlyAnnotations.add(TransactionManagement.class);
    }

    public void validate(final EjbModule ejbModule) {
        final ClassLoader loader = ejbModule.getClassLoader();
        for (final EnterpriseBean bean : ejbModule.getEjbJar().getEnterpriseBeans()) {
            try {
                final Class<?> beanClass = check_hasEjbClass(loader, bean);

                // All the subsequent checks require the bean class
                if (beanClass == null) {
                    continue;
                }

                if (!(bean instanceof RemoteBean)) {
                    continue;
                }

                if (bean instanceof SessionBean && ((SessionBean) bean).getProxy() != null) {
                    continue;
                }

                final RemoteBean b = (RemoteBean) bean;

                check_isEjbClass(b);
                check_hasDependentClasses(b, b.getEjbClass(), "ejb-class");
                check_hasInterface(b);

                if (b.getRemote() != null) {
                    checkInterface(loader, b, beanClass, "remote", b.getRemote());
                }

                if (b.getHome() != null) {
                    checkInterface(loader, b, beanClass, "home", b.getHome());
                }

                if (b.getLocal() != null) {
                    checkInterface(loader, b, beanClass, "local", b.getLocal());
                }

                if (b.getLocalHome() != null) {
                    checkInterface(loader, b, beanClass, "local-home", b.getLocalHome());
                }

                if (b instanceof SessionBean) {
                    final SessionBean sessionBean = (SessionBean) b;

                    for (final String interfce : sessionBean.getBusinessLocal()) {
                        checkInterface(loader, b, beanClass, "business-local", interfce);
                    }

                    for (final String interfce : sessionBean.getBusinessRemote()) {
                        checkInterface(loader, b, beanClass, "business-remote", interfce);
                    }
                }
            } catch (final RuntimeException e) {
                throw new OpenEJBRuntimeException(bean.getEjbName(), e);
            }
        }

        for (final Interceptor interceptor : ejbModule.getEjbJar().getInterceptors()) {
            check_hasInterceptorClass(loader, interceptor);
        }
    }

    private void checkInterface(final ClassLoader loader, final RemoteBean b, final Class<?> beanClass, String tag, final String className) {
        final Class<?> interfce = lookForClass(loader, className, tag, b.getEjbName());

        if (interfce == null) {
            return;
        }

        check_hasDependentClasses(b, className, tag);

        tag = Strings.lcfirst(Strings.camelCase(tag));

        isValidInterface(b, interfce, beanClass, tag);

        final ClassFinder finder = new ClassFinder(interfce);

        for (final Class<? extends Annotation> annotation : beanOnlyAnnotations) {

            if (interfce.isAnnotationPresent(annotation)) {
                warn(b, "interface.beanOnlyAnnotation", annotation.getSimpleName(), interfce.getName(), b.getEjbClass());
            }

            for (final Method method : finder.findAnnotatedMethods(annotation)) {
                warn(b, "interfaceMethod.beanOnlyAnnotation", annotation.getSimpleName(), interfce.getName(), method.getName(), b.getEjbClass());
            }
        }

    }

    private void check_hasInterface(final RemoteBean b) {
        if (b.getRemote() != null) {
            return;
        }
        if (b.getLocal() != null) {
            return;
        }

        Class<?> beanClass = null;
        try {
            beanClass = loadClass(b.getEjbClass());
        } catch (final OpenEJBException e) {
            // no-op
        }

        if (b instanceof EntityBean) {
            fail(b, "noInterfaceDeclared.entity", beanClass.getSimpleName());
            return;
        }

        if (b.getBusinessLocal().size() > 0) {
            return;
        }
        if (b.getBusinessRemote().size() > 0) {
            return;
        }

        if (((SessionBean) b).getServiceEndpoint() != null) {
            return;
        }

        if (beanClass.isAnnotationPresent(WebService.class)) {
            return;
        }

        //fail(b, "noInterfaceDeclared.session");
    }

    private void check_hasDependentClasses(final RemoteBean b, final String className, final String type) {
        try {
            final ClassLoader cl = module.getClassLoader();
            final Class<?> clazz = cl.loadClass(className);
            for (final Object item : clazz.getFields()) {
                item.toString();
            }
            for (final Object item : clazz.getMethods()) {
                item.toString();
            }
            for (final Object item : clazz.getConstructors()) {
                item.toString();
            }
            for (final Object item : clazz.getAnnotations()) {
                item.toString();
            }
            // checking for any declared enum constants
            for (final Class klass : clazz.getClasses()) {
                if (klass.isEnum()) {
                    klass.toString();
                }
            }
        } catch (final ClassNotFoundException | NoClassDefFoundError e) {
            /*
            # 0 - Referring Class name
            # 1 - Dependent Class name
            # 2 - Element (home, ejb-class, remote)
            # 3 - Bean name
            */
            final String missingClass = e.getMessage();
            fail(b, "missing.dependent.class", className, missingClass, type, b.getEjbName());
        }
    }

    public Class<?> check_hasEjbClass(final ClassLoader loader, final EnterpriseBean b) {

        final String ejbName = b.getEjbName();

        final Class<?> beanClass = lookForClass(loader, b.getEjbClass(), "ejb-class", ejbName);
        final boolean isDynamicProxyImpl = DynamicProxyImplFactory.isKnownDynamicallyImplemented(beanClass);

        if (beanClass == null) {
            return null;
        }

        if (beanClass.isInterface() && !isDynamicProxyImpl) {
            fail(ejbName, "interfaceDeclaredAsBean", beanClass.getName());
        }

        if (isCmp(b)) {
            return beanClass;
        }

        if (isAbstract(beanClass.getModifiers()) && !isAbstractAllowed(beanClass)) {
            fail(ejbName, "abstractDeclaredAsBean", beanClass.getName());
        }

        return beanClass;
    }

    public static boolean isAbstractAllowed(final Class clazz) {
        if (DynamicProxyImplFactory.isKnownDynamicallyImplemented(clazz)) {
            return true;
        }
        if (DynamicSubclass.isDynamic(clazz)) {
            return true;
        }
        return false;
    }

    private void check_hasInterceptorClass(final ClassLoader loader, final Interceptor i) {

        lookForClass(loader, i.getInterceptorClass(), "interceptor-class", "Interceptor");

    }

    private void check_isEjbClass(final RemoteBean b) {
        if (b instanceof SessionBean) { //NOPMD
            // DMB: Beans in ejb 3 are not required to implement javax.ejb.SessionBean
            // but it would still be nice to think of some sort of check to do here.
            // compareTypes(b, b.getEjbClass(), javax.ejb.SessionBean.class);

        } else if (b instanceof EntityBean) {
            compareTypes(b, b.getEjbClass(), javax.ejb.EntityBean.class);
        }
    }

    private Class<?> lookForClass(final ClassLoader loader, final String clazz, final String type, final String ejbName) {
        try {
            return loadClass(loader, clazz);
        } catch (final OpenEJBException e) {
            /*
            # 0 - Class name
            # 1 - Element (home, ejb-class, remote)
            # 2 - Bean name
            */

            fail(ejbName, "missing.class", clazz, type, ejbName);

        } catch (final NoClassDefFoundError e) {
            /*
             # 0 - Class name
             # 1 - Element (home, ejb-class, remote)
             # 2 - Bean name
             # 3 - Misslocated Class name
             */
            fail(ejbName, "misslocated.class", clazz, type, ejbName, e.getMessage());

            throw e;
        }

        return null;
    }

    private boolean isValidInterface(final RemoteBean b, final Class clazz, final Class beanClass, final String tag) {

        if (clazz.equals(beanClass)) {

            fail(b, "xml." + tag + ".beanClass", clazz.getName());

        } else if (!clazz.isInterface()) {

            fail(b, "xml." + tag + ".notInterface", clazz.getName());

        } else if (EJBHome.class.isAssignableFrom(clazz)) {

            if (tag.equals("home")) {
                return true;
            }

            fail(b, "xml." + tag + ".ejbHome", clazz.getName());

        } else if (EJBLocalHome.class.isAssignableFrom(clazz)) {

            if (tag.equals("localHome")) {
                return true;
            }

            fail(b, "xml." + tag + ".ejbLocalHome", clazz.getName());

        } else if (EJBObject.class.isAssignableFrom(clazz)) {

            if (tag.equals("remote")) {
                return true;
            }

            fail(b, "xml." + tag + ".ejbObject", clazz.getName());

        } else if (EJBLocalObject.class.isAssignableFrom(clazz)) {

            if (tag.equals("local")) {
                return true;
            }

            fail(b, "xml." + tag + ".ejbLocalObject", clazz.getName());

        } else {
            if (tag.equals("businessLocal") || tag.equals("businessRemote")) {

                return true;

            } else if (clazz.isAnnotationPresent(Local.class)) {

                fail(b, "xml." + tag + ".businessLocal", clazz.getName());

            } else if (clazz.isAnnotationPresent(Remote.class)) {

                fail(b, "xml." + tag + ".businessRemote", clazz.getName());

            } else {

                fail(b, "xml." + tag + ".unknown", clazz.getName());

            }

        }

        // must be tagged as <home>, <local-home>, <remote>, or <local>

        return false;
    }

    private void compareTypes(final RemoteBean b, final String clazz1, final Class<?> class2) {
        Class<?> class1 = null;
        try {
            class1 = loadClass(clazz1);
        } catch (final OpenEJBException e) {
            return;
        }

        if (class1 != null && !class2.isAssignableFrom(class1)) {
            fail(b, "wrong.class.type", clazz1, class2.getName());
        }
    }

    protected Class<?> loadClass(final ClassLoader cl, final String clazz) throws OpenEJBException {
        try {
            return Class.forName(clazz, false, cl == null ? module.getClassLoader() : cl);
        } catch (final ClassNotFoundException cnfe) {
            throw new OpenEJBException(
                    new Messages("org.apache.openejb.util.resources").format("cl0007", clazz, module.getJarLocation()), cnfe);
        }
    }
}

