/*
 * 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.axis2.jaxws.description.builder;

import org.apache.axis2.java.security.AccessController;
import org.apache.axis2.jaxws.ExceptionFactory;
import org.apache.axis2.jaxws.i18n.Messages;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.xml.namespace.QName;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;

/**
 * 
 */
class DescriptionBuilderUtils {

    private static final Log log = LogFactory.getLog(DescriptionBuilderUtils.class);

    static String JAXWS_HOLDER_CLASS = "jakarta.xml.ws.Holder";

    private static final String INT_PRIMITIVE = "int";
    private static final String INT_PRIMITIVE_ENCODING = "I";
    private static final String BYTE_PRIMITIVE = "byte";
    private static final String BYTE_PRIMITIVE_ENCODING = "B";
    private static final String CHAR_PRIMITIVE = "char";
    private static final String CHAR_PRIMITIVE_ENCODING = "C";
    private static final String SHORT_PRIMITIVE = "short";
    private static final String SHORT_PRIMITIVE_ENCODING = "S";
    private static final String BOOLEAN_PRIMITIVE = "boolean";
    private static final String BOOLEAN_PRIMITIVE_ENCODING = "Z";
    private static final String LONG_PRIMITIVE = "long";
    private static final String LONG_PRIMITIVE_ENCODING = "J";
    private static final String FLOAT_PRIMITIVE = "float";
    private static final String FLOAT_PRIMITIVE_ENCODING = "F";
    private static final String DOUBLE_PRIMITIVE = "double";
    private static final String DOUBLE_PRIMITIVE_ENCODING = "D";
    private static final String VOID_PRIMITIVE = "void";
    // REVIEW: This may not be the correct encoding for Void
    private static final String VOID_PRIMITIVE_ENCODING = "V";

    /**
     * Returns a string representing the outermost generic raw type class, or null if the argument
     * is not a generic.  For example if the string "jakarta.xml.ws.Holder<my.package.MyObject>" is
     * passed in, the string "jakarta.xml.ws.Holder" will be returned.
     * <p/>
     * Note that generic arrays are supported.  For example, for "Holder<List<String>[][]", the
     * returned value will be "List[][]".
     *
     * @param inputType
     * @return A string representing the generic raw type or null if there is no generic.
     */
    static String getRawType(String inputType) {
        String returnRawType = null;
        int leftBracket = inputType.indexOf("<");
        int rightBracket = inputType.lastIndexOf(">");
        if (leftBracket > 0 && rightBracket > 0 && rightBracket > leftBracket) {
            String part1 = inputType.substring(0, leftBracket);
            if ((rightBracket + 1) == inputType.length()) {
                // There is nothing after the closing ">" we need to append to the raw type
                returnRawType = part1;
            } else {
                // Skip over the closing ">" then append the rest of the string to the raw type
                // This would be an array declaration for example.
                String part2 = inputType.substring(rightBracket + 1).trim();
                returnRawType = part1 + part2;
            }
        }
        return returnRawType;
    }

    /**
     * Return the actual type in a JAX-WS holder declaration.  For example, for the argument
     * "jakarta.xml.ws.Holder<my.package.MyObject>", return "my.package.MyObject". If the actual type
     * itself is a generic, then that raw type will be returned.  For example,
     * "jakarta.xml.ws.Holder<java.util.List<my.package.MyObject>>" will return "java.util.List".
     * <p/>
     * Note that Holders of Arrays and of Generic Arrays are also supported.  For example, for
     * "jakarta.xml.ws.Holder<String[]>", return "String[]".  For an array of a generic, the array of
     * the raw type is returned.  For example, for "jakarta.xml.ws.Holder<List<String>[][]>", return
     * "List[][]".
     * <p/>
     * Important note!  The JAX-WS Holder generic only supports a single actual type, i.e. the
     * generic is jakarta.xml.ws.Holder<T>.  This method is not general purpose; it does not support
     * generics with multiple types such as Generic<K,V> at the outermost level.
     *
     * @param holderInputString
     * @return return the actual argument class name for a JAX-WS Holder; returns null if the
     *         argument is not a JAX-WS Holder
     */
    static String getHolderActualType(String holderInputString) {
        String returnString = null;
        if (DescriptionBuilderUtils.isHolderType(holderInputString)) {
            int leftBracket = holderInputString.indexOf("<");
            int rightBracket = holderInputString.lastIndexOf(">");
            if (leftBracket > 0 && rightBracket > leftBracket + 1) {
                // Get everything between the outermost "<" and ">"
                String actualType =
                        holderInputString.substring(leftBracket + 1, rightBracket).trim();
                // If the holder contained a generic, then get the generic raw type (e.g. "List" for
                // Holder<List<String>>).
                String rawType = getRawType(actualType);
                if (rawType != null) {
                    returnString = rawType;
                } else {
                    return returnString = actualType;
                }
            }
        }
        return returnString;
    }


