/*
 * Copyright 2003-2006 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */


package com.sun.corba.se.impl.presentation.rmi ;

import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.util.Set;
import java.util.HashSet;
import java.util.Iterator;

/**
 * Utility class for testing RMI/IDL Types as defined in
 * Section 1.2 of The Java Language to IDL Mapping.  Note that
 * these are static checks only.  Runtime checks, such as those
 * described in Section 1.2.3, #3, are not covered.
 */
public final class IDLTypesUtil {

    private static final String GET_PROPERTY_PREFIX = "get";
    private static final String SET_PROPERTY_PREFIX = "set";
    private static final String IS_PROPERTY_PREFIX  = "is";

    public static final int VALID_TYPE   = 0;
    public static final int INVALID_TYPE = 1;

    /* rmic -iiop does not correctly implement the clause in 1.3.4.3
     * about is<NAME>/get<NAME> conflicts.  The spec says that
     * is<NAME> is the property and get<NAME> is left alone,
     * but rmic does the opposite.  We will follow rmic in this,
     * but it's easy to change.
     */
    public static final boolean FOLLOW_RMIC = true ;

    /**
     * Validate a class to ensure it conforms to the rules for a
     * Java RMI/IIOP interface.
     *
     * @throws IDLTypeException if not a valid RMI/IIOP interface.
     */
    public void validateRemoteInterface(Class c) throws IDLTypeException
    {
        if( c == null ) {
            throw new IllegalArgumentException();
        }

        if( !c.isInterface() ) {
            String msg = "Class " + c + " must be a java interface.";
            throw new IDLTypeException(msg);
        }

        if( !java.rmi.Remote.class.isAssignableFrom(c) ) {
            String msg = "Class " + c + " must extend java.rmi.Remote, " +
                "either directly or indirectly.";
            throw new IDLTypeException(msg);
        }

        // Get all methods, including super-interface methods.
        Method[] methods = c.getMethods();

        for(int i = 0; i < methods.length; i++) {
            Method next = methods[i];
            validateExceptions(next);
        }

        // Removed because of bug 4989053
        // validateDirectInterfaces(c);
        validateConstants(c);

        return;
    }

    public boolean isRemoteInterface(Class c)
    {
        boolean remoteInterface = true;
        try {
            validateRemoteInterface(c);
        } catch(IDLTypeException ite) {
            remoteInterface = false;
        }

        return remoteInterface;
    }

    /**
     * Section 1.2.2 Primitive Types
     */
    public boolean isPrimitive(Class c)
    {
        if( c == null ) {
            throw new IllegalArgumentException();
        }

        return c.isPrimitive();
    }

    /**
     * Section 1.2.4
     */
    public boolean isValue(Class c)
    {
        if( c == null ) {
            throw new IllegalArgumentException();
        }

        return
            (!c.isInterface() &&
             java.io.Serializable.class.isAssignableFrom(c) &&
             !java.rmi.Remote.class.isAssignableFrom(c));
    }

    /**
     * Section 1.2.5
     */
    public boolean isArray(Class c)
    {
        boolean arrayType = false;

        if( c == null ) {
            throw new IllegalArgumentException();
        }

        if( c.isArray() ) {
            Class componentType = c.getComponentType();
            arrayType =
                (isPrimitive(componentType) || isRemoteInterface(componentType) ||
                 isEntity(componentType) || isException(componentType) ||
                 isValue(componentType) || isObjectReference(componentType) );
        }

        return arrayType;
    }

    /**
     * Section 1.2.6
     */
    public boolean isException(Class c)
    {
        if( c == null ) {
            throw new IllegalArgumentException();
        }

        // Must be a checked exception, not including RemoteException or
        // its subclasses.
        return isCheckedException(c) && !isRemoteException(c) && isValue(c);
    }

    public boolean isRemoteException(Class c)
    {
        if( c == null ) {
            throw new IllegalArgumentException();
        }

        return java.rmi.RemoteException.class.isAssignableFrom(c) ;
    }

    public boolean isCheckedException(Class c)
    {
        if( c == null ) {
            throw new IllegalArgumentException();
        }

        return Throwable.class.isAssignableFrom(c) &&
            !RuntimeException.class.isAssignableFrom(c) &&
            !Error.class.isAssignableFrom(c) ;
    }

