/* * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of Business Objects nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* * ForeignEntityResolver.java * Created: July 4, 2007 * By: Joseph Wong */ package org.openquark.cal.internal.compiler; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.HashMap; import java.util.Map; /** * This class contains helper methods for resolving foreign entities from their * Java names and type signatures: * {@link Class}es, {@link Method}s, {@link Field}s, and {@link Constructor}s. * This class is not meant to be instantiated. * * @author Bo Ilic * @author Joseph Wong */ final public class ForeignEntityResolver { /** * (String -> Class) map from the primitive Java type names such as "int" to their Class objects. */ static private final Map<String, Class<?>> primitiveJavaTypesMap = new HashMap<String, Class<?>>(); static { //note that ("void", void.class) is intentionally not added here. primitiveJavaTypesMap.put("char", char.class); primitiveJavaTypesMap.put("boolean", boolean.class); primitiveJavaTypesMap.put("byte", byte.class); primitiveJavaTypesMap.put("short", short.class); primitiveJavaTypesMap.put("int", int.class); primitiveJavaTypesMap.put("long", long.class); primitiveJavaTypesMap.put("float", float.class); primitiveJavaTypesMap.put("double", double.class); } /** * An enumeration class containing constants for the different kinds of status that could result from * an attempt to resolve a foreign entity. * * @author Joseph Wong */ public static final class ResolutionStatus { /** * A textual description of the status. For debug purposes. */ private final String description; /** * Status for a successful attempt at resolving a foreign entity. */ public static final ResolutionStatus SUCCESS = new ResolutionStatus("Success"); /** * Status for the case when the foreign entity cannot be found. * This corresponds to an exception of the following types: * {@link NoSuchMethodException} for resolving methods and constructors, * {@link NoSuchFieldException} for resolving fields, and * {@link ClassNotFoundException} for resolving classes. */ public static final ResolutionStatus NO_SUCH_ENTITY = new ResolutionStatus("NoSuchEntity"); /** * Status for the case when the resolution attempt results in a security violation. * This corresponds to an exception of the type {@link SecurityException}. */ public static final ResolutionStatus SECURITY_VIOLATION = new ResolutionStatus("SecurityViolation"); /** * Status for the case when a dependee class of the class to be resolved is not found. * This corresponds to an error of the type {@link NoClassDefFoundError} with a message explaining the dependee class needed. */ public static final ResolutionStatus DEPENDEE_CLASS_NOT_FOUND = new ResolutionStatus("DependeeClassNotFound"); /** * Status for the case when a class can be found, but cannot be loaded. * This corresponds to an error of the type {@link NoClassDefFoundError} without a message. */ public static final ResolutionStatus CANNOT_LOAD_CLASS = new ResolutionStatus("CannotLoadClass"); /** * Status for the case when a class cannot be initialized (because its static initializers result in an uncaught exception). * This corresponds to an error of the type {@link ExceptionInInitializerError}. */ public static final ResolutionStatus CANNOT_INITIALIZE_CLASS = new ResolutionStatus("CannotInitializeClass"); /** * Status for the case when a dependee class has incompatibly changed after the compilation of the dependent class. * This corresponds to an error of the type {@link LinkageError}. */ public static final ResolutionStatus LINKAGE_ERROR = new ResolutionStatus("LinkageError"); /** * Status for the case when the class to be resolved should be public, but is in fact not public. */ public static final ResolutionStatus NOT_ACCESSIBLE = new ResolutionStatus("NotAccessible"); /** * Status for the case when the method/field is expected to be static but is found to be non-static. */ public static final ResolutionStatus EXPECT_STATIC_FOUND_NON_STATIC = new ResolutionStatus("ExpectStaticFoundNonStatic"); /** * Status for the case when the method/field is expected to be non-static but is found to be static. */ public static final ResolutionStatus EXPECT_NON_STATIC_FOUND_STATIC = new ResolutionStatus("ExpectNonStaticFoundStatic"); /** * Status for the case when the actual return type of the method/field/constructor does not match the declared return type. */ public static final ResolutionStatus ACTUAL_RETURN_TYPE_DOES_NOT_MATCH_DECLARED_RETURN_TYPE = new ResolutionStatus("ActualReturnTypeDoesNotMatchDeclaredReturnType"); /** * Private constructor for this enumeration class. * @param description a textual description of the status. For debug purposes. */ private ResolutionStatus(final String description) { this.description = description; } /** * {@inheritDoc} */ @Override public String toString() { return description; } } /** * This is the abstract base class for representing the result of an attempt to resolve a foreign entity: * a class, a method, a constructor, or a field. These different kinds of resolution results are represented * by concrete subclasses. * * @param <T> the type of the resolved entity. This can be a {@link Class}, a {@link Constructor}, a {@link Method} * or a {@link Field}. * * @author Joseph Wong */ public static final class ResolutionResult<T> { /** * The resolved entity. Can be null if the resolution failed. */ private final T resolvedEntity; /** * The status of the resolution attempt. Can either be success or one of the failure codes. * Cannot be null. */ private final ResolutionStatus status; /** * The message associated with the status. * Can be null. */ private final String associatedMessage; /** * The {@link Exception} or {@link Error} associated with the status. * Can be null. */ private final Throwable throwable; /** * Private constructor. Only meant to be called by subclasses. * @param resolvedEntity the resolved entity. Can be null if the resolution failed. * @param status the status of the resolution attempt. Can either be success or one of the failure codes. Cannot be null. * @param associatedMessage the associated message. Can be null. * @param throwable the associated {@link Exception} or {@link Error}. Can be null. */ private ResolutionResult(final T resolvedEntity, final ResolutionStatus status, final String associatedMessage, final Throwable throwable) { if (status == null) { throw new NullPointerException(); } this.resolvedEntity = resolvedEntity; this.status = status; this.associatedMessage = associatedMessage; this.throwable = throwable; } /** * @return the resolved entity. Can be null if the resolution failed. */ public T getResolvedEntity() { return resolvedEntity; } /** * @return the status of the resolution attempt. Can either be success or one of the failure codes. Cannot be null. */ public ResolutionStatus getStatus() { return status; } /** * @return the message associated with the status. Can be null. */ public final String getAssociatedMessage() { return associatedMessage; } /** * @return the {@link Exception} or {@link Error} associated with the status. Can be null. */ public Throwable getThrowable() { return throwable; } } /** Private constructor. */ private ForeignEntityResolver() {} /** * Resolves the specified method (with given name and argument types) in the given Class object. * @param isStatic true if the Java method is static. * @param foreignClass the class containing the method to be resolved. * @param methodName the name of the method. * @param argTypes the types of the arguments of the method. * @param declaredReturnType the Class object corresponding to the declared return type of the foreign function. * @return the result of the resolution attempt, which contains a status code and the {@link Method} object, * if the resolution was successful. */ public static ResolutionResult<Method> resolveMethod(final boolean isStatic, final Class<?> foreignClass, final String methodName, final Class<?>[] argTypes, final Class<?> declaredReturnType) { Method method = null; ResolutionStatus status = ResolutionStatus.SUCCESS; String associatedMessage = null; Throwable exceptionOrError = null; try { method = foreignClass.getMethod(methodName, argTypes); } catch (final NoSuchMethodException e) { //todoBI it would be better to provide a more precise error message in the case where the desired //method exists but is not accessible. This could be done by analyzing the returned methods //from Class.getMethods() which includes all the methods (i.e. non-publics as well). //The difficulty is in reverse engineering the lookup process that Class.getMethod() does to locate the method with //the given specified arguments. //Note that a similar comment also applied to the code resolving Java fields and constructors, but I've only put the //todo here. status = ResolutionStatus.NO_SUCH_ENTITY; exceptionOrError = e; } catch (final SecurityException e) { status = ResolutionStatus.SECURITY_VIOLATION; exceptionOrError = e; } catch (final NoClassDefFoundError e) { // Class.getMethod() indirectly calls an internal version of Class.getDeclaredMethods(). // This can cause a NoClassDefFoundError to be thrown if any of the Classes used in any methods are not findable. final String notFoundClass = e.getMessage(); if (notFoundClass != null) { status = ResolutionStatus.DEPENDEE_CLASS_NOT_FOUND; associatedMessage = notFoundClass; } else { status = ResolutionStatus.CANNOT_LOAD_CLASS; } exceptionOrError = e; } catch (final LinkageError e) { // Class.getMethod() indirectly calls an internal version of Class.getDeclaredMethods(). // This can cause a LinkageError to be thrown if there is a problem in any of the Classes used in any of the other methods. status = ResolutionStatus.LINKAGE_ERROR; exceptionOrError = e; } // If we actually got a Method object, we need to run more checks on it. if (method != null) { //check that the method is actually static if so specified within CAL. //Note that this is a necessary check since in some degenerate cases this is not caught otherwise. For example: //public TypeExpr getResultType() {...} //within the TypeExpr class //foreign unsafe import jvm "static method org.openquark.cal.compiler.TypeExpr.getResultType" public getResultType :: TypeExpr; if (isStatic != Modifier.isStatic(method.getModifiers())) { if (isStatic) { status = ResolutionStatus.EXPECT_STATIC_FOUND_NON_STATIC; } else { status = ResolutionStatus.EXPECT_NON_STATIC_FOUND_STATIC; } } //check that the actual return type is the same as the declared return type or a subclass of it final Class<?> actualReturnType = method.getReturnType(); if (!declaredReturnType.isAssignableFrom(actualReturnType)) { status = ResolutionStatus.ACTUAL_RETURN_TYPE_DOES_NOT_MATCH_DECLARED_RETURN_TYPE; } } return new ResolutionResult<Method>(method, status, associatedMessage, exceptionOrError); } /** * Resolves the specified field in the given Class object. * @param isStatic true if the Java field is static. * @param foreignClass the class containing the field to be resolved. * @param fieldName the name of the field. * @param declaredReturnType the Class object corresponding to the declared return type of the foreign function. * @return the result of the resolution attempt, which contains a status code and the {@link Field} object, * if the resolution was successful. */ public static ResolutionResult<Field> resolveField(final boolean isStatic, final Class<?> foreignClass, final String fieldName, final Class<?> declaredReturnType) { Field field = null; ResolutionStatus status = ResolutionStatus.SUCCESS; String associatedMessage = null; Throwable exceptionOrError = null; try { field = foreignClass.getField(fieldName); } catch (final NoSuchFieldException e) { status = ResolutionStatus.NO_SUCH_ENTITY; exceptionOrError = e; } catch (final SecurityException e) { status = ResolutionStatus.SECURITY_VIOLATION; exceptionOrError = e; } catch (final NoClassDefFoundError e) { // Class.getField() indirectly calls an internal version of Class.getDeclaredFields(). // This can cause a NoClassDefFoundError to be thrown if any of the Classes used in any fields are not findable. final String notFoundClass = e.getMessage(); if (notFoundClass != null) { status = ResolutionStatus.DEPENDEE_CLASS_NOT_FOUND; associatedMessage = notFoundClass; } else { status = ResolutionStatus.CANNOT_LOAD_CLASS; } exceptionOrError = e; } catch (final LinkageError e) { // Class.getField() indirectly calls an internal version of Class.getDeclaredFields(). // This can cause a LinkageError to be thrown if there is a problem in any of the Classes used in any of the other fields. status = ResolutionStatus.LINKAGE_ERROR; exceptionOrError = e; } // If we actually got a Field object, we need to run more checks on it. if (field != null) { //check that the field is actually static if so specified within CAL. //Note that this is a necessary check since in some degenerate cases this is not caught otherwise. For example: if (isStatic != Modifier.isStatic(field.getModifiers())) { if (isStatic) { status = ResolutionStatus.EXPECT_STATIC_FOUND_NON_STATIC; } else { status = ResolutionStatus.EXPECT_NON_STATIC_FOUND_STATIC; } } //check that the actual return type is the same as the declared return type or a subclass of it final Class<?> actualReturnType = field.getType(); if (!declaredReturnType.isAssignableFrom(actualReturnType)) { status = ResolutionStatus.ACTUAL_RETURN_TYPE_DOES_NOT_MATCH_DECLARED_RETURN_TYPE; } } return new ResolutionResult<Field>(field, status, associatedMessage, exceptionOrError); } /** * Resolves the specified constructor (with the specified argument types) in the given Class object. * @param objectType the class containing the constructor to be resolved. * @param argTypes the types of the arguments of the constructor . * @param declaredReturnType the Class object corresponding to the declared return type of the foreign function. * @return the result of the resolution attempt, which contains a status code and the {@link Constructor} object, * if the resolution was successful. */ public static ResolutionResult<Constructor<?>> resolveConstructor(final Class<?> objectType, final Class<?>[] argTypes, final Class<?> declaredReturnType) { Constructor<?> constructor = null; ResolutionStatus status = ResolutionStatus.SUCCESS; String associatedMessage = null; Throwable exceptionOrError = null; try { constructor = objectType.getConstructor(argTypes); } catch (final NoSuchMethodException e) { status = ResolutionStatus.NO_SUCH_ENTITY; exceptionOrError = e; } catch (final SecurityException e) { status = ResolutionStatus.SECURITY_VIOLATION; exceptionOrError = e; } catch (final NoClassDefFoundError e) { // Class.getConstructor() indirectly calls an internal version of Class.getDeclaredConstructors(). // This can cause a NoClassDefFoundError to be thrown if any of the Classes used in any constructors are not findable. final String notFoundClass = e.getMessage(); if (notFoundClass != null) { status = ResolutionStatus.DEPENDEE_CLASS_NOT_FOUND; associatedMessage = notFoundClass; } else { status = ResolutionStatus.CANNOT_LOAD_CLASS; } exceptionOrError = e; } catch (final LinkageError e) { // Class.getConstructor() indirectly calls an internal version of Class.getDeclaredConstructors(). // This can cause a LinkageError to be thrown if there is a problem in any of the Classes used in any of the other constructors. status = ResolutionStatus.LINKAGE_ERROR; exceptionOrError = e; } //check that the actual return type is the same as the declared return type or a subclass of it if (!declaredReturnType.isAssignableFrom(objectType)) { status = ResolutionStatus.ACTUAL_RETURN_TYPE_DOES_NOT_MATCH_DECLARED_RETURN_TYPE; } return new ResolutionResult<Constructor<?>>(constructor, status, associatedMessage, exceptionOrError); } /** * Resolves the specified class using the given class loader. * @param className the name of the class to be resolved. * @param classLoader the class loader to use for loading the requested class. * @return the result of the resolution attempt, which contains a status code and the {@link Class} object, * if the resolution was successful. */ public static ResolutionResult<Class<?>> resolveClass(final String className, final ClassLoader classLoader) { Class<?> theClass = null; ResolutionStatus status = ResolutionStatus.SUCCESS; String associatedMessage = null; Throwable exceptionOrError = null; try { theClass = Class.forName(className, true, classLoader); } catch (final ClassNotFoundException e) { status = ResolutionStatus.NO_SUCH_ENTITY; exceptionOrError = e; } catch (final NoClassDefFoundError e) { final String notFoundClass = e.getMessage(); if (notFoundClass != null) { status = ResolutionStatus.DEPENDEE_CLASS_NOT_FOUND; associatedMessage = notFoundClass; } else { status = ResolutionStatus.CANNOT_LOAD_CLASS; } exceptionOrError = e; } catch (final ExceptionInInitializerError e) { //Class.forName could also throw an ExceptionInInitializerError. status = ResolutionStatus.CANNOT_INITIALIZE_CLASS; exceptionOrError = e; } catch (final LinkageError e) { status = ResolutionStatus.LINKAGE_ERROR; exceptionOrError = e; } // If everything up until now is okay, then check the access modifier of the class. if (status == ResolutionStatus.SUCCESS) { //Class.forName will successfully locate non-public classes and interfaces. However, these cannot be used successfully //from within CAL since they will cause access violation exceptions when running CAL functions that make use of these types. // //We also exclude classes in an unnamed package. These can't be imported by classes outside the unnamed //package so they effectively cannot be used by a CAL module, since its generated code is always within a named package. if (!Modifier.isPublic(theClass.getModifiers()) || inUnnamedPackage(theClass)) { status = ResolutionStatus.NOT_ACCESSIBLE; } } return new ResolutionResult<Class<?>>(theClass, status, associatedMessage, exceptionOrError); } /** * A helper function to convert Java type names to the format required by Class.forName. * * @param javaSourceName the name of the Java type from CAL source e.g. essentially the fully qualified Java souce name * except that inner classes are separated using a $. * @return the name */ public static String javaSourceReferenceNameToJvmInternalName(final String javaSourceName) { if (primitiveJavaTypesMap.containsKey(javaSourceName) || javaSourceName.equals("void")) { throw new IllegalStateException("the name of a reference type is required"); } //count the number of int arrayDim = 0; for (int currentPos = javaSourceName.length() - 1; currentPos - 1 >= 0 && javaSourceName.charAt(currentPos - 1) == '[' && javaSourceName.charAt(currentPos) == ']'; currentPos -= 2) { ++arrayDim; } if (arrayDim > 0) { final char[] arrayHeader = new char[arrayDim]; Arrays.fill(arrayHeader, '['); final String javaSourceElementTypeName = javaSourceName.substring(0, javaSourceName.length() - 2*arrayDim); final String internalElementTypeName; if (primitiveJavaTypesMap.containsKey(javaSourceElementTypeName)) { //there are special element type names for the primitive types if (javaSourceElementTypeName.equals("boolean")) { internalElementTypeName = "Z"; } else if (javaSourceElementTypeName.equals("byte")) { internalElementTypeName = "B"; } else if (javaSourceElementTypeName.equals("char")) { internalElementTypeName = "C"; } else if (javaSourceElementTypeName.equals("double")) { internalElementTypeName = "D"; } else if (javaSourceElementTypeName.equals("float")) { internalElementTypeName = "F"; } else if (javaSourceElementTypeName.equals("int")) { internalElementTypeName = "I"; } else if (javaSourceElementTypeName.equals("long")) { internalElementTypeName = "J"; } else if (javaSourceElementTypeName.equals("short")) { internalElementTypeName = "S"; } else { throw new IllegalStateException(); } } else { internalElementTypeName = new StringBuilder("L").append(javaSourceElementTypeName).append(";").toString(); } return new StringBuilder(String.valueOf(arrayHeader)).append(internalElementTypeName).toString(); } return javaSourceName; } /** * A helper function that converts from String names of primitive types to their class object. This is need because * Class.forName does not recognize the primitive type names. * @param primitiveJavaTypeName primitive Java type name such as "int" * @return Class the Class object for the primitive type e.g. int.class */ public static Class<?> getPrimitiveType(final String primitiveJavaTypeName) { return primitiveJavaTypesMap.get(primitiveJavaTypeName); } /** * @param type * @return true if the type belongs to an unnamed package (also called "the default package"). * Note that primitive types and primitive array types are not in an unnamed package. */ static private boolean inUnnamedPackage(final Class<?> type) { //If there is no period in the name, and the type is not a primitive or primitive array type //then it is in an unnamed package //the element type e.g. int for int[][][] Class<?> elemType = type; while (elemType.getComponentType() != null) { elemType = elemType.getComponentType(); } if (elemType.isPrimitive()) { return false; } return elemType.getName().indexOf('.') == -1; } }