/* * Copyright 2008 Google Inc. * * Licensed 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 com.google.gwt.user.rebind.rpc; import com.google.gwt.core.ext.GeneratorContext; import com.google.gwt.core.ext.PropertyOracle; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.core.ext.typeinfo.JArrayType; import com.google.gwt.core.ext.typeinfo.JClassType; import com.google.gwt.core.ext.typeinfo.JField; import com.google.gwt.core.ext.typeinfo.JGenericType; import com.google.gwt.core.ext.typeinfo.JParameterizedType; import com.google.gwt.core.ext.typeinfo.JRealClassType; import com.google.gwt.core.ext.typeinfo.JType; import com.google.gwt.core.ext.typeinfo.JTypeParameter; import com.google.gwt.core.ext.typeinfo.JWildcardType; import com.google.gwt.core.ext.typeinfo.NotFoundException; import com.google.gwt.core.ext.typeinfo.TypeOracle; import com.google.gwt.dev.util.log.PrintWriterTreeLogger; import com.google.gwt.user.client.rpc.GwtTransient; import com.google.gwt.user.client.rpc.IsSerializable; import com.google.gwt.user.rebind.rpc.ProblemReport.Priority; import com.google.gwt.user.rebind.rpc.TypeParameterExposureComputer.TypeParameterFlowInfo; import com.google.gwt.user.rebind.rpc.TypePaths.TypePath; import java.io.PrintWriter; import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.Map.Entry; /** * Builds a {@link SerializableTypeOracle} for a given set of root types. * * <p> * There are two goals for this builder. First, discover the set of serializable * types that can be serialized if you serialize one of the root types. Second, * to make sure that all root types can actually be serialized by GWT. * </p> * * <p> * To find the serializable types, it includes the root types, and then it * iteratively traverses the type hierarchy and the fields of any type already * deemed serializable. To improve the accuracy of the traversal there is a * computations of the exposure of type parameters. When the traversal reaches a * parameterized type, these exposure values are used to determine how to treat * the arguments. * </p> * * <p> * A type qualifies for serialization if it or one of its subtypes is * automatically or manually serializable. Automatic serialization is selected * if the type is assignable to {@link IsSerializable} or {@link Serializable} * or if the type is a primitive type such as int, boolean, etc. Manual * serialization is selected if there exists another type with the same fully * qualified name concatenated with "_CustomFieldSerializer". If a type * qualifies for both manual and automatic serialization, manual serialization * is preferred. * </p> * * <p> * Some types may be marked as "enhanced," either automatically by the presence * of a JDO <code>@PersistenceCapable</code> or JPA <code>@Entity</code> tag on * the class definition, or manually by extending the 'rpc.enhancedClasses' * configuration property in the GWT module XML file. For example, to manually * mark the class com.google.myapp.MyPersistentClass as enhanced, use: * * <pre> * <extend-configuration-property name='rpc.enhancedClasses' * value='com.google.myapp.MyPersistentClass'/> * </pre> * * <p> * Enhanced classes are checked for the presence of additional serializable * fields on the server that were not defined in client code as seen by the GWT * compiler. If it is possible for an instance of such a class to be transmitted * bidrectionally between server and client, a special RPC rule is used. The * server-only fields are serialized using standard Java serialization and sent * between the client and server as a blob of opaque base-64 encoded binary * data. When an instance is sent from client to server, the server instance is * populated by invoking setter methods where possible rather than by setting * fields directly. This allows APIs such as JDO the opportunity to update the * object state properly to take into account changes that may have occurred to * the object's state while resident on the client. * </p> */ public class SerializableTypeOracleBuilder { class TypeInfoComputed { /** * <code>true</code> if the type is assignable to {@link IsSerializable} or * {@link java.io.Serializable Serializable}. */ private final boolean autoSerializable; /** * <code>true</code> if the this type directly implements one of the marker * interfaces. */ private final boolean directlyImplementsMarker; /** * <code>true</code> if the type is automatically or manually serializable * and the corresponding checks succeed. */ private boolean fieldSerializable = false; /** * <code>true</code> if this type might be instantiated. */ private boolean instantiable = false; /** * <code>true</code> if there are instantiable subtypes assignable to this * one. */ private boolean instantiableSubtypes; /** * All instantiable types found when this type was quaried, including the * type itself. */ private Set<JClassType> instantiableTypes = new HashSet<JClassType>(); /** * Custom field serializer or <code>null</code> if there isn't one. */ private final JClassType manualSerializer; /** * <code>true</code> if this class might be enhanced on the server to * contain extra fields. */ private final boolean maybeEnhanced; /** * Path used to discover this type. */ private final TypePath path; /** * The state that this type is currently in. */ private TypeState state = TypeState.NOT_CHECKED; /** * {@link JClassType} associated with this metadata. */ private final JType type; public TypeInfoComputed(JType type, TypePath path) { this.type = type; this.path = path; if (type instanceof JClassType) { JClassType typeClass = (JClassType) type; autoSerializable = SerializableTypeOracleBuilder.isAutoSerializable(typeClass); manualSerializer = findCustomFieldSerializer(typeOracle, typeClass); directlyImplementsMarker = directlyImplementsMarkerInterface(typeClass); maybeEnhanced = hasJdoAnnotation(typeClass) || hasJpaAnnotation(typeClass); } else { autoSerializable = false; manualSerializer = null; directlyImplementsMarker = false; maybeEnhanced = false; } } /** * Returns the internal set of instantiable types for this TIC. * Modifications to this set are immediately recorded into the TIC as well. * TODO(spoon) maybe pass the TIC around instead of the set? then there * could be addInstantiableType(JClassType) instead of speccing this to be * mutable. */ public Set<JClassType> getInstantiableTypes() { return instantiableTypes; } public JClassType getManualSerializer() { return manualSerializer; } public TypePath getPath() { return path; } public JType getType() { return type; } public boolean hasInstantiableSubtypes() { return isInstantiable() || instantiableSubtypes || isPendingInstantiable(); } public boolean isAutoSerializable() { return autoSerializable; } public boolean isDeclaredSerializable() { return autoSerializable || isManuallySerializable(); } public boolean isDirectlySerializable() { return directlyImplementsMarker || isManuallySerializable(); } public boolean isDone() { return state == TypeState.CHECK_DONE; } public boolean isFieldSerializable() { return fieldSerializable; } public boolean isInstantiable() { return instantiable; } public boolean isManuallySerializable() { return manualSerializer != null; } public boolean isPendingInstantiable() { return state == TypeState.CHECK_IN_PROGRESS; } public boolean maybeEnhanced() { return maybeEnhanced; } public void setFieldSerializable() { fieldSerializable = true; } public void setInstantiable(boolean instantiable) { this.instantiable = instantiable; if (instantiable) { fieldSerializable = true; } state = TypeState.CHECK_DONE; } public void setInstantiableSubtypes(boolean instantiableSubtypes) { this.instantiableSubtypes = instantiableSubtypes; } public void setPendingInstantiable() { state = TypeState.CHECK_IN_PROGRESS; } } private enum TypeState { /** * The instantiability of a type has been determined. */ CHECK_DONE("Check succeeded"), /** * The instantiability of a type is being checked. */ CHECK_IN_PROGRESS("Check in progress"), /** * The instantiability of a type has not been checked. */ NOT_CHECKED("Not checked"); private final String message; TypeState(String message) { this.message = message; } @Override public String toString() { return message; } } /** * Compares {@link JType}s according to their qualified source names. */ static final Comparator<JType> JTYPE_COMPARATOR = new Comparator<JType>() { @Override public int compare(JType t1, JType t2) { return t1.getQualifiedSourceName().compareTo(t2.getQualifiedSourceName()); } }; /** * No type filtering by default.. */ private static final TypeFilter DEFAULT_TYPE_FILTER = new TypeFilter() { @Override public String getName() { return "Default"; } @Override public boolean isAllowed(JClassType type) { return true; } }; /** * A reference to the annotation class * javax.jdo.annotations.PersistenceCapable used by the JDO API. May be null * if JDO is not present in the runtime environment. */ private static Class<? extends Annotation> JDO_PERSISTENCE_CAPABLE_ANNOTATION = null; /** * A reference to the method 'String * javax.jdo.annotations.PersistenceCapable.detachable()'. */ private static Method JDO_PERSISTENCE_CAPABLE_DETACHABLE_METHOD; /** * A reference to the annotation class javax.persistence.Entity used by the * JPA API. May be null if JPA is not present in the runtime environment. */ private static Class<? extends Annotation> JPA_ENTITY_ANNOTATION = null; static { try { JDO_PERSISTENCE_CAPABLE_ANNOTATION = Class.forName("javax.jdo.annotations.PersistenceCapable").asSubclass(Annotation.class); JDO_PERSISTENCE_CAPABLE_DETACHABLE_METHOD = JDO_PERSISTENCE_CAPABLE_ANNOTATION.getDeclaredMethod("detachable", (Class[]) null); } catch (ClassNotFoundException e) { // Ignore, JDO_PERSISTENCE_CAPABLE_ANNOTATION will be null } catch (NoSuchMethodException e) { JDO_PERSISTENCE_CAPABLE_ANNOTATION = null; } try { JPA_ENTITY_ANNOTATION = Class.forName("javax.persistence.Entity").asSubclass(Annotation.class); } catch (ClassNotFoundException e) { // Ignore, JPA_ENTITY_CAPABLE_ANNOTATION will be null } } static boolean canBeInstantiated(JClassType type, ProblemReport problems) { if (type.isEnum() == null) { if (type.isAbstract()) { // Abstract types will be picked up if there is an instantiable // subtype. return false; } if (!type.isDefaultInstantiable() && !isManuallySerializable(type)) { // Warn and return false. problems.add(type, type.getParameterizedQualifiedSourceName() + " is not default instantiable (it must have a zero-argument " + "constructor or no constructors at all) and has no custom " + "serializer.", Priority.DEFAULT); return false; } } else { /* * Enums are always instantiable regardless of abstract or default * instantiability. */ } return true; } /** * Finds the custom field serializer for a given type. * * @param typeOracle * @param type * @return the custom field serializer for a type or <code>null</code> if * there is not one */ public static JClassType findCustomFieldSerializer(TypeOracle typeOracle, JType type) { JClassType classOrInterface = type.isClassOrInterface(); if (classOrInterface == null) { return null; } String customFieldSerializerName = getCustomFieldSerializerName(type.getQualifiedSourceName()); return findCustomFieldSerializer(typeOracle, customFieldSerializerName); } /** * Finds the custom field serializer for a given qualified source name. * * @param typeOracle * @param customFieldSerializerName * @return the custom field serializer for a type of <code>null</code> if * there is not one */ public static JClassType findCustomFieldSerializer(TypeOracle typeOracle, String customFieldSerializerName) { JClassType customSerializer = typeOracle.findType(customFieldSerializerName); if (customSerializer == null) { // If the type is in the java.lang or java.util packages then it will be // mapped into com.google.gwt.user.client.rpc.core package customSerializer = typeOracle.findType("com.google.gwt.user.client.rpc.core." + customFieldSerializerName); } return customSerializer; } /** * Returns the name for a custom field serializer, given a source name. * * @param sourceName * @return the custom field serializer type name for a given source name. */ public static String getCustomFieldSerializerName(String sourceName) { return sourceName + "_CustomFieldSerializer"; } static JRealClassType getBaseType(JClassType type) { if (type.isParameterized() != null) { return type.isParameterized().getBaseType(); } else if (type.isRawType() != null) { return type.isRawType().getBaseType(); } return (JRealClassType) type; } static boolean hasGwtTransientAnnotation(JField field) { for (Annotation a : field.getAnnotations()) { if (a.annotationType().getSimpleName().equals(GwtTransient.class.getSimpleName())) { return true; } } return false; } /** * @param type the type to query * @return true if the type is annotated with @PersistenceCapable(..., * detachable="true") */ static boolean hasJdoAnnotation(JClassType type) { if (JDO_PERSISTENCE_CAPABLE_ANNOTATION == null) { return false; } Annotation annotation = type.getAnnotation(JDO_PERSISTENCE_CAPABLE_ANNOTATION); if (annotation == null) { return false; } try { Object value = JDO_PERSISTENCE_CAPABLE_DETACHABLE_METHOD.invoke(annotation, (Object[]) null); if (value instanceof String) { return "true".equalsIgnoreCase((String) value); } else { return false; } } catch (IllegalAccessException e) { // will return false } catch (InvocationTargetException e) { // will return false } return false; } /** * @param type the type to query * @return true if the type is annotated with @Entity */ static boolean hasJpaAnnotation(JClassType type) { if (JPA_ENTITY_ANNOTATION == null) { return false; } Annotation annotation = type.getAnnotation(JPA_ENTITY_ANNOTATION); return annotation != null; } static boolean isAutoSerializable(JClassType type) { try { JClassType isSerializable = getIsSerializableMarkerInterface(type); JClassType serializable = getSerializableMarkerInterface(type); return type.isAssignableTo(isSerializable) || type.isAssignableTo(serializable); } catch (NotFoundException e) { return false; } } /** * Returns <code>true</code> if this type is part of the standard java * packages. */ static boolean isInStandardJavaPackage(String className) { if (className.startsWith("java.")) { return true; } if (className.startsWith("javax.")) { return true; } return false; } static void recordTypeParametersIn(JType type, Set<JTypeParameter> params) { JTypeParameter isTypeParameter = type.isTypeParameter(); if (isTypeParameter != null) { params.add(isTypeParameter); } JArrayType isArray = type.isArray(); if (isArray != null) { recordTypeParametersIn(isArray.getComponentType(), params); } JWildcardType isWildcard = type.isWildcard(); if (isWildcard != null) { for (JClassType bound : isWildcard.getUpperBounds()) { recordTypeParametersIn(bound, params); } } JParameterizedType isParameterized = type.isParameterized(); if (isParameterized != null) { for (JClassType arg : isParameterized.getTypeArgs()) { recordTypeParametersIn(arg, params); } } } /** * Return <code>true</code> if a class's fields should be considered for * serialization. If it returns <code>false</code> then none of the fields of * this class should be serialized. */ static boolean shouldConsiderFieldsForSerialization(JClassType type, TypeFilter filter, ProblemReport problems) { if (!isAllowedByFilter(filter, type, problems)) { return false; } if (!isDeclaredSerializable(type)) { problems.add(type, type.getParameterizedQualifiedSourceName() + " is not assignable to '" + IsSerializable.class.getName() + "' or '" + Serializable.class.getName() + "' nor does it have a custom field serializer", Priority.DEFAULT); return false; } if (isManuallySerializable(type)) { JClassType manualSerializer = findCustomFieldSerializer(type.getOracle(), type); assert (manualSerializer != null); List<String> fieldProblems = CustomFieldSerializerValidator.validate(manualSerializer, type); if (!fieldProblems.isEmpty()) { for (String problem : fieldProblems) { problems.add(type, problem, Priority.FATAL); } return false; } } else { assert (isAutoSerializable(type)); if (!isAccessibleToSerializer(type)) { // Class is not visible to a serializer class in the same package problems.add(type, type.getParameterizedQualifiedSourceName() + " is not accessible from a class in its same package; it " + "will be excluded from the set of serializable types", Priority.DEFAULT); return false; } if (type.isMemberType() && !type.isStatic()) { // Non-static member types cannot be serialized problems.add(type, type.getParameterizedQualifiedSourceName() + " is nested but " + "not static; it will be excluded from the set of serializable " + "types", Priority.DEFAULT); return false; } } return true; } /** * Returns <code>true</code> if the field qualifies for serialization without * considering its type. */ static boolean shouldConsiderForSerialization(TreeLogger logger, boolean suppressNonStaticFinalFieldWarnings, JField field) { if (field.isStatic() || field.isTransient()) { return false; } if (hasGwtTransientAnnotation(field)) { return false; } if (field.isFinal()) { TreeLogger.Type logLevel; if (isManuallySerializable(field.getEnclosingType())) { /* * If the type has a custom serializer, assume the programmer knows * best. */ logLevel = TreeLogger.DEBUG; } else { logLevel = TreeLogger.WARN; } logger.branch(suppressNonStaticFinalFieldWarnings ? TreeLogger.DEBUG : logLevel, "Field '" + field.toString() + "' will not be serialized because it is final", null); return false; } return true; } private static boolean directlyImplementsMarkerInterface(JClassType type) { try { return TypeHierarchyUtils.directlyImplementsInterface(type, getIsSerializableMarkerInterface(type)) || TypeHierarchyUtils.directlyImplementsInterface(type, getSerializableMarkerInterface(type)); } catch (NotFoundException e) { return false; } } private static JArrayType getArrayType(TypeOracle typeOracle, int rank, JType component) { assert (rank > 0); JArrayType array = null; JType currentComponent = component; for (int i = 0; i < rank; ++i) { array = typeOracle.getArrayType(currentComponent); currentComponent = array; } return array; } private static JClassType getIsSerializableMarkerInterface(JClassType type) throws NotFoundException { return type.getOracle().getType(IsSerializable.class.getName()); } private static JClassType getSerializableMarkerInterface(JClassType type) throws NotFoundException { return type.getOracle().getType(Serializable.class.getName()); } /** * Returns <code>true</code> if a serializer class could access this type. */ private static boolean isAccessibleToSerializer(JClassType type) { if (type.isPrivate()) { return false; } if (isInStandardJavaPackage(type.getQualifiedSourceName())) { if (!type.isPublic()) { return false; } } if (type.isMemberType()) { return isAccessibleToSerializer(type.getEnclosingType()); } return true; } private static boolean isAllowedByFilter(TypeFilter filter, JClassType classType, ProblemReport problems) { if (!filter.isAllowed(classType)) { problems.add(classType, classType.getParameterizedQualifiedSourceName() + " is excluded by type filter ", Priority.AUXILIARY); return false; } return true; } private static boolean isDeclaredSerializable(JClassType type) { return isAutoSerializable(type) || isManuallySerializable(type); } private static boolean isDirectlySerializable(JClassType type) { return directlyImplementsMarkerInterface(type) || isManuallySerializable(type); } private static boolean isManuallySerializable(JClassType type) { return findCustomFieldSerializer(type.getOracle(), type) != null; } private static void logSerializableTypes(TreeLogger logger, Set<JClassType> fieldSerializableTypes) { if (!logger.isLoggable(TreeLogger.DEBUG)) { return; } TreeLogger localLogger = logger.branch(TreeLogger.DEBUG, "Identified " + fieldSerializableTypes.size() + " serializable type" + ((fieldSerializableTypes.size() == 1) ? "" : "s"), null); for (JClassType fieldSerializableType : fieldSerializableTypes) { localLogger.branch(TreeLogger.DEBUG, fieldSerializableType .getParameterizedQualifiedSourceName(), null); } } private boolean alreadyCheckedObject; /** * Cache of the {@link JClassType} for {@link Collection}. */ private final JGenericType collectionClass; private final GeneratorContext context; private Set<String> enhancedClasses = null; private PrintWriter logOutputWriter; /** * Cache of the {@link JClassType} for {@link Map}. */ private final JGenericType mapClass; private final Map<JClassType, TreeLogger> rootTypes = new LinkedHashMap<JClassType, TreeLogger>(); /** * If <code>true</code> we will not warn if a serializable type contains a * non-static final field. We warn because these fields are not serialized. */ private final boolean suppressNonStaticFinalFieldWarnings; private final TypeConstrainer typeConstrainer; private TypeFilter typeFilter = DEFAULT_TYPE_FILTER; private final TypeOracle typeOracle; private final TypeParameterExposureComputer typeParameterExposureComputer = new TypeParameterExposureComputer(typeFilter); /** * The set of type parameters that appear in one of the root types. * TODO(spoon): It would be cleaner to delete this field, and instead to have * {@link #addRootType(TreeLogger, JType)} replace parameters with wildcard * types. Then the root types would not contain any parameters. */ private Set<JTypeParameter> typeParametersInRootTypes = new HashSet<JTypeParameter>(); /** * Map of {@link JType} to {@link TypeInfoComputed}. */ private final Map<JType, TypeInfoComputed> typeToTypeInfoComputed = new HashMap<JType, TypeInfoComputed>(); /** * Constructs a builder. * * @param logger * @param propertyOracle * @param context * * @throws UnableToCompleteException if we fail to find one of our special * types */ public SerializableTypeOracleBuilder(TreeLogger logger, PropertyOracle propertyOracle, GeneratorContext context) throws UnableToCompleteException { this.context = context; this.typeOracle = context.getTypeOracle(); typeConstrainer = new TypeConstrainer(typeOracle); try { collectionClass = typeOracle.getType(Collection.class.getName()).isGenericType(); mapClass = typeOracle.getType(Map.class.getName()).isGenericType(); } catch (NotFoundException e) { logger.log(TreeLogger.ERROR, null, e); throw new UnableToCompleteException(); } suppressNonStaticFinalFieldWarnings = Shared.shouldSuppressNonStaticFinalFieldWarnings(logger, propertyOracle); enhancedClasses = Shared.getEnhancedTypes(propertyOracle); } public void addRootType(TreeLogger logger, JType type) { if (type.isPrimitive() != null) { return; } JClassType clazz = (JClassType) type; if (!rootTypes.containsKey(clazz)) { recordTypeParametersIn(type, typeParametersInRootTypes); rootTypes.put(clazz, logger); } else { if (logger.isLoggable(TreeLogger.TRACE)) { logger.log(TreeLogger.TRACE, clazz.getParameterizedQualifiedSourceName() + " is already a root type."); } } } /** * Builds a {@link SerializableTypeOracle} for a given set of root types. * * @param logger * @return a {@link SerializableTypeOracle} for the specified set of root * types * * @throws UnableToCompleteException if there was not at least one * instantiable type assignable to each of the specified root types */ public SerializableTypeOracle build(TreeLogger logger) throws UnableToCompleteException { alreadyCheckedObject = false; boolean allSucceeded = true; for (Entry<JClassType, TreeLogger> entry : rootTypes.entrySet()) { ProblemReport problems = new ProblemReport(); problems.setContextType(entry.getKey()); boolean entrySucceeded = computeTypeInstantiability(entry.getValue(), entry.getKey(), TypePaths.createRootPath(entry.getKey()), problems).hasInstantiableSubtypes(); if (!entrySucceeded) { problems.report(logger, TreeLogger.ERROR, TreeLogger.INFO); } else { if (problems.hasFatalProblems()) { entrySucceeded = false; problems.reportFatalProblems(logger, TreeLogger.ERROR); } // if entrySucceeded, there may still be "warning" problems, but too // often they're expected (e.g. non-instantiable subtypes of List), so // we log them at DEBUG. // TODO(fabbott): we could blacklist or graylist those types here, so // instantiation during code generation would flag them for us. problems.report(logger, TreeLogger.DEBUG, TreeLogger.DEBUG); } allSucceeded &= entrySucceeded; } if (!allSucceeded) { throw new UnableToCompleteException(); } for (TypeInfoComputed tic : typeToTypeInfoComputed.values()) { assert (!tic.isPendingInstantiable()); } pruneUnreachableTypes(); logReachableTypes(logger); Set<JClassType> possiblyInstantiatedTypes = new TreeSet<JClassType>(JTYPE_COMPARATOR); Set<JClassType> fieldSerializableTypes = new TreeSet<JClassType>(JTYPE_COMPARATOR); for (TypeInfoComputed tic : typeToTypeInfoComputed.values()) { if (!(tic.getType() instanceof JClassType)) { continue; } JClassType type = (JClassType) tic.getType(); type = type.getErasedType(); if (tic.isInstantiable()) { assert (!type.isAbstract() || type.isEnum() != null); possiblyInstantiatedTypes.add(type); } if (tic.isFieldSerializable()) { assert (type.isInterface() == null); fieldSerializableTypes.add(type); } if (tic.maybeEnhanced() || (enhancedClasses != null && enhancedClasses.contains(type.getQualifiedSourceName()))) { type.setEnhanced(); } } logSerializableTypes(logger, fieldSerializableTypes); return new SerializableTypeOracleImpl(fieldSerializableTypes, possiblyInstantiatedTypes); } /** * Set the {@link PrintWriter} which will receive a detailed log of the types * which were examined in order to determine serializability. */ public void setLogOutputWriter(PrintWriter logOutputWriter) { this.logOutputWriter = logOutputWriter; } public void setTypeFilter(TypeFilter typeFilter) { this.typeFilter = typeFilter; typeParameterExposureComputer.setTypeFilter(typeFilter); } /** * This method determines information about serializing a type with GWT. To do * so, it must traverse all subtypes as well as all field types of those * types, transitively. * * It returns a {@link TypeInfoComputed} with the information found. * * As a side effect, all types needed--plus some--to serialize this type are * accumulated in {@link #typeToTypeInfoComputed}. In particular, there will * be an entry for any type that has been validated by this method, as a * shortcircuit to avoid recomputation. * * The method is exposed using default access to enable testing. */ TypeInfoComputed computeTypeInstantiability(TreeLogger logger, JType type, TypePath path, ProblemReport problems) { assert (type != null); if (type.isPrimitive() != null) { TypeInfoComputed tic = getTypeInfoComputed(type, path, true); tic.setInstantiableSubtypes(true); tic.setInstantiable(false); return tic; } assert (type instanceof JClassType); JClassType classType = (JClassType) type; TypeInfoComputed tic = getTypeInfoComputed(classType, path, false); if (tic != null && tic.isDone()) { // we have an answer already; use it. return tic; } TreeLogger localLogger = logger.branch(TreeLogger.DEBUG, classType.getParameterizedQualifiedSourceName(), null); JTypeParameter isTypeParameter = classType.isTypeParameter(); if (isTypeParameter != null) { if (typeParametersInRootTypes.contains(isTypeParameter)) { return computeTypeInstantiability(localLogger, isTypeParameter.getFirstBound(), TypePaths .createTypeParameterInRootPath(path, isTypeParameter), problems); } /* * This type parameter was not in a root type and therefore it is the * caller's responsibility to deal with it. We assume that it is * indirectly instantiable here. */ tic = getTypeInfoComputed(classType, path, true); tic.setInstantiableSubtypes(true); tic.setInstantiable(false); return tic; } JWildcardType isWildcard = classType.isWildcard(); if (isWildcard != null) { boolean success = true; for (JClassType bound : isWildcard.getUpperBounds()) { success &= computeTypeInstantiability(localLogger, bound, path, problems) .hasInstantiableSubtypes(); } tic = getTypeInfoComputed(classType, path, true); tic.setInstantiableSubtypes(success); tic.setInstantiable(false); return tic; } JArrayType isArray = classType.isArray(); if (isArray != null) { TypeInfoComputed arrayTic = checkArrayInstantiable(localLogger, isArray, path, problems); assert getTypeInfoComputed(classType, path, false) != null; return arrayTic; } if (classType == typeOracle.getJavaLangObject()) { /* * Report an error if the type is or erases to Object since this violates * our restrictions on RPC. Should be fatal, but I worry users may have * Object-using code they can't readily get out of the class hierarchy. */ problems.add(classType, "In order to produce smaller client-side code, 'Object' is not " + "allowed; please use a more specific type", Priority.DEFAULT); tic = getTypeInfoComputed(classType, path, true); tic.setInstantiable(false); return tic; } if (classType.isRawType() != null) { localLogger .log( TreeLogger.DEBUG, "Type '" + classType.getQualifiedSourceName() + "' should be parameterized to help the compiler produce the smallest code size possible for your module", null); } JClassType originalType = (JClassType) type; // TreeLogger subtypesLogger = localLogger.branch(TreeLogger.DEBUG, // "Analyzing subclasses:", null); tic = getTypeInfoComputed(classType, path, true); boolean anySubtypes = checkSubtypes(localLogger, originalType, tic.getInstantiableTypes(), path, problems); if (!tic.isDone()) { tic.setInstantiableSubtypes(anySubtypes); tic.setInstantiable(false); } return tic; } int getTypeParameterExposure(JGenericType type, int index) { return getFlowInfo(type, index).getExposure(); } /** * Returns <code>true</code> if the fields of the type should be considered * for serialization. * * Default access to allow for testing. */ boolean shouldConsiderFieldsForSerialization(JClassType type, ProblemReport problems) { return shouldConsiderFieldsForSerialization(type, typeFilter, problems); } /** * Consider any subtype of java.lang.Object which qualifies for serialization. * * @param logger */ private void checkAllSubtypesOfObject(TreeLogger logger, TypePath parent, ProblemReport problems) { if (alreadyCheckedObject) { return; } alreadyCheckedObject = true; /* * This will pull in the world and the set of serializable types will be * larger than it needs to be. We exclude types that do not qualify for * serialization to avoid generating false errors due to types that do not * qualify for serialization and have no serializable subtypes. */ TreeLogger localLogger = logger.branch(TreeLogger.WARN, "Checking all subtypes of Object which qualify for serialization", null); JClassType[] allTypes = typeOracle.getJavaLangObject().getSubtypes(); for (JClassType cls : allTypes) { if (isDeclaredSerializable(cls)) { computeTypeInstantiability(localLogger, cls, TypePaths.createSubtypePath(parent, cls, typeOracle.getJavaLangObject()), problems); } } } private TypeInfoComputed checkArrayInstantiable(TreeLogger logger, JArrayType array, TypePath path, ProblemReport problems) { JType leafType = array.getLeafType(); JWildcardType leafWild = leafType.isWildcard(); if (leafWild != null) { JArrayType arrayType = getArrayType(typeOracle, array.getRank(), leafWild.getUpperBound()); return checkArrayInstantiable(logger, arrayType, path, problems); } JClassType leafClass = leafType.isClassOrInterface(); JTypeParameter isLeafTypeParameter = leafType.isTypeParameter(); if (isLeafTypeParameter != null && !typeParametersInRootTypes.contains(isLeafTypeParameter)) { // Don't deal with non root type parameters, but make a TIC entry to // save time if it recurs. We assume they're indirectly instantiable. TypeInfoComputed tic = getTypeInfoComputed(array, path, true); tic.setInstantiableSubtypes(true); tic.setInstantiable(false); return tic; } if (!isAllowedByFilter(array, problems)) { // Don't deal with filtered out types either, but make a TIC entry to // save time if it recurs. We assume they're not instantiable. TypeInfoComputed tic = getTypeInfoComputed(array, path, true); tic.setInstantiable(false); return tic; } TypeInfoComputed tic = getTypeInfoComputed(array, path, true); if (tic.isDone()) { return tic; } else if (tic.isPendingInstantiable()) { return tic; } tic.setPendingInstantiable(); TreeLogger branch = logger.branch(TreeLogger.DEBUG, "Analyzing component type:", null); TypeInfoComputed leafTic = computeTypeInstantiability(branch, leafType, TypePaths .createArrayComponentPath(array, path), problems); boolean succeeded = leafTic.hasInstantiableSubtypes(); if (succeeded) { if (leafClass == null) { assert leafType.isPrimitive() != null; markArrayTypesInstantiable(leafType, array.getRank(), path); } else { TreeLogger covariantArrayLogger = logger.branch(TreeLogger.DEBUG, "Covariant array types"); /* * Compute covariant arrays for arrays of reference types. */ for (JClassType instantiableType : TypeHierarchyUtils.getAllTypesBetweenRootTypeAndLeaves( leafClass, leafTic.getInstantiableTypes())) { if (!isAccessibleToSerializer(instantiableType)) { // Skip types that are not accessible from a serializer continue; } if (covariantArrayLogger.isLoggable(TreeLogger.DEBUG)) { covariantArrayLogger.branch(TreeLogger.DEBUG, getArrayType(typeOracle, array.getRank(), instantiableType).getParameterizedQualifiedSourceName()); } markArrayTypesInstantiable(instantiableType, array.getRank(), path); } } } tic.setInstantiable(succeeded); return tic; } /** * Returns <code>true</code> if the declared fields of this type are all * instantiable. As a side-effect it fills in {@link TypeInfoComputed} for all * necessary types. */ private boolean checkDeclaredFields(TreeLogger logger, TypeInfoComputed typeInfo, TypePath parent, ProblemReport problems) { JClassType classOrInterface = (JClassType) typeInfo.getType(); if (classOrInterface.isEnum() != null) { // The fields of an enum are never serialized; they are always okay. return true; } JClassType baseType = getBaseType(classOrInterface); boolean allSucceeded = true; // TODO: Propagating the constraints will produce better results as long // as infinite expansion can be avoided in the process. JField[] fields = baseType.getFields(); if (fields.length > 0) { TreeLogger localLogger = logger.branch(TreeLogger.DEBUG, "Analyzing the fields of type '" + classOrInterface.getParameterizedQualifiedSourceName() + "' that qualify for serialization", null); for (JField field : fields) { if (!shouldConsiderForSerialization(localLogger, suppressNonStaticFinalFieldWarnings, field)) { continue; } TreeLogger fieldLogger = localLogger.branch(TreeLogger.DEBUG, field.toString(), null); JType fieldType = field.getType(); TypePath path = TypePaths.createFieldPath(parent, field); if (typeInfo.isManuallySerializable() && fieldType.getLeafType() == typeOracle.getJavaLangObject()) { checkAllSubtypesOfObject(fieldLogger.branch(TreeLogger.WARN, "Object was reached from a manually serializable type", null), path, problems); } else { allSucceeded &= computeTypeInstantiability(fieldLogger, fieldType, path, problems) .hasInstantiableSubtypes(); } } } boolean succeeded = allSucceeded || typeInfo.isManuallySerializable(); if (succeeded) { typeInfo.setFieldSerializable(); } return succeeded; } private boolean checkSubtype(TreeLogger logger, JClassType classOrInterface, JClassType originalType, TypePath parent, ProblemReport problems) { if (classOrInterface.isEnum() != null) { // The fields of an enum are never serialized; they are always okay. return true; } JParameterizedType isParameterized = classOrInterface.isParameterized(); if (isParameterized != null) { if (isRawMapOrRawCollection(classOrInterface)) { /* * Backwards compatibility. Raw collections or maps force all object * subtypes to be considered. */ checkAllSubtypesOfObject(logger, parent, problems); } else { TreeLogger paramsLogger = logger.branch(TreeLogger.DEBUG, "Checking parameters of '" + isParameterized.getParameterizedQualifiedSourceName() + "'"); for (JTypeParameter param : isParameterized.getBaseType().getTypeParameters()) { if (!checkTypeArgument(paramsLogger, isParameterized.getBaseType(), param.getOrdinal(), isParameterized.getTypeArgs()[param.getOrdinal()], parent, problems)) { return false; } } } } // Check all super type fields first (recursively). JClassType superType = classOrInterface.getSuperclass(); if (superType != null && superType.isRawType() != null) { superType = superType.isRawType().asParameterizedByWildcards(); } if (superType != null && isDeclaredSerializable(superType)) { superType = constrainTypeBy(superType, originalType); if (superType == null) { return false; } boolean superTypeOk = false; superTypeOk = checkSubtype(logger, superType, originalType, TypePaths.createSupertypePath(parent, superType, classOrInterface), problems); /* * If my super type did not check out, then I am not instantiable and we * should error out... UNLESS I amdirectly serializable myself, in which * case it's ok for me to be the root of a new instantiable hierarchy. */ if (!superTypeOk && !isDirectlySerializable(classOrInterface)) { return false; } } TypeInfoComputed tic = getTypeInfoComputed(classOrInterface, parent, true); return checkDeclaredFields(logger, tic, parent, problems); } /** * Returns <code>true</code> if this type or one of its subtypes is * instantiable relative to a known base type. */ private boolean checkSubtypes(TreeLogger logger, JClassType originalType, Set<JClassType> instSubtypes, TypePath path, ProblemReport problems) { JClassType baseType = getBaseType(originalType); TreeLogger computationLogger = logger.branch(TreeLogger.DEBUG, "Finding possibly instantiable subtypes"); List<JClassType> candidates = getPossiblyInstantiableSubtypes(computationLogger, baseType, problems); boolean anySubtypes = false; TreeLogger verificationLogger = logger.branch(TreeLogger.DEBUG, "Verifying instantiability"); for (JClassType candidate : candidates) { if (getBaseType(candidate) == baseType && originalType.isRawType() == null) { // Don't rely on the constrainer when we have perfect information. candidate = originalType; } else { candidate = constrainTypeBy(candidate, originalType); if (candidate == null) { continue; } } if (!isAllowedByFilter(candidate, problems)) { continue; } TypePath subtypePath = TypePaths.createSubtypePath(path, candidate, originalType); TypeInfoComputed tic = getTypeInfoComputed(candidate, subtypePath, true); if (tic.isDone()) { if (tic.isInstantiable()) { anySubtypes = true; instSubtypes.add(candidate); } continue; } else if (tic.isPendingInstantiable()) { anySubtypes = true; instSubtypes.add(candidate); continue; } tic.setPendingInstantiable(); TreeLogger subtypeLogger = verificationLogger.branch(TreeLogger.DEBUG, candidate .getParameterizedQualifiedSourceName()); boolean instantiable = checkSubtype(subtypeLogger, candidate, originalType, subtypePath, problems); anySubtypes |= instantiable; tic.setInstantiable(instantiable); if (instantiable) { subtypeLogger.branch(TreeLogger.DEBUG, "Is instantiable"); } // Note we are leaving hasInstantiableSubtypes() as false which might be // wrong but it is only used by arrays and thus it will never be looked at // for this tic. if (instantiable && instSubtypes != null) { instSubtypes.add(candidate); } } return anySubtypes; } /** * Check the argument to a parameterized type to see if it will make the type * it is applied to be serializable. As a side effect, populates * {@link #typeToTypeInfoComputed} in the same way as * {@link #computeTypeInstantiability}. * * @param logger * @param baseType - The generic type the parameter is on * @param paramIndex - The index of the parameter in the generic type * @param typeArg - An upper bound on the actual argument being applied to the * generic type * * @return Whether the a parameterized type can be serializable if * <code>baseType</code> is the base type and the * <code>paramIndex</code>th type argument is a subtype of * <code>typeArg</code>. */ private boolean checkTypeArgument(TreeLogger logger, JGenericType baseType, int paramIndex, JClassType typeArg, TypePath parent, ProblemReport problems) { JWildcardType isWildcard = typeArg.isWildcard(); if (isWildcard != null) { return checkTypeArgument(logger, baseType, paramIndex, isWildcard.getUpperBound(), parent, problems); } JArrayType typeArgAsArray = typeArg.isArray(); if (typeArgAsArray != null) { JTypeParameter parameterOfTypeArgArray = typeArgAsArray.getLeafType().isTypeParameter(); if (parameterOfTypeArgArray != null) { JGenericType declaringClass = parameterOfTypeArgArray.getDeclaringClass(); if (declaringClass != null) { TypeParameterFlowInfo flowInfoForArrayParam = getFlowInfo(declaringClass, parameterOfTypeArgArray.getOrdinal()); TypeParameterFlowInfo otherFlowInfo = getFlowInfo(baseType, paramIndex); if (otherFlowInfo.getExposure() >= 0 && otherFlowInfo.isTransitivelyAffectedBy(flowInfoForArrayParam)) { problems.add(baseType, "Cannot serialize type '" + baseType.getParameterizedQualifiedSourceName() + "' when given an argument of type '" + typeArg.getParameterizedQualifiedSourceName() + "' because it appears to require serializing arrays " + "of unbounded dimension", Priority.DEFAULT); return false; } } } } TypePath path = TypePaths.createTypeArgumentPath(parent, baseType, paramIndex, typeArg); int exposure = getTypeParameterExposure(baseType, paramIndex); switch (exposure) { case TypeParameterExposureComputer.EXPOSURE_DIRECT: { TreeLogger branch = logger.branch(TreeLogger.DEBUG, "Checking type argument " + paramIndex + " of type '" + baseType.getParameterizedQualifiedSourceName() + "' because it is directly exposed in this type or in one of its subtypes"); return computeTypeInstantiability(branch, typeArg, path, problems) .hasInstantiableSubtypes() || mightNotBeExposed(baseType, paramIndex); } case TypeParameterExposureComputer.EXPOSURE_NONE: // Ignore this argument logger.log(TreeLogger.DEBUG, "Ignoring type argument " + paramIndex + " of type '" + baseType.getParameterizedQualifiedSourceName() + "' because it is not exposed in this or any subtype"); return true; default: { assert (exposure >= TypeParameterExposureComputer.EXPOSURE_MIN_BOUNDED_ARRAY); problems.add(getArrayType(typeOracle, exposure, typeArg), "Checking type argument " + paramIndex + " of type '" + baseType.getParameterizedQualifiedSourceName() + "' because it is exposed as an array with a maximum dimension of " + exposure + " in this type or one of its subtypes", Priority.AUXILIARY); return computeTypeInstantiability(logger, getArrayType(typeOracle, exposure, typeArg), path, problems).hasInstantiableSubtypes() || mightNotBeExposed(baseType, paramIndex); } } } /** * If <code>type</code>'s base class does not inherit from * <code>superType</code>'s base class, then return <code>type</code> * unchanged. If it does, then return a subtype of <code>type</code> that * includes all values in both <code>type</code> and <code>superType</code>. * If there are definitely no such values, return <code>null</code>. */ private JClassType constrainTypeBy(JClassType type, JClassType superType) { return typeConstrainer.constrainTypeBy(type, superType); } private TypeParameterFlowInfo getFlowInfo(JGenericType type, int index) { return typeParameterExposureComputer.computeTypeParameterExposure(type, index); } /** * Returns the subtypes of a given base type as parameterized by wildcards. */ private List<JClassType> getPossiblyInstantiableSubtypes(TreeLogger logger, JClassType baseType, ProblemReport problems) { assert (baseType == getBaseType(baseType)); List<JClassType> possiblyInstantiableTypes = new ArrayList<JClassType>(); if (baseType == typeOracle.getJavaLangObject()) { return possiblyInstantiableTypes; } List<JClassType> candidates = new ArrayList<JClassType>(); candidates.add(baseType); List<JClassType> subtypes = Arrays.asList(baseType.getSubtypes()); candidates.addAll(subtypes); for (JClassType subtype : candidates) { JClassType subtypeBase = getBaseType(subtype); if (maybeInstantiable(logger, subtypeBase, problems)) { /* * Convert the generic type into a parameterization that only includes * wildcards. */ JGenericType isGeneric = subtype.isGenericType(); if (isGeneric != null) { subtype = isGeneric.asParameterizedByWildcards(); } else { assert (subtype instanceof JRealClassType); } possiblyInstantiableTypes.add(subtype); } } if (possiblyInstantiableTypes.size() == 0) { String possibilities[] = new String[candidates.size()]; for (int i = 0; i < possibilities.length; i++) { JClassType subtype = candidates.get(i); String worstMessage = problems.getWorstMessageForType(subtype); if (worstMessage == null) { possibilities[i] = " subtype " + subtype.getParameterizedQualifiedSourceName() + " is not instantiable"; } else { // message with have the form "FQCN some-problem-description" possibilities[i] = " subtype " + worstMessage; } } problems.add(baseType, baseType.getParameterizedQualifiedSourceName() + " has no available instantiable subtypes.", Priority.DEFAULT, possibilities); } return possiblyInstantiableTypes; } private TypeInfoComputed getTypeInfoComputed(JType type, TypePath path, boolean createIfNeeded) { TypeInfoComputed tic = typeToTypeInfoComputed.get(type); if (tic == null && createIfNeeded) { tic = new TypeInfoComputed(type, path); typeToTypeInfoComputed.put(type, tic); } return tic; } private boolean isAllowedByFilter(JClassType classType, ProblemReport problems) { return isAllowedByFilter(typeFilter, classType, problems); } /** * Returns <code>true</code> if the type is Collection<? extends Object> or * Map<? extends Object, ? extends Object>. */ private boolean isRawMapOrRawCollection(JClassType type) { if (type.asParameterizationOf(collectionClass) == collectionClass.asParameterizedByWildcards()) { return true; } if (type.asParameterizationOf(mapClass) == mapClass.asParameterizedByWildcards()) { return true; } return false; } private void logPath(TreeLogger logger, TypePath path) { if (path == null) { return; } if (logger.isLoggable(TreeLogger.DEBUG)) { logger.log(TreeLogger.DEBUG, path.toString()); } logPath(logger, path.getParent()); } private void logReachableTypes(TreeLogger logger) { if (!context.isProdMode() && !logger.isLoggable(TreeLogger.DEBUG)) { return; } if (logOutputWriter != null) { // Route the TreeLogger output to an output stream. PrintWriterTreeLogger printWriterTreeLogger = new PrintWriterTreeLogger(logOutputWriter); printWriterTreeLogger.setMaxDetail(TreeLogger.ALL); logger = printWriterTreeLogger; } if (logger.isLoggable(TreeLogger.DEBUG)) { logger.log(TreeLogger.DEBUG, "Reachable types computed on: " + new Date().toString()); } Set<JType> keySet = typeToTypeInfoComputed.keySet(); JType[] types = keySet.toArray(new JType[0]); Arrays.sort(types, JTYPE_COMPARATOR); for (JType type : types) { TypeInfoComputed tic = typeToTypeInfoComputed.get(type); assert (tic != null); TreeLogger typeLogger = logger.branch(TreeLogger.DEBUG, tic.getType().getParameterizedQualifiedSourceName()); TreeLogger serializationStatus = typeLogger.branch(TreeLogger.DEBUG, "Serialization status"); if (tic.isInstantiable()) { serializationStatus.branch(TreeLogger.DEBUG, "Instantiable"); } else { if (tic.isFieldSerializable()) { serializationStatus.branch(TreeLogger.DEBUG, "Field serializable"); } else { serializationStatus.branch(TreeLogger.DEBUG, "Not serializable"); } } TreeLogger pathLogger = typeLogger.branch(TreeLogger.DEBUG, "Path"); logPath(pathLogger, tic.getPath()); logger.log(TreeLogger.DEBUG, ""); } if (logOutputWriter != null) { logOutputWriter.flush(); } } /** * Mark arrays of <code>leafType</code> as instantiable, for arrays of * dimension up to <code>maxRank</code>. */ private void markArrayTypesInstantiable(JType leafType, int maxRank, TypePath path) { for (int rank = 1; rank <= maxRank; ++rank) { JArrayType covariantArray = getArrayType(typeOracle, rank, leafType); TypeInfoComputed covariantArrayTic = getTypeInfoComputed(covariantArray, path, true); covariantArrayTic.setInstantiable(true); } } private boolean maybeInstantiable(TreeLogger logger, JClassType type, ProblemReport problems) { boolean success = canBeInstantiated(type, problems) && shouldConsiderFieldsForSerialization(type, problems); if (success) { if (logger.isLoggable(TreeLogger.DEBUG)) { logger.log(TreeLogger.DEBUG, type.getParameterizedQualifiedSourceName() + " might be instantiable"); } } return success; } private boolean mightNotBeExposed(JGenericType baseType, int paramIndex) { TypeParameterFlowInfo flowInfo = getFlowInfo(baseType, paramIndex); return flowInfo.getMightNotBeExposed() || isManuallySerializable(baseType); } /** * Remove serializable types that were visited due to speculative paths but * are not really needed for serialization. * * NOTE: This is currently much more limited than it should be. For example, a * path sensitive prune could remove instantiable types also. */ private void pruneUnreachableTypes() { /* * Record all supertypes of any instantiable type, whether or not they are * field serialziable. */ Set<JType> supersOfInstantiableTypes = new LinkedHashSet<JType>(); for (TypeInfoComputed tic : typeToTypeInfoComputed.values()) { if (tic.isInstantiable() && tic.getType() instanceof JClassType) { JClassType type = (JClassType) tic.getType().getErasedType(); JClassType sup = type; while (sup != null) { supersOfInstantiableTypes.add(sup.getErasedType()); sup = sup.getErasedType().getSuperclass(); } } } /* * Record any field serializable type that is not in the supers of any * instantiable type. */ Set<JType> toKill = new LinkedHashSet<JType>(); for (TypeInfoComputed tic : typeToTypeInfoComputed.values()) { if (tic.isFieldSerializable() && !supersOfInstantiableTypes.contains(tic.getType().getErasedType())) { toKill.add(tic.getType()); } } /* * Remove any field serializable supers that cannot be reached from an * instantiable type. */ for (JType type : toKill) { typeToTypeInfoComputed.remove(type); } } }