    /**
     * Section 1.2.7
     */
    public boolean isObjectReference(Class c)
    {
        if( c == null ) {
            throw new IllegalArgumentException();
        }

        return (c.isInterface() &&
                org.omg.CORBA.Object.class.isAssignableFrom(c));
    }

    /**
     * Section 1.2.8
     */
    public boolean isEntity(Class c)
    {
        if( c == null ) {
            throw new IllegalArgumentException();
        }

        Class superClass = c.getSuperclass();
        return (!c.isInterface() &&
                (superClass != null) &&
                (org.omg.CORBA.portable.IDLEntity.class.isAssignableFrom(c)));
    }

    /**
     * Return true if given method is legal property accessor as defined in
     * Section 1.3.4.3 of Java2IDL spec.
     */
    public boolean isPropertyAccessorMethod(Method m, Class c) {

        String methodName = m.getName();
        Class returnType  = m.getReturnType();
        Class[] parameters = m.getParameterTypes();
        Class[] exceptionTypes = m.getExceptionTypes();
        String propertyType = null;

        if( methodName.startsWith(GET_PROPERTY_PREFIX) ) {

            if((parameters.length == 0) && (returnType != Void.TYPE) &&
                !readHasCorrespondingIsProperty(m, c)) {
                propertyType = GET_PROPERTY_PREFIX;
            }

        } else if( methodName.startsWith(SET_PROPERTY_PREFIX) ) {

            if((returnType == Void.TYPE) && (parameters.length == 1)) {
                if (hasCorrespondingReadProperty(m, c, GET_PROPERTY_PREFIX) ||
                    hasCorrespondingReadProperty(m, c, IS_PROPERTY_PREFIX)) {
                    propertyType = SET_PROPERTY_PREFIX;
                }
            }

        } else if( methodName.startsWith(IS_PROPERTY_PREFIX) ) {
            if((parameters.length == 0) && (returnType == Boolean.TYPE) &&
                !isHasCorrespondingReadProperty(m, c)) {
                propertyType = IS_PROPERTY_PREFIX;
            }
        }

        // Some final checks that apply to all properties.
        if( propertyType != null ) {
            if(!validPropertyExceptions(m) ||
               (methodName.length() <= propertyType.length())) {
                propertyType = null;
            }
        }

        return (propertyType != null);
    }

    private boolean hasCorrespondingReadProperty
        (Method writeProperty, Class c, String readPropertyPrefix)
    {
        String writePropertyMethodName = writeProperty.getName();
        Class[] writePropertyParameters = writeProperty.getParameterTypes();
        boolean foundReadProperty = false;

        try {
            // Look for a valid corresponding Read property
            String readPropertyMethodName =
                writePropertyMethodName.replaceFirst
                    (SET_PROPERTY_PREFIX, readPropertyPrefix);
            Method readPropertyMethod = c.getMethod(readPropertyMethodName,
                                                    new Class[] {});
            foundReadProperty =
                ( isPropertyAccessorMethod(readPropertyMethod, c) &&
                  (readPropertyMethod.getReturnType() ==
                   writePropertyParameters[0]) );
        } catch(Exception e) {
            // ignore. this means we didn't find a corresponding get property.
        }

        return foundReadProperty;
    }

    private boolean readHasCorrespondingIsProperty(Method readProperty,
        Class c)
    {
        if (FOLLOW_RMIC)
            return false ;

        String readPropertyMethodName = readProperty.getName();
        boolean foundIsProperty = false;

        try {
            // Look for a valid corresponding Is property
            String isPropertyMethodName =
                readPropertyMethodName.replaceFirst(GET_PROPERTY_PREFIX,
                    IS_PROPERTY_PREFIX);
            Method isPropertyMethod = c.getMethod( isPropertyMethodName,
                                                    new Class[] {});
            foundIsProperty = isPropertyAccessorMethod(isPropertyMethod,
                c) ;
        } catch(Exception e) {
            // ignore. this means we didn't find a corresponding Is property.
        }

        return foundIsProperty;
    }

    private boolean isHasCorrespondingReadProperty(Method readProperty,
        Class c)
    {
        if (!FOLLOW_RMIC)
            return false ;

        String readPropertyMethodName = readProperty.getName();
        boolean foundIsProperty = false;

        try {
            // Look for a valid corresponding Read property
            String isPropertyMethodName =
                readPropertyMethodName.replaceFirst(IS_PROPERTY_PREFIX,
                    GET_PROPERTY_PREFIX);
            Method isPropertyMethod = c.getMethod( isPropertyMethodName,
                                                    new Class[] {});
            foundIsProperty = isPropertyAccessorMethod(isPropertyMethod,
                c) ;
        } catch(Exception e) {
            // ignore. this means we didn't find a corresponding read property.
        }

        return foundIsProperty;
    }

    public String getAttributeNameForProperty(String propertyName) {
        String attributeName = null;
        String prefix = null;

        if( propertyName.startsWith(GET_PROPERTY_PREFIX) ) {
            prefix = GET_PROPERTY_PREFIX;
        } else if( propertyName.startsWith(SET_PROPERTY_PREFIX) ) {
            prefix = SET_PROPERTY_PREFIX;
        } else if( propertyName.startsWith(IS_PROPERTY_PREFIX) ) {
            prefix = IS_PROPERTY_PREFIX;
        }

        if( (prefix != null) && (prefix.length() < propertyName.length()) ) {
            String remainder = propertyName.substring(prefix.length());
            if( (remainder.length() >= 2) &&
                Character.isUpperCase(remainder.charAt(0)) &&
                Character.isUpperCase(remainder.charAt(1)) ) {
                // don't set the first letter to lower-case if the
                // first two are upper-case
                attributeName = remainder;
            } else {
                attributeName = Character.toLowerCase(remainder.charAt(0)) +
                    remainder.substring(1);
            }
        }

        return attributeName;
    }

    /**
     * Return IDL Type name for primitive types as defined in
     * Section 1.3.3 of Java2IDL spec or null if not a primitive type.
     */
    public IDLType getPrimitiveIDLTypeMapping(Class c) {

        if( c == null ) {
            throw new IllegalArgumentException();
        }

        if( c.isPrimitive() ) {
            if( c == Void.TYPE ) {
                return new IDLType( c, "void" ) ;
            } else if( c == Boolean.TYPE ) {
                return new IDLType( c, "boolean" ) ;
            } else if( c == Character.TYPE ) {
                return new IDLType( c, "wchar" ) ;
            } else if( c == Byte.TYPE ) {
                return new IDLType( c, "octet" ) ;
            } else if( c == Short.TYPE ) {
                return new IDLType( c, "short" ) ;
            } else if( c == Integer.TYPE ) {
                return new IDLType( c, "long" ) ;
            } else if( c == Long.TYPE ) {
                return new IDLType( c, "long_long" ) ;
            } else if( c == Float.TYPE ) {
                return new IDLType( c, "float" ) ;
            } else if( c == Double.TYPE ) {
                return new IDLType( c, "double" ) ;
            }
        }

        return null;
    }

    /**
     * Return IDL Type name for special case type mappings as defined in
     * Table 1-1 of Java2IDL spec or null if given class is not a special
     * type.
     */
    public IDLType getSpecialCaseIDLTypeMapping(Class c) {

        if( c == null ) {
            throw new IllegalArgumentException();
        }

        if( c == java.lang.Object.class ) {
            return new IDLType( c, new String[] { "java", "lang" },
                "Object" ) ;
        } else if( c == java.lang.String.class ) {
            return new IDLType( c, new String[] { "CORBA" },
                "WStringValue" ) ;
        } else if( c == java.lang.Class.class ) {
            return new IDLType( c, new String[] { "javax", "rmi", "CORBA" },
                "ClassDesc" ) ;
        } else if( c == java.io.Serializable.class ) {
            return new IDLType( c, new String[] { "java", "io" },
                "Serializable" ) ;
        } else if( c == java.io.Externalizable.class ) {
            return new IDLType( c, new String[] { "java", "io" },
                "Externalizable" ) ;
        } else if( c == java.rmi.Remote.class ) {
            return new IDLType( c, new String[] { "java", "rmi" },
                "Remote" ) ;
        } else if( c == org.omg.CORBA.Object.class ) {
            return new IDLType( c, "Object" ) ;
        } else {
            return null;
        }
    }