    /**
     * Check if the input String is a JAX-WS Holder.  For example "jakarta.xml.ws.Holder<my.package.MyObject>".
     *
     * @param checkType
     * @return true if it is a JAX-WS Holder type; false otherwise.
     */
    static boolean isHolderType(String checkType) {
        boolean isHolder = false;
        if (checkType != null) {
            if (checkType.startsWith(JAXWS_HOLDER_CLASS)) {
                isHolder = checkType.length() == JAXWS_HOLDER_CLASS.length() ||
                           checkType.charAt(JAXWS_HOLDER_CLASS.length()) == '<';
            }
        }
        return isHolder;
    }

    /**
     * Answers if the String representing the class contains an array declaration. For example
     * "Foo[][]" would return true, as would "int[]".
     *
     * @param className
     * @return
     */
    static boolean isClassAnArray(String className) {
        if (className != null && className.indexOf("[") > 0) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * For an class name that is an array, return the non-array declaration portion. For example
     * "my.package.Foo[][]" would return "my.package.Foo". Returns null if the argument does not
     * contain an array declaration.
     *
     * @param fullClassName
     * @return
     */
    static String getBaseArrayClassName(String fullClassName) {
        String baseArrayClassName = null;
        if (fullClassName != null) {
            int firstArrayDimension = fullClassName.indexOf("[");
            if (firstArrayDimension > 0) {
                baseArrayClassName = fullClassName.substring(0, firstArrayDimension);
            }
        }
        return baseArrayClassName;
    }

    /**
     * Return a prefix suitable for passing to Class.forName(String) for an array.  Each array
     * dimension represented by "[]" will be represented by a single "[".
     *
     * @param arrayClassName
     * @return
     */
    static String getArrayDimensionPrefix(String arrayClassName) {
        StringBuffer arrayDimPrefix = new StringBuffer();

        if (arrayClassName != null) {
            int arrayDimIndex = arrayClassName.indexOf("[]");
            while (arrayDimIndex > 0) {
                arrayDimPrefix.append("[");
                // Skip over this "[]" and see if there are any more.
                int startNext = arrayDimIndex + 2;
                arrayDimIndex = arrayClassName.indexOf("[]", startNext);
            }
        }

        if (arrayDimPrefix.length() > 0)
            return arrayDimPrefix.toString();
        else
            return null;
    }

    /**
     * For primitives, return the appropriate primitive class.  Note that arrays of primitives are
     * handled differently, like arrays of objects.  Only non-array primitives are processed by this
     * method.  This method understands both the typical primitive declaration (e.g. "int") and the
     * encoding used as for arrays (e.g. "I").
     *
     * @param classType
     * @return
     */
    static Class getPrimitiveClass(String classType) {

        Class paramClass = null;

        if (INT_PRIMITIVE.equals(classType) || INT_PRIMITIVE_ENCODING.equals(classType)) {
            paramClass = int.class;
        } else if (BYTE_PRIMITIVE.equals(classType) || BYTE_PRIMITIVE_ENCODING.equals(classType)) {
            paramClass = byte.class;
        } else if (CHAR_PRIMITIVE.equals(classType) || CHAR_PRIMITIVE_ENCODING.equals(classType)) {
            paramClass = char.class;
        } else
        if (SHORT_PRIMITIVE.equals(classType) || SHORT_PRIMITIVE_ENCODING.equals(classType)) {
            paramClass = short.class;
        } else
        if (BOOLEAN_PRIMITIVE.equals(classType) || BOOLEAN_PRIMITIVE_ENCODING.equals(classType)) {
            paramClass = boolean.class;
        } else if (LONG_PRIMITIVE.equals(classType) || LONG_PRIMITIVE_ENCODING.equals(classType)) {
            paramClass = long.class;
        } else
        if (FLOAT_PRIMITIVE.equals(classType) || FLOAT_PRIMITIVE_ENCODING.equals(classType)) {
            paramClass = float.class;
        } else
        if (DOUBLE_PRIMITIVE.equals(classType) || DOUBLE_PRIMITIVE_ENCODING.equals(classType)) {
            paramClass = double.class;
        } else if (VOID_PRIMITIVE.equals(classType) || VOID_PRIMITIVE_ENCODING.equals(classType)) {
            paramClass = void.class;
        }
        return paramClass;
    }

    /**
     * Returns the encoding used to represent a Class for an array of a primitive type.  For
     * example, an array of boolean is represented by "Z". This is as described in the javadoc for
     * Class.getName().  If the argument is not a primitive type, a null will be returned.
     * <p/>
     * Note that arrays of voids are not allowed; a null will be returned.
     *
     * @param primitiveType
     * @return
     */
    static String getPrimitiveTypeArrayEncoding(String primitiveType) {
        String encoding = null;

        if (BOOLEAN_PRIMITIVE.equals(primitiveType)) {
            encoding = BOOLEAN_PRIMITIVE_ENCODING;
        } else if (BYTE_PRIMITIVE.equals(primitiveType)) {
            encoding = BYTE_PRIMITIVE_ENCODING;
        } else if (CHAR_PRIMITIVE.equals(primitiveType)) {
            encoding = CHAR_PRIMITIVE_ENCODING;
        } else if (DOUBLE_PRIMITIVE.equals(primitiveType)) {
            encoding = DOUBLE_PRIMITIVE_ENCODING;
        } else if (FLOAT_PRIMITIVE.equals(primitiveType)) {
            encoding = FLOAT_PRIMITIVE_ENCODING;
        } else if (INT_PRIMITIVE.equals(primitiveType)) {
            encoding = INT_PRIMITIVE_ENCODING;
        } else if (LONG_PRIMITIVE.equals(primitiveType)) {
            encoding = LONG_PRIMITIVE_ENCODING;
        } else if (SHORT_PRIMITIVE.equals(primitiveType)) {
            encoding = SHORT_PRIMITIVE_ENCODING;
        }
        return encoding;
    }

    /**
     * If the parameter represents and array, then the returned string is in a format that a
     * Class.forName(String) can be done on it.  This format is described by Class.getName(). If the
     * parameter does not represent an array, the parememter is returned unmodified.
     * <p/>
     * Note that arrays of primitives are processed as well as arrays of objects.
     *
     * @param classToLoad
     * @return
     */
    static String reparseIfArray(String classToLoad) {
        if (log.isDebugEnabled())
        {
            log.debug("entry with String parameter classToLoad: " + classToLoad);
        }
        if (classToLoad.startsWith("[")) {
            // It appears that the string is already in binary form.
            // Detect if the form is valid and fix it if it is not.
            // For example, sometimes a [my.Foo is input instead of the required [Lmy.Foo;
            String binaryForm = classToLoad;
            
            int indexAfterBracket = classToLoad.lastIndexOf("[") + 1;
            
            String base = classToLoad.substring(indexAfterBracket);
            String dims = classToLoad.substring(0, indexAfterBracket);
            
            if (getPrimitiveClass(base) == null) {
                // Make sure the base starts with an L and ends with a ;
                if (!base.startsWith("L") && !base.endsWith(";")) {
                    binaryForm = dims + "L" + base + ";";
                }
            }
            if (log.isDebugEnabled())
            {
                log.debug("exit method with String return value binaryForm: " + binaryForm);
            }            
            return binaryForm;
        }
                    
        String reparsedClassName = classToLoad;
        if (isClassAnArray(classToLoad)) {
            String baseType = getBaseArrayClassName(classToLoad);
            String dimensionPrefix = getArrayDimensionPrefix(classToLoad);
            if (getPrimitiveTypeArrayEncoding(baseType) != null) {
                reparsedClassName = dimensionPrefix + getPrimitiveTypeArrayEncoding(baseType);
            } else {
                reparsedClassName = dimensionPrefix + "L" + baseType + ";";
            }
        }
        if (log.isDebugEnabled())
        {
            log.debug("exit method with String return value reparsedClassName: " + reparsedClassName);
        }        
        return reparsedClassName;
    }

    /**
     * Load a class represented in a Description Builder Composite.  If a classloader is specified,
     * it will be used; otherwise the default classloader is used.
     *
     * @param classToLoad
     * @param classLoader
     * @return
     */
    static Class loadClassFromComposite(String classToLoad, ClassLoader classLoader) {
        Class returnClass = null;

        // If this is an array, then create a string version as described by Class.getName.
        // For example, "Foo[][]" becomes "[[LFoo".  Note that arrays of primitives must also be parsed.
        classToLoad = DescriptionBuilderUtils.reparseIfArray(classToLoad);

        if (classLoader != null) {
            // Use the specified classloader to load the class.
            try {
                returnClass = forName(classToLoad, false, classLoader);
            }
            //Catch Throwable as ClassLoader can throw an NoClassDefFoundError that
            //does not extend Exception, so lets catch everything that extends Throwable
            //rather than just Exception.
            catch (Throwable ex) {
                throw ExceptionFactory.makeWebServiceException(
                        Messages.getMessage("DBUClassNotFound", classToLoad, classLoader.toString()));
            }
        } else {
            //Use the thread context class loader to load the class.
            try {
                returnClass = forName(classToLoad, false,
                                                       getContextClassLoader(null));
            }
            catch (Throwable ex) {
                //Use the default classloader to load the class.
                try {
                    returnClass = forName(classToLoad);
                }
                //Catch Throwable as ClassLoader can throw an NoClassDefFoundError that
                //does not extend Exception
                catch (Throwable ex2) {
                    throw ExceptionFactory.makeWebServiceException(
                            Messages.getMessage("DBUClassNotFound2", classToLoad));
                }
            }
        }
        return returnClass;
    }

    static boolean isEmpty(String string) {
        return (string == null || "".equals(string));
    }

    static boolean isEmpty(QName qname) {
        return qname == null || isEmpty(qname.getLocalPart());
    }

    /**
     * Return the class for this name
     *
     * @return Class
     */
    private static Class forName(final String className, final boolean initialize,
                                final ClassLoader classloader) throws ClassNotFoundException {
        Class cl = null;
        try {
            cl = (Class) AccessController.doPrivileged(
                    new PrivilegedExceptionAction() {
                        public Object run() throws ClassNotFoundException {
                            return Class.forName(className, initialize, classloader);
                        }
                    }
            );
        } catch (PrivilegedActionException e) {
            if (log.isDebugEnabled()) {
                log.debug("Exception thrown from AccessController: " + e.getMessage(), e);
            }
            throw (ClassNotFoundException) e.getException();
        }

        return cl;
    }

    /**
     * Return the class for this name
     *
     * @return Class
     */
    private static Class forName(final String className) throws ClassNotFoundException {
        Class cl = null;
        try {
            cl = (Class) AccessController.doPrivileged(
                    new PrivilegedExceptionAction() {
                        public Object run() throws ClassNotFoundException {
                            return Class.forName(className);
                        }
                    }
            );
        } catch (PrivilegedActionException e) {
            if (log.isDebugEnabled()) {
                log.debug("Exception thrown from AccessController: " + e.getMessage(), e);
            }
            throw (ClassNotFoundException) e.getException();
        }

        return cl;
    }

    /**
     * @return ClassLoader
     */
    private static ClassLoader getContextClassLoader(final ClassLoader classLoader) {
        ClassLoader cl;
        try {
            cl = (ClassLoader) AccessController.doPrivileged(
                    new PrivilegedExceptionAction() {
                        public Object run() throws ClassNotFoundException {
                            return classLoader != null ? classLoader : Thread.currentThread().getContextClassLoader();
                        }
                    }
            );
        } catch (PrivilegedActionException e) {
            if (log.isDebugEnabled()) {
                log.debug("Exception thrown from AccessController: " + e.getMessage(), e);
            }
            throw ExceptionFactory.makeWebServiceException(e.getException());
        }

        return cl;
    }
}
