/*
 * Copyright (c) 2009, Oracle and its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
/*
 * $Id: InjectionResolver.java 62592 2011-04-20 20:07:16Z kgrucci $
 */
package com.sun.ts.tests.common.vehicle.ejbembed;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.ejb.EJBs;
import javax.ejb.embeddable.EJBContainer;

import com.sun.ts.tests.common.vehicle.ejbliteshare.EJBLiteClientIF;

/**
 * Since JavaEE annotations are not required in ejb embeddable usage, this class
 * helps resolving @EJB and @PostConstruct in client classes when tests are running
 * in ejbembed vehicle.
 * 
 * It resolves type-level, field and setter @EJB injections, and @PostConstruct methods
 * as well.  This class constructs a portable jndi name from the metadata in @EJB 
 * annotations.
 * 
 * All client classes and their superclasses need to be scanned for @EJB and @PostConstruct.
 * The most general superclass should be processed first.  However, all @PostConstruct
 * methods must be invoked after all @EJB injections have been resolved and initialized.
 * 
 * For type-level injections, name, beanName, and beanInterface are all required, and
 * so they are sufficient to construct the portable jndi name.  Then a mapping between
 * JavaEE lookup name and portable global jndi name is recorded, which can be consulted
 * when test methods look up the ejb ref.
 * 
 * For field and setter @EJB injection, all 3 @EJB attributes are optional.  beanInterface
 * may be present or be inferred from the field or parameter type.  Obtaining beanName
 * is complicated and requires searching all ejb bean classes, parsing its component-
 * defining annotations and parsing ejb-jar.xml.  This part is too much for our purpose.
 * 
 * So we make it a rule for test writers that all client classes that are to be run in
 * ejbembed vehicle always use beanName attribute in @EJB injections.
 * 
 * After a portable global jndi name is constructed, the field is initialized to the 
 * lookup result, and the setter method is invoked, passing the lookup result as the
 * parameter.
 * 
 * moduleName is set by various vehicles.
 */
public class InjectionResolver {
    private static final Logger logger = Logger
            .getLogger("com.sun.ts.tests.common.vehicle.ejbembed");

    private EJBContainer container;

    private EJBLiteClientIF client;
    
    private List<Method> postConstructMethods = new ArrayList<Method>();

    public InjectionResolver(EJBLiteClientIF client, EJBContainer container) {
        super();
        this.client = client;
        this.container = container;
    }

    public void resolve() {
        resolve0(client.getClass());
        invokePostConstructMethods();
    }
    
    public void resolve0(Class<? extends EJBLiteClientIF> cls) {
        Class<?> sup = cls.getSuperclass();
        if(sup != null && EJBLiteClientIF.class.isAssignableFrom(sup)) {
            resolve0((Class<? extends EJBLiteClientIF>) sup);
        }
        
        resolveTypeLevelInjections(cls);
        resolveFieldInjections(cls);
        resolveSetterInjections(cls);
        resolvePostConstruct(cls);
        
        logger.info("Resolved " + cls);
    }

    private void resolvePostConstruct(Class<? extends EJBLiteClientIF> cls) {
        Method[] methods = cls.getDeclaredMethods();
        for (Method m : methods) {
            PostConstruct pc = m.getAnnotation(PostConstruct.class);
            if(pc != null) {
                postConstructMethods.add(m);
            }
        }
    }
    