    /**
     * Implements 1.2.3 #2 and #4
     */
    private void validateExceptions(Method method) throws IDLTypeException {

        Class[] exceptions = method.getExceptionTypes();

        boolean declaresRemoteExceptionOrSuperClass = false;

        // Section 1.2.3, #2
        for(int eIndex = 0; eIndex < exceptions.length; eIndex++) {
            Class exception = exceptions[eIndex];
            if( isRemoteExceptionOrSuperClass(exception) ) {
                declaresRemoteExceptionOrSuperClass = true;
                break;
            }
        }

        if( !declaresRemoteExceptionOrSuperClass ) {
            String msg = "Method '" + method + "' must throw at least one " +
                "exception of type java.rmi.RemoteException or one of its " +
                "super-classes";
            throw new IDLTypeException(msg);
        }

        // Section 1.2.3, #4
        // See also bug 4972402
        // For all exceptions E in exceptions,
        // (isCheckedException(E) => (isValue(E) || RemoteException.isAssignableFrom( E ) )
        for(int eIndex = 0; eIndex < exceptions.length; eIndex++) {
            Class exception = exceptions[eIndex];

            if (isCheckedException(exception) && !isValue(exception) &&
                !isRemoteException(exception))
            {
                String msg = "Exception '" + exception + "' on method '" +
                    method + "' is not a allowed RMI/IIOP exception type";
                throw new IDLTypeException(msg);
            }
        }

        return;
    }

    /**
     * Returns true if the method's throw clause conforms to the exception
     * restrictions for properties as defined in Section 1.3.4.3 of
     * Java2IDL spec.  This means that for all exceptions E declared on the
     * method, E isChecked => RemoteException.isAssignableFrom( E ).
     */
    private boolean validPropertyExceptions(Method method)
    {
        Class[] exceptions = method.getExceptionTypes();

        for(int eIndex = 0; eIndex < exceptions.length; eIndex++) {
            Class exception = exceptions[eIndex];

            if (isCheckedException(exception) && !isRemoteException(exception))
                return false ;
        }

        return true;
    }

    /**
     * Implements Section 1.2.3, #2.
     */
    private boolean isRemoteExceptionOrSuperClass(Class c) {
        return
            ((c == java.rmi.RemoteException.class) ||
             (c == java.io.IOException.class) ||
             (c == java.lang.Exception.class) ||
             (c == java.lang.Throwable.class));
    }

    /**
     * Implements Section 1.2.3, #5.
     */
    private void validateDirectInterfaces(Class c) throws IDLTypeException {

        Class[] directInterfaces = c.getInterfaces();

        if( directInterfaces.length < 2 ) {
            return;
        }

        Set allMethodNames = new HashSet();
        Set currentMethodNames = new HashSet();

        for(int i = 0; i < directInterfaces.length; i++) {
            Class next = directInterfaces[i];
            Method[] methods = next.getMethods();

            // Comparison is based on method names only.  First collect
            // all methods from current interface, eliminating duplicate
            // names.
            currentMethodNames.clear();
            for(int m = 0; m < methods.length; m++) {
                currentMethodNames.add(methods[m].getName());
            }

            // Now check each method against list of all unique method
            // names processed so far.
            for(Iterator iter=currentMethodNames.iterator(); iter.hasNext();) {
                String methodName = (String) iter.next();
                if( allMethodNames.contains(methodName) ) {
                    String msg = "Class " + c + " inherits method " +
                        methodName + " from multiple direct interfaces.";
                    throw new IDLTypeException(msg);
                } else {
                    allMethodNames.add(methodName);
                }
            }
        }

        return;
    }

    /**
     * Implements 1.2.3 #6
     */
    private void validateConstants(final Class c)
        throws IDLTypeException {

        Field[] fields = null;

        try {
            fields = (Field[])
                java.security.AccessController.doPrivileged
                (new java.security.PrivilegedExceptionAction() {
                        public java.lang.Object run() throws Exception {
                            return c.getFields();
                        }
                    });
        } catch(java.security.PrivilegedActionException pae) {
            IDLTypeException ite = new IDLTypeException();
            ite.initCause(pae);
            throw ite;
        }

        for(int i = 0; i < fields.length; i++) {
            Field next = fields[i];
            Class fieldType = next.getType();
            if( (fieldType != java.lang.String.class) &&
                !isPrimitive(fieldType) ) {
                String msg = "Constant field '" + next.getName() +
                    "' in class '" + next.getDeclaringClass().getName() +
                    "' has invalid type' " + next.getType() + "'. Constants" +
                    " in RMI/IIOP interfaces can only have primitive" +
                    " types and java.lang.String types.";
                throw new IDLTypeException(msg);
            }
        }


        return;
    }

}