    private void invokePostConstructMethods() {
        for(Method m : postConstructMethods) {
            m.setAccessible(true);
            try {
                m.invoke(client);
                logger.info("Invoked PostConstruct method: " + m);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private void resolveSetterInjections(Class<? extends EJBLiteClientIF> cls) {
        Method[] methods = cls.getDeclaredMethods();
        for (Method m : methods) {
            EJB b = m.getAnnotation(EJB.class);
            if (b == null) {
                continue;
            }

            logger.info("Resolving setter @EJB injection: " + b);

            String lookup = b.lookup(); // default value is ""
            if (lookup.length() > 0 && lookup.startsWith("java:global")) {
                logger.info("Got @EJB.lookup " + lookup);
            } else {
                Class<?> beanInterface = b.beanInterface();
                String beanName = b.beanName();
                if (beanInterface.equals(Object.class)) {
                    Class<?>[] paramTypes = m.getParameterTypes();
                    beanInterface = paramTypes[0];
                }
                if (beanName.length() == 0) {
                    beanName = getBeanNameFromDescription(b.description());
                }
                if (beanName.length() == 0) {
                    throw new RuntimeException(
                            "beanName is not specified in @EJB injection on method " + m.toString());
                }
                lookup = createGlobalJNDIName(beanInterface, beanName);
            }

            Object beanFromLookup = lookup(lookup);

            m.setAccessible(true);
            try {
                m.invoke(client, beanFromLookup);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private void resolveFieldInjections(Class<? extends EJBLiteClientIF> cls) {
        Field[] fields = cls.getDeclaredFields();
        for (Field f : fields) {
            EJB b = f.getAnnotation(EJB.class);
            if (b == null) {
                continue;
            }

            logger.info("Resolving field @EJB injection: " + b);
            
            String lookup = b.lookup(); // default value is ""
            if (lookup.length() > 0 && lookup.startsWith("java:global")) {
                logger.info("Got @EJB.lookup " + lookup);
            } else {
                Class<?> beanInterface = b.beanInterface();
                String beanName = b.beanName();
                if (beanInterface.equals(Object.class)) {
                    beanInterface = f.getType();
                }
                if (beanName.length() == 0) {
                    beanName = getBeanNameFromDescription(b.description());
                }
                if (beanName.length() == 0) {
                    throw new RuntimeException(
                            "beanName is not specified in @EJB injection on field " + f.toString());
                }
                lookup = createGlobalJNDIName(beanInterface, beanName);
            }
                
            Object beanFromLookup = lookup(lookup);

            f.setAccessible(true);
            try {
                f.set(client, beanFromLookup);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
    /**
     * EJB spec requires that lookup and beanName attributes cannot be both
     * present in the same @EJB. For those tests that specifically test
     * lookup attribute, beanName value has to be indirectly specified in
     * description attribute in the form:
     * description="beanName=LookupBean"
     * This method extracts the beanName value from description attribute
     * of @EJB
     */
    private String getBeanNameFromDescription(String description) {
        String[] tokens = description.split("=");
        String beanName = "";
        if(tokens.length == 2) {
            beanName = tokens[1];
            logger.info("Got beanName indirectly from description: " + beanName);
        }
        return beanName;
    }

    private void resolveTypeLevelInjections(Class<? extends EJBLiteClientIF> cls) {
        EJBs ejbs = cls.getAnnotation(EJBs.class);
        EJB ejb = cls.getAnnotation(EJB.class);

        if (ejbs != null) {
            for (EJB b : ejbs.value()) {
                resolveTypeLevelEJB(b);
            }
        }
        if (ejb != null) {
            resolveTypeLevelEJB(ejb);
        }
    }

    private void resolveTypeLevelEJB(EJB b) {
        // For type-level @EJB injections, all 3 attr are required
        logger.info("Resolving type-level @EJB injection: " + b);
        
        Class<?> beanInterface = b.beanInterface();
        String beanName = b.beanName();
        String name = b.name();
        String lookup = b.lookup();  //should be very rare to use lookup attr in type-level injections
        
        if (lookup.length() > 0 && lookup.startsWith("java:global")) {
            logger.info("Got @EJB.lookup " + lookup);
        } else {
            lookup = createGlobalJNDIName(beanInterface, beanName);
        }
        
        client.getJndiMapping().put(createJavaEELookupName(name), lookup);
    }

    private String createJavaEELookupName(String name) {
        return "java:comp/env/" + name;
    }

    // java:global[/<app-name>]/<module-name>/<bean-name>[!<fully-qualified-interface-name>]
    private String createGlobalJNDIName(Class<?> beanInterface, String beanName) {
        String result = EJBLiteClientIF.JAVA_GLOBAL_PREFIX;
        result += client.getModuleName() + "/" + beanName + "!" + beanInterface.getName();
        
        logger.info("Constructed portable global jndi name: " + result);
        return result;
    }

    private Object lookup(String lookupName) {
        Object result = null;
        javax.naming.Context context = container.getContext();
        try {
            result = context.lookup(lookupName);
        } catch (javax.naming.NamingException e) {
            throw new RuntimeException(e);
        }

        return result;
    }

}
