/* * Copyright 2006-2012 Amazon Technologies, Inc. or its affiliates. * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks * of Amazon Technologies, Inc. or its affiliates. All rights reserved. * * 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.amazon.carbonado.info; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.lang.annotation.Annotation; import java.lang.ref.Reference; import java.lang.ref.SoftReference; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.TypeVariable; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.WildcardType; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.cojen.classfile.MethodDesc; import org.cojen.classfile.TypeDesc; import org.cojen.util.BeanComparator; import org.cojen.util.BeanProperty; import org.cojen.util.BeanIntrospector; import org.cojen.util.ThrowUnchecked; import org.cojen.util.WeakIdentityMap; import com.amazon.carbonado.Alias; import com.amazon.carbonado.AlternateKeys; import com.amazon.carbonado.Authoritative; import com.amazon.carbonado.Automatic; import com.amazon.carbonado.Derived; import com.amazon.carbonado.FetchException; import com.amazon.carbonado.Index; import com.amazon.carbonado.Indexes; import com.amazon.carbonado.Join; import com.amazon.carbonado.Key; import com.amazon.carbonado.MalformedTypeException; import com.amazon.carbonado.Name; import com.amazon.carbonado.Nullable; import com.amazon.carbonado.Independent; import com.amazon.carbonado.PartitionKey; import com.amazon.carbonado.PrimaryKey; import com.amazon.carbonado.Query; import com.amazon.carbonado.Sequence; import com.amazon.carbonado.Storable; import com.amazon.carbonado.Version; import com.amazon.carbonado.adapter.AdapterDefinition; import com.amazon.carbonado.constraint.ConstraintDefinition; import com.amazon.carbonado.lob.Lob; import com.amazon.carbonado.util.ConversionComparator; /** * Supports examination of {@link Storable} types, returning all metadata * associated with it. As part of the examination, all annotations are gathered * up. All examined data is cached, so repeat examinations are fast, unless the * examination failed. * * @author Brian S O'Neill * @author Fang Chen * @author Tobias Holgers * @author Archit Shivaprakash */ public class StorableIntrospector { // Weakly maps Class objects to softly referenced StorableInfo objects. @SuppressWarnings("unchecked") private static Map<Class<?>, Reference<StorableInfo<?>>> cCache = new WeakIdentityMap(); private static final Class[] EMPTY_CLASSES_ARRAY = new Class[0]; private static final Method cCovariantTypesMethod; static { Method method; try { method = BeanProperty.class.getMethod("getCovariantTypes", (Class[]) null); } catch (NoSuchMethodException e) { method = null; } cCovariantTypesMethod = method; } private static Class<?>[] getCovariantTypes(BeanProperty property) { // Access via reflection since this is a feature not available in all // versions of Cojen. if (cCovariantTypesMethod != null) { try { return (Class[]) cCovariantTypesMethod.invoke(property, (Object[]) null); } catch (InvocationTargetException e) { ThrowUnchecked.fireDeclaredCause(e); } catch (IllegalAccessException e) { ThrowUnchecked.fireDeclared(e); } } return EMPTY_CLASSES_ARRAY; } /** * Test program which examines candidate Storable classes. If any fail, an * exception is thrown. * * @param args names of classes to examine */ public static void main(String[] args) throws Exception { for (String arg : args) { Class clazz = Class.forName(arg); System.out.println("Examining: " + clazz.getName()); try { examine(clazz); System.out.println("Passed"); } catch (MalformedTypeException e) { System.out.println("Malformed type: " + e.getMalformedType().getName()); for (String message : e.getMessages()) { System.out.println(message); } } } } /** * Examines the given class and returns a StorableInfo describing it. A * MalformedTypeException is thrown for a variety of reasons if the given * class is an invalid Storable type. * * @param type Storable type to examine * @throws MalformedTypeException if Storable type is invalid * @throws IllegalArgumentException if type is null */ @SuppressWarnings("unchecked") public static <S extends Storable> StorableInfo<S> examine(Class<S> type) { if (type == null) { throw new IllegalArgumentException("Storable type must not be null"); } synchronized (cCache) { StorableInfo<S> info; Reference<StorableInfo<?>> ref = cCache.get(type); if (ref != null) { info = (StorableInfo<S>) ref.get(); if (info != null) { return info; } } List<String> errorMessages = new ArrayList<String>(); // Pull these annotations out but finish processing later. List<NameAndDirection> primaryKeyProps; List<List<NameAndDirection>> alternateKeyProps; List<List<NameAndDirection>> indexProps; List<NameAndDirection> partitionKeyProps; { try { primaryKeyProps = gatherListProperties(errorMessages, type.getAnnotation(PrimaryKey.class)); } catch (IndexOutOfBoundsException e) { errorMessages.add("No primary key defined"); primaryKeyProps = Collections.emptyList(); } alternateKeyProps = gatherListProperties(errorMessages, type.getAnnotation(AlternateKeys.class)); indexProps = gatherListProperties(errorMessages, type.getAnnotation(Indexes.class)); partitionKeyProps = gatherListProperties(errorMessages, type.getAnnotation(PartitionKey.class)); } // Get all the properties. Map<String, StorableProperty<S>> properties = examineProperties(type, primaryKeyProps, alternateKeyProps, partitionKeyProps); // Resolve keys and indexes. StorableKey<S> primaryKey; { Set<OrderedProperty<S>> propSet = resolveKey(errorMessages, type, properties, "primary key", primaryKeyProps); primaryKey = new SKey<S>(true, propSet); } StorableKey<S>[] alternateKeys; { alternateKeys = new StorableKey[alternateKeyProps.size()]; int i = 0; for (List<NameAndDirection> nameAndDirs : alternateKeyProps) { Set<OrderedProperty<S>> propSet = resolveKey(errorMessages, type, properties, "alternate key", nameAndDirs); alternateKeys[i++] = new SKey<S>(false, propSet); } } StorableKey<S> partitionKey = null; if (partitionKeyProps != null) { Set<OrderedProperty<S>> propSet = resolveKey(errorMessages, type, properties, "partition key", partitionKeyProps); partitionKey = new SKey<S>(false, propSet); } StorableIndex<S>[] indexes; { indexes = new StorableIndex[indexProps.size()]; int i = 0; for (List<NameAndDirection> nameAndDirs : indexProps) { int errorCount = errorMessages.size(); Set<OrderedProperty<S>> propSet = resolveKey(errorMessages, type, properties, "index", nameAndDirs); if (errorMessages.size() <= errorCount) { // If index property not found, error message has been // added to list, but propSet might end up being // empty. Rather than get an exception thrown from the // StorableIndex constructor, just don't try to define // the bogus index at all. OrderedProperty<S>[] propArray = new OrderedProperty[propSet.size()]; propSet.toArray(propArray); indexes[i] = new StorableIndex<S>(propArray, null); } i++; } } // Sort properties by name, grouped with primary keys first. This // ensures a consistent arrangement, even if methods move around in // the class file. { // Store results in a LinkedHashMap to preserve sort order. Map<String, StorableProperty<S>> arrangedProperties = new LinkedHashMap<String, StorableProperty<S>>(); // First dump in primary key properties, in their proper order. for (OrderedProperty<S> orderedProp : primaryKey.getProperties()) { StorableProperty<S> prop = orderedProp.getChainedProperty().getPrimeProperty(); arrangedProperties.put(prop.getName(), prop); } // Gather all remaining properties, and then sort them. List<StorableProperty<S>> nonPkProperties = new ArrayList<StorableProperty<S>>(); for (StorableProperty<S> prop : properties.values()) { if (!arrangedProperties.containsKey(prop.getName())) { nonPkProperties.add(prop); } } Collections.sort(nonPkProperties, BeanComparator.forClass(StorableProperty.class).orderBy("name")); for (StorableProperty<S> prop : nonPkProperties) { arrangedProperties.put(prop.getName(), prop); } properties = Collections.unmodifiableMap(arrangedProperties); } // Process type aliases String[] aliases; Alias alias = type.getAnnotation(Alias.class); if (alias == null) { aliases = null; } else { aliases = alias.value(); if (aliases.length == 0) { errorMessages.add("Alias list is empty"); } } info = new Info<S>(type, aliases, indexes, properties, primaryKey, alternateKeys, partitionKey, type.getAnnotation(Independent.class) != null, type.getAnnotation(Authoritative.class) != null); cCache.put(type, new SoftReference<StorableInfo<?>>(info)); // Now that the StorableInfo object has been constructed, assign it // to all properties to prevent it from being prematurely uncached. // Also assign number now that properties have been sorted. { int number = 0; for (StorableProperty property : properties.values()) { if (property instanceof SimpleProperty) { SimpleProperty sp = (SimpleProperty) property; sp.setEnclosingInfo(info); sp.setNumber(number); } number++; } } // Finish resolving join properties, after properties have been // added to cache. This makes it possible for joins to (directly or // indirectly) reference their own enclosing type. If not resolved // late, then there would be a stack overflow. for (StorableProperty property : properties.values()) { if (property instanceof JoinProperty) { ((JoinProperty)property).resolveJoin(errorMessages); } } // Resolve derived properties after join properties, since they may // depend on them. boolean anyDerived = false; for (StorableProperty<S> property : properties.values()) { if (property instanceof SimpleProperty && property.isDerived()) { anyDerived = true; ((SimpleProperty)property).resolveDerivedFrom(errorMessages); } } if (anyDerived && errorMessages.size() == 0) { // Make sure that any indexes which refer to derived properties // throwing FetchException have derived-from properties // listed. Why? The exception likely indicates that a join // property is being fetched. for (StorableIndex<S> index : indexes) { for (StorableProperty<S> property : index.getProperties()) { if (property.isDerived() && property.getReadMethod() != null && property.getDerivedFromProperties().length == 0) { Class exceptionType = FetchException.class; Class<?>[] exceptions = property.getReadMethod().getExceptionTypes(); boolean fetches = false; for (int i=exceptions.length; --i>=0; ) { if (exceptions[i].isAssignableFrom(exceptionType)) { fetches = true; break; } } if (fetches) { errorMessages.add ("Index refers to a derived property which declares " + "throwing a FetchException, but property does not " + "list any derived-from properties: \"" + property.getName() + "'"); } } } } } if (errorMessages.size() > 0) { cCache.remove(type); throw new MalformedTypeException(type, errorMessages); } return info; } } /** * Examines a class and determines what Storable type it implements. If it * cannot be unambiguously inferred, null is returned. A non-null return * value does not imply that the Storable type is valid, however. It must * be {@link #examine examined} to check validity. * * @since 1.2 */ public static Class<? extends Storable> inferType(Class clazz) { if (clazz == null || !Storable.class.isAssignableFrom(clazz)) { return null; } if (clazz.isAnnotationPresent(PrimaryKey.class)) { return clazz; } Class candidate = inferType(clazz.getSuperclass()); for (Class iface : clazz.getInterfaces()) { Class inferred = inferType(iface); if (inferred != null) { if (candidate == null) { candidate = inferred; } else { // Inference is ambiguous. return null; } } } return candidate; } private static class NameAndDirection { final String name; final Direction direction; NameAndDirection(String name, Direction direction) { this.name = name; this.direction = direction; } @Override public int hashCode() { return name.hashCode() + direction.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof NameAndDirection) { // Only compare name. return name.equals(((NameAndDirection) obj).name); } return false; } } /** * @param indexes pass in for gathering index properties */ private static List<List<NameAndDirection>> gatherListProperties(List<String> errorMessages, Indexes indexes) { List<List<NameAndDirection>> listlist = new ArrayList<List<NameAndDirection>>(); if (indexes != null) { Index[] ixs = indexes.value(); if (ixs != null && ixs.length > 0) { for (int i=0; i<ixs.length; i++) { String[] propNames = ixs[i].value(); if (propNames == null || propNames.length == 0) { errorMessages.add("Empty index defined"); continue; } gatherListProperties(errorMessages, "index", propNames, listlist); } } } return listlist; } /** * @param alternateKeys pass in for gathering alternate key properties */ private static List<List<NameAndDirection>> gatherListProperties(List<String> errorMessages, AlternateKeys alternateKeys) { List<List<NameAndDirection>> listlist = new ArrayList<List<NameAndDirection>>(); if (alternateKeys != null) { Key[] keys = alternateKeys.value(); if (keys != null && keys.length > 0) { for (int i=0; i<keys.length; i++) { String[] propNames = keys[i].value(); if (propNames == null || propNames.length == 0) { errorMessages.add("Empty alternate key defined"); continue; } gatherListProperties(errorMessages, "alternate key", propNames, listlist); } } } return listlist; } /** * @param primaryKey pass in for gathering primary key properties */ private static List<NameAndDirection> gatherListProperties(List<String> errorMessages, PrimaryKey primaryKey) { List<List<NameAndDirection>> listlist = new ArrayList<List<NameAndDirection>>(); if (primaryKey != null) { String[] propNames = primaryKey.value(); if (propNames == null || propNames.length == 0) { errorMessages.add("Empty primary key defined"); } else { gatherListProperties(errorMessages, "primary key", propNames, listlist); } } return listlist.get(0); } /** * @param partitionKey pass in for gathering partition key properties */ private static List<NameAndDirection> gatherListProperties(List<String> errorMessages, PartitionKey partitionKey) { List<List<NameAndDirection>> listlist = new ArrayList<List<NameAndDirection>>(); if (partitionKey != null) { String[] propNames = partitionKey.value(); if (propNames == null || propNames.length == 0) { errorMessages.add("Empty partition key defined"); } else { gatherListProperties(errorMessages, "partition key", propNames, listlist); } } return listlist.size() > 0 ? listlist.get(0) : null; } private static void gatherListProperties(List<String> errorMessages, String listName, String[] propNames, List<List<NameAndDirection>> listlist) { int length = propNames.length; List<NameAndDirection> nameAndDirs = new ArrayList<NameAndDirection>(length); for (int i=0; i<length; i++) { String name = propNames[i]; Direction dir = Direction.UNSPECIFIED; if (name.length() > 0) { if (name.charAt(0) == '+') { name = name.substring(1); dir = Direction.ASCENDING; } else if (name.charAt(0) == '-') { name = name.substring(1); dir = Direction.DESCENDING; } } NameAndDirection nameAndDir = new NameAndDirection(name, dir); if (nameAndDirs.contains(nameAndDir)) { errorMessages.add ("Duplicate property in " + listName + ": " + Arrays.toString(propNames)); continue; } else { nameAndDirs.add(nameAndDir); } } if (nameAndDirs.size() == 0) { return; } if (listlist.contains(nameAndDirs)) { errorMessages.add ("Duplicate " + listName + " specification: " + Arrays.toString(propNames)); return; } listlist.add(nameAndDirs); } private static <S extends Storable> Set<OrderedProperty<S>> resolveKey(List<String> errorMessages, Class<S> type, Map<String, StorableProperty<S>> properties, String elementName, List<NameAndDirection> nameAndDirs) { Set<OrderedProperty<S>> orderedProps = new LinkedHashSet<OrderedProperty<S>>(); for (NameAndDirection nameAndDir : nameAndDirs) { String name = nameAndDir.name; if (name.indexOf('.') > 0) { errorMessages.add("Chained property not allowed in " + elementName + ": " + name); continue; } StorableProperty<S> prop = properties.get(name); if (prop == null) { errorMessages.add ("Property for " + elementName + " not found: " + name); continue; } if (prop.isJoin()) { errorMessages.add ("Property of " + elementName + " cannot reference a join property: " + name); continue; } if (Lob.class.isAssignableFrom(prop.getType())) { errorMessages.add ("Property of " + elementName + " cannot reference a LOB property: " + name); continue; } orderedProps.add(OrderedProperty.get(prop, nameAndDir.direction)); } if (orderedProps.size() == 0) { return Collections.emptySet(); } if (orderedProps.size() == 1) { return Collections.singleton(orderedProps.iterator().next()); } return Collections.unmodifiableSet(orderedProps); } /** * Does the real work in examining the given type. The join properties and * alternate keys must still be resolved afterwards. */ private static <S extends Storable> Map<String, StorableProperty<S>> examineProperties(Class<S> type, List<NameAndDirection> primaryKeyProps, List<List<NameAndDirection>> alternateKeyProps, List<NameAndDirection> partitionKeyProps) throws MalformedTypeException { if (Storable.class.isAssignableFrom(type)) { if (Storable.class == type) { throw new MalformedTypeException(type, "Storable interface must be extended"); } } else { throw new MalformedTypeException(type, "Does not implement Storable interface"); } int modifiers = type.getModifiers(); if (Modifier.isFinal(modifiers)) { throw new MalformedTypeException(type, "Class is declared final"); } if (!Modifier.isPublic(modifiers)) { throw new MalformedTypeException(type, "Class is not public"); } List<String> errorMessages = new ArrayList<String>(); checkTypeParameter(errorMessages, type); // If type is a class, it must have a public or protected no-arg // constructor. if (!type.isInterface()) { Constructor[] ctors = type.getDeclaredConstructors(); findCtor: { for (Constructor c : ctors) { if (c.getParameterTypes().length == 0) { modifiers = c.getModifiers(); if (!Modifier.isPublic(modifiers) && !Modifier.isProtected(modifiers)) { errorMessages.add("Cannot call constructor: " + c); } break findCtor; } } if (type.getEnclosingClass() == null) { errorMessages.add ("Class must have a public or protected constructor " + "that accepts no arguments"); } else { errorMessages.add ("Inner class must have a public or protected constructor " + "that accepts no arguments"); } } } // All methods to be implemented must be bean property methods that // operate on a supported type. // First, gather all methods that must be implemented. // Gather all methods. We'll be removing them as we implement them, // and if there are any abstract ones left over at the end, why, // that would be bad. Map<String, Method> methods = gatherAllDeclaredMethods(type); // Remove methods not abstract or defined explicitly in // Storable. Storable methods still must be implemented, but not as // properties. for (Iterator<Method> it = methods.values().iterator(); it.hasNext(); ) { Method m = it.next(); if (!Modifier.isAbstract(m.getModifiers()) || m.getDeclaringClass() == Storable.class) { it.remove(); continue; } // Check if abstract method is just redefining a method in // Storable. try { Method m2 = Storable.class.getMethod(m.getName(), (Class[]) m.getParameterTypes()); if (m.getReturnType() == m2.getReturnType()) { it.remove(); } // Copy method can be redefined with specialized return type. if (m.getName().equals("copy") && type.isAssignableFrom(m.getReturnType())) { it.remove(); } } catch (NoSuchMethodException e) { // Not defined in Storable. } } // Identify which properties are members of a primary, alternate or partition key. Set<String> pkPropertyNames, altKeyPropertyNames, parKeyPropertyNames; { pkPropertyNames = new HashSet<String>(); altKeyPropertyNames = new HashSet<String>(); parKeyPropertyNames = new HashSet<String>(); for (NameAndDirection nameAndDir : primaryKeyProps) { pkPropertyNames.add(nameAndDir.name); } for (List<NameAndDirection> list : alternateKeyProps) { for (NameAndDirection nameAndDir : list) { altKeyPropertyNames.add(nameAndDir.name); } } if (partitionKeyProps != null) { for (NameAndDirection nameAndDir : partitionKeyProps) { parKeyPropertyNames.add(nameAndDir.name); } } } Map allProperties = BeanIntrospector.getAllProperties(type); // Copy only the properties that should be implemented here. Map<String, StorableProperty<S>> properties = new HashMap<String, StorableProperty<S>>(); // Remove methods for properties that can be implemented. Iterator it = allProperties.values().iterator(); while (it.hasNext()) { BeanProperty property = BeanProperty.class.cast(it.next()); Method readMethod = property.getReadMethod(); Method writeMethod = property.getWriteMethod(); if (readMethod == null) { if (writeMethod == null) { continue; } else if (!Modifier.isAbstract(writeMethod.getModifiers()) && writeMethod.getAnnotation(Derived.class) == null) { // Ignore concrete property methods unless they're derived. continue; } } else if (!Modifier.isAbstract(readMethod.getModifiers()) && readMethod.getAnnotation(Derived.class) == null) { // Ignore concrete property methods unless they're derived. continue; } StorableProperty<S> storableProp = makeStorableProperty(errorMessages, property, type, pkPropertyNames, altKeyPropertyNames, parKeyPropertyNames); if (storableProp == null) { // Errors. continue; } if (properties.containsKey(storableProp.getName())) { errorMessages.add("Duplicate property defined: " + storableProp.getName()); continue; } if (readMethod != null) { String sig = createSig(readMethod); if (storableProp.isDerived() || methods.containsKey(sig)) { methods.remove(sig); properties.put(storableProp.getName(), storableProp); } else { continue; } } if (writeMethod != null) { String sig = createSig(writeMethod); if (storableProp.isDerived() || methods.containsKey(sig)) { methods.remove(sig); properties.put(storableProp.getName(), storableProp); } else { continue; } } } // Only include errors on unimplementable methods if there are no other // errors. This prevents producing errors caused by other errors. if (errorMessages.size() == 0) { Iterator<Method> iter = methods.values().iterator(); while (iter.hasNext()) { Method m = iter.next(); int methodModifiers = m.getModifiers(); if (Modifier.isAbstract(methodModifiers)) { String message; if (!Modifier.isPublic(methodModifiers) && !Modifier.isProtected(methodModifiers)) { message = "Abstract method cannot be defined " + "(neither public or protected): "; } else if (!isCovariant(allProperties, m)) { message = "Abstract method cannot be defined (not a bean property): "; } else { message = null; } if (message != null) { errorMessages.add(message + m); } // We've reported an error or validated method. No need to // check it again. iter.remove(); } } } // Verify at most one version property exists. { boolean hasVersionProp = false; for (StorableProperty property : properties.values()) { if (property.isVersion()) { if (hasVersionProp) { errorMessages.add ("At most one property may be designated as the version number"); break; } hasVersionProp = true; } } } // Only include errors on unimplementable methods if there are no other // errors. This prevents producing errors caused by other errors. if (errorMessages.size() == 0 && methods.size() > 0) { for (Method m : methods.values()) { errorMessages.add("Method cannot be implemented: " + m); } } if (errorMessages.size() > 0) { throw new MalformedTypeException(type, errorMessages); } return Collections.unmodifiableMap(properties); } /** * @param allProperties map of BeanProperty instances */ private static boolean isCovariant(Map allProperties, Method m) { for (Object obj : allProperties.values()) { BeanProperty property = (BeanProperty) obj; Class[] covariantTypes = getCovariantTypes(property); if (covariantTypes == null || covariantTypes.length == 0) { continue; } Class returnType = m.getReturnType(); Class[] paramTypes = m.getParameterTypes(); Class type; if (m.getName().equals(property.getReadMethod().getName())) { if (returnType == null || returnType == void.class) { continue; } if (paramTypes == null || paramTypes.length > 0) { continue; } type = returnType; } else if (m.getName().equals(property.getWriteMethod().getName())) { if (returnType != null && returnType != void.class) { continue; } if (paramTypes == null || paramTypes.length != 1) { continue; } type = paramTypes[0]; } else { continue; } for (Class covariantType : covariantTypes) { if (type == covariantType) { return true; } } } return false; } /** * Make sure that the parameter type that is specified to Storable can be * assigned to a Storable, and that the given type can be assigned to * it. Put another way, the upper bound is Storable, and the lower bound * is the given type. type <= parameterized type <= Storable */ @SuppressWarnings("unchecked") private static void checkTypeParameter(List<String> errorMessages, Class type) { // Only check classes and interfaces that extend Storable. if (type != null && Storable.class.isAssignableFrom(type)) { if (Storable.class == type) { return; } } else { return; } // Check all superclasses and interfaces. checkTypeParameter(errorMessages, type.getSuperclass()); for (Class c : type.getInterfaces()) { checkTypeParameter(errorMessages, c); } for (Type t : type.getGenericInterfaces()) { if (t instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType)t; if (pt.getRawType() == Storable.class) { // Found exactly which parameter is passed directly to // Storable. Make sure that it is in the proper bounds. Type arg = pt.getActualTypeArguments()[0]; Class param; if (arg instanceof ParameterizedType) { Type raw = ((ParameterizedType)arg).getRawType(); if (raw instanceof Class) { param = (Class)raw; } else { continue; } } else if (arg instanceof Class) { param = (Class)arg; } else if (arg instanceof TypeVariable) { // TODO continue; } else { continue; } if (Storable.class.isAssignableFrom(param)) { if (!param.isAssignableFrom(type)) { errorMessages.add ("Type parameter passed from " + type + " to Storable must be a " + type.getName() + ": " + param); return; } } else { errorMessages.add ("Type parameter passed from " + type + " to Storable must be a Storable: " + param); return; } } } } } /** * If property is a join, then it is not yet completely resolved. Returns * null if there are any errors. * * @param errorMessages error messages go here * @param property property to examine * @param enclosing enclosing class * @param pkPropertyNames primary key property names * @param altKeyPropertyNames alternate key property names * @param parKeyPropertyNames partition key property names */ @SuppressWarnings("unchecked") private static <S extends Storable> StorableProperty<S> makeStorableProperty (List<String> errorMessages, BeanProperty property, Class<S> enclosing, Set<String> pkPropertyNames, Set<String> altKeyPropertyNames, Set<String> parKeyPropertyNames) { Nullable nullable = null; Alias alias = null; Version version = null; Sequence sequence = null; Automatic automatic = null; Independent independent = null; Join join = null; Derived derived = null; Name name = null; Method readMethod = property.getReadMethod(); Method writeMethod = property.getWriteMethod(); if (readMethod == null) { if (writeMethod == null || Modifier.isAbstract(writeMethod.getModifiers())) { // If we got here, the onus is on us to create this property. It's never // ok for the read method (get) to be null. errorMessages.add ("Must define proper 'get' method for property: " + property.getName()); } } else { nullable = readMethod.getAnnotation(Nullable.class); alias = readMethod.getAnnotation(Alias.class); version = readMethod.getAnnotation(Version.class); sequence = readMethod.getAnnotation(Sequence.class); automatic = readMethod.getAnnotation(Automatic.class); independent = readMethod.getAnnotation(Independent.class); join = readMethod.getAnnotation(Join.class); derived = readMethod.getAnnotation(Derived.class); name = readMethod.getAnnotation(Name.class); } String propertyName; if (name == null) { propertyName = property.getName(); } else { propertyName = name.value(); // Ensure that only valid characters are used. int length = propertyName.length(); if (length == 0) { errorMessages.add("Property name for method cannot be blank: " + readMethod); } else { if (!Character.isUnicodeIdentifierStart(propertyName.charAt(0))) { errorMessages.add("First character of property name must be a " + "unicode identifier start: " + propertyName); } else { for (int i=1; i<length; i++) { if (!Character.isUnicodeIdentifierPart(propertyName.charAt(i))) { errorMessages.add("Characters of property name must be a " + "unicode identifier part: " + propertyName); break; } } } } } boolean pk = pkPropertyNames.contains(propertyName); boolean altKey = altKeyPropertyNames.contains(propertyName); boolean parKey = parKeyPropertyNames.contains(propertyName); if (writeMethod == null) { if (readMethod == null || Modifier.isAbstract(readMethod.getModifiers())) { // Set method is always required for non-join properties. More // work is done later on join properties, and sometimes the // write method is required. Derived properties don't need a // set method. if (join == null && derived == null) { errorMessages.add("Must define proper 'set' method for property: " + propertyName); } } } else { Class[] writeParams = writeMethod.getParameterTypes(); if (writeParams == null || writeParams.length != 1) { errorMessages.add("Mutator method must contain one parameter: " + writeMethod); } else if (!writeParams[0].isAssignableFrom(property.getType())) { errorMessages.add ("Property type doesn't match mutator method parameter: " + property.getType().getName() + " != " + writeParams[0].getName() + " for " + writeMethod); } if (writeMethod.getAnnotation(Nullable.class) != null) { errorMessages.add ("Nullable annotation not allowed on mutator: " + writeMethod); } if (writeMethod.getAnnotation(Alias.class) != null) { errorMessages.add ("Alias annotation not allowed on mutator: " + writeMethod); } if (writeMethod.getAnnotation(Version.class) != null) { errorMessages.add ("Version annotation not allowed on mutator: " + writeMethod); } if (writeMethod.getAnnotation(Sequence.class) != null) { errorMessages.add ("Sequence annotation not allowed on mutator: " + writeMethod); } if (writeMethod.getAnnotation(Automatic.class) != null) { errorMessages.add ("Automatic annotation not allowed on mutator: " + writeMethod); } if (writeMethod.getAnnotation(Independent.class) != null) { errorMessages.add ("Independent annotation not allowed on mutator: " + writeMethod); } if (writeMethod.getAnnotation(Join.class) != null) { errorMessages.add ("Join annotation not allowed on mutator: " + writeMethod); } if (writeMethod.getAnnotation(Derived.class) != null) { errorMessages.add ("Derived annotation not allowed on mutator: " + writeMethod); } if (writeMethod.getAnnotation(Name.class) != null) { errorMessages.add ("Name annotation not allowed on mutator: " + writeMethod); } } if (derived != null) { if (readMethod != null && Modifier.isAbstract(readMethod.getModifiers()) || writeMethod != null && Modifier.isAbstract(writeMethod.getModifiers())) { errorMessages.add("Derived properties cannot be abstract: " + propertyName); } if (writeMethod == null && derived.shouldCopy()) { errorMessages.add("Derived properties which should be copied " + "must have a write method: " + propertyName); } if (pk) { errorMessages.add("Derived properties cannot be a member of primary key: " + propertyName); } if (parKey) { errorMessages.add("Derived properties cannot be a member of partition key: " + propertyName); } if (sequence != null) { errorMessages.add("Derived properties cannot have a Sequence annotation: " + propertyName); } if (automatic != null) { errorMessages.add("Derived properties cannot have an Automatic annotation: " + propertyName); } if (join != null) { errorMessages.add("Derived properties cannot have a Join annotation: " + propertyName); } } if (nullable != null && property.getType().isPrimitive()) { errorMessages.add ("Properties which have a primitive type cannot be declared nullable: " + "Property \"" + propertyName + "\" has type \"" + property.getType() + '"'); } String[] aliases = null; if (alias != null) { aliases = alias.value(); if (aliases.length == 0) { errorMessages.add("Alias list is empty for property: " + propertyName); } } StorablePropertyConstraint[] constraints = null; if (readMethod != null) { // Constraints not allowed on read method. Look for them and // generate errors if any found. gatherConstraints(property, readMethod, false, errorMessages); } if (writeMethod != null) { constraints = gatherConstraints(property, writeMethod, true, errorMessages); } StorablePropertyAdapter[] adapters = null; if (readMethod != null) { adapters = gatherAdapters(property, readMethod, true, errorMessages); if (adapters != null && adapters.length > 0) { if (join != null) { errorMessages.add ("Join properties cannot have adapters: " + propertyName); } if (adapters.length > 1) { errorMessages.add ("Only one adpater allowed per property: " + propertyName); } } if (adapters == null || adapters.length == 0) { StorablePropertyAdapter autoAdapter = AutomaticAdapterSelector.selectAdapterFor(property); if (autoAdapter != null) { adapters = new StorablePropertyAdapter[] {autoAdapter}; } } } if (writeMethod != null) { // Adapters not allowed on write method. Look for them and generate // errors if any found. gatherAdapters(property, writeMethod, false, errorMessages); } // Check that declared checked exceptions are allowed. if (readMethod != null) { for (Class<?> ex : readMethod.getExceptionTypes()) { if (RuntimeException.class.isAssignableFrom(ex) || Error.class.isAssignableFrom(ex)) { continue; } if (join != null || derived != null) { if (FetchException.class.isAssignableFrom(ex)) { continue; } errorMessages.add ("Checked exceptions thrown by join or derived property accessors " + "must be of type FetchException: \"" + readMethod.getName() + "\" declares throwing \"" + ex.getName() + '"'); break; } else { errorMessages.add ("Only join and derived property accessors can throw checked " + "exceptions: \"" + readMethod.getName() + "\" declares throwing \"" + ex.getName() + '"'); break; } } } // Check that declared checked exceptions are allowed. if (writeMethod != null) { for (Class<?> ex : writeMethod.getExceptionTypes()) { if (RuntimeException.class.isAssignableFrom(ex) || Error.class.isAssignableFrom(ex)) { continue; } errorMessages.add ("Mutators cannot throw checked exceptions: \"" + writeMethod.getName() + "\" declares throwing \"" + ex.getName() + '"'); break; } } String sequenceName = null; if (sequence != null) { sequenceName = sequence.value(); } if (join == null) { if (errorMessages.size() > 0) { return null; } return new SimpleProperty<S> (property, enclosing, nullable != null, pk, altKey, parKey, aliases, constraints, adapters == null ? null : adapters[0], version != null, sequenceName, independent != null, automatic != null, derived, propertyName); } // Do additional work for join properties. String[] internal = join.internal(); String[] external = join.external(); if (internal == null) { internal = new String[0]; } if (external == null) { external = new String[0]; } if (internal.length != external.length) { errorMessages.add ("Internal/external lists on Join property \"" + propertyName + "\" differ in length: " + internal.length + " != " + external.length); } Class joinedType = property.getType(); if (Query.class == joinedType) { if (nullable != null) { errorMessages.add ("Join property \"" + propertyName + "\" cannot be declared as nullable because the type is Query"); } // Recover the results element type from the accessor. A Mutator is // not allowed. if (property.getWriteMethod() != null) { errorMessages.add ("Join property \"" + propertyName + "\" cannot have a mutator because the type is Query: " + property.getWriteMethod()); } if (property.getReadMethod() == null) { // Default. joinedType = Storable.class; } else { Type genericType = property.getReadMethod().getGenericReturnType(); if (genericType instanceof Class) { // Default. joinedType = Storable.class; } else if (genericType instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType)genericType; Type[] args = pt.getActualTypeArguments(); if (args == null || args.length == 0) { // Default. joinedType = Storable.class; } else { Type arg = args[0]; if (arg instanceof WildcardType) { Type[] upper = ((WildcardType) arg).getUpperBounds(); // Length should only be one or zero. if (upper.length == 1) { arg = upper[0]; } else { // Default. arg = Storable.class; } } while (arg instanceof ParameterizedType) { arg = ((ParameterizedType)arg).getRawType(); } if (arg instanceof Class) { joinedType = (Class)arg; } } } } } if (!Storable.class.isAssignableFrom(joinedType)) { errorMessages.add ("Type of join property \"" + propertyName + "\" is not a Storable: " + joinedType); } if (property.getReadMethod() != null) { Class exceptionType = FetchException.class; Class<?>[] exceptions = property.getReadMethod().getExceptionTypes(); check: { for (int i=exceptions.length; --i>=0; ) { if (exceptions[i].isAssignableFrom(exceptionType)) { break check; } } String exceptionName = exceptionType.getName(); int index = exceptionName.lastIndexOf('.'); if (index >= 0) { exceptionName = exceptionName.substring(index + 1); } errorMessages.add ("Join property accessor must declare throwing a " + exceptionName + ": " + property.getReadMethod()); } } if (version != null) { errorMessages.add ("Join property cannot be declared as a version property: " + propertyName); } if (errorMessages.size() > 0) { return null; } return new JoinProperty<S> (property, enclosing, nullable != null, aliases, constraints, adapters == null ? null : adapters[0], sequenceName, independent != null, automatic != null, derived, joinedType, internal, external, propertyName); } private static StorablePropertyConstraint[] gatherConstraints (BeanProperty property, Method method, boolean isAllowed, List<String> errorMessages) { Annotation[] allAnnotations = method.getAnnotations(); if (allAnnotations.length == 0) { return null; } List<StorablePropertyConstraint> list = new ArrayList<StorablePropertyConstraint>(); for (Annotation annotation : allAnnotations) { Class<? extends Annotation> type = annotation.annotationType(); ConstraintDefinition cd = type.getAnnotation(ConstraintDefinition.class); if (cd == null) { continue; } if (!isAllowed) { errorMessages.add("Constraint not allowed on method: " + method); return null; } Class constraintClass = cd.implementation(); if (constraintClass == void.class) { // Magic value meaning "use default", which is an inner class // of the annotation. constraintClass = null; // Search for inner class named "Constraint". Class[] innerClasses = type.getClasses(); for (Class c : innerClasses) { if ("Constraint".equals(c.getSimpleName())) { constraintClass = c; break; } } if (constraintClass == null) { errorMessages.add ("By default, constraint implementation class must be a static inner " + "class of the annotation named \"Constraint\". Fully qualified name: " + type.getCanonicalName() + ".Constraint"); continue; } } int modifiers = constraintClass.getModifiers(); if (Modifier.isAbstract(modifiers) || Modifier.isInterface(modifiers) || !Modifier.isPublic(modifiers)) { errorMessages.add ("Constraint implementation class must be a concrete public class: " + constraintClass.getName()); continue; } Constructor ctor; try { ctor = constraintClass.getConstructor(Class.class, String.class, type); } catch (NoSuchMethodException e) { errorMessages.add ("Constraint implementation class does not have proper constructor: " + constraintClass.getName()); continue; } // Find best constrain method to bind to. ConversionComparator cc = new ConversionComparator(property.getType()); Class bestMatchingType = null; Method bestConstrainMethod = null; for (Method constrainMethod : constraintClass.getMethods()) { if (!constrainMethod.getName().equals("constrain")) { continue; } if (constrainMethod.getReturnType() != void.class) { continue; } Class<?>[] paramTypes = constrainMethod.getParameterTypes(); if (paramTypes.length != 1) { continue; } Class candidateType = paramTypes[0]; if (!cc.isConversionPossible(candidateType)) { continue; } if (bestMatchingType == null || cc.compare(bestMatchingType, candidateType) > 0) { bestMatchingType = candidateType; bestConstrainMethod = constrainMethod; } } if (bestConstrainMethod == null) { errorMessages.add("Constraint does not support property type: " + TypeDesc.forClass(property.getType()).getFullName() + "; constraint type: " + annotation.annotationType().getName()); } else { StorablePropertyAnnotation spa = new StorablePropertyAnnotation(annotation, method); list.add(new StorablePropertyConstraint(spa, ctor, bestConstrainMethod)); } } if (list.size() == 0) { return null; } return (StorablePropertyConstraint[]) list.toArray (new StorablePropertyConstraint[list.size()]); } private static StorablePropertyAdapter[] gatherAdapters (BeanProperty property, Method method, boolean isAllowed, List<String> errorMessages) { Annotation[] allAnnotations = method.getAnnotations(); if (allAnnotations.length == 0) { return null; } List<StorablePropertyAdapter> list = new ArrayList<StorablePropertyAdapter>(); for (Annotation annotation : allAnnotations) { Class<? extends Annotation> type = annotation.annotationType(); AdapterDefinition ad = type.getAnnotation(AdapterDefinition.class); if (ad == null) { continue; } if (!isAllowed) { errorMessages.add("Adapter not allowed on method: " + method); return null; } Class adapterClass = StorablePropertyAdapter.findAdapterClass(type); if (adapterClass == null) { errorMessages.add ("By default, adapter implementation class must be a static inner " + "class of the annotation named \"Adapter\". Fully qualified name: " + type.getCanonicalName() + ".Adapter"); continue; } int modifiers = adapterClass.getModifiers(); if (Modifier.isAbstract(modifiers) || Modifier.isInterface(modifiers) || !Modifier.isPublic(modifiers)) { errorMessages.add ("Adapter implementation class must be a concrete public class: " + adapterClass.getName()); continue; } Constructor ctor; try { ctor = adapterClass.getConstructor(Class.class, String.class, type); } catch (NoSuchMethodException e) { errorMessages.add ("Adapter implementation class does not have proper constructor: " + adapterClass.getName()); continue; } Method[] adaptMethods = StorablePropertyAdapter.findAdaptMethods(property.getType(), adapterClass); if (adaptMethods.length == 0) { errorMessages.add("Adapter does not support property type: " + TypeDesc.forClass(property.getType()).getFullName() + "; adapter type: " + annotation.annotationType().getName()); } else { StorablePropertyAnnotation spa = new StorablePropertyAnnotation(annotation, method); list.add(new StorablePropertyAdapter(property, spa, ad, ctor, adaptMethods)); } } if (list.size() == 0) { return null; } return (StorablePropertyAdapter[]) list.toArray(new StorablePropertyAdapter[list.size()]); } /** * Returns a new modifiable mapping of method signatures to methods. * * @return map of {@link #createSig signatures} to methods */ private static Map<String, Method> gatherAllDeclaredMethods(Class clazz) { Map<String, Method> methods = new HashMap<String, Method>(); gatherAllDeclaredMethods(methods, clazz); return methods; } private static void gatherAllDeclaredMethods(Map<String, Method> methods, Class clazz) { for (Method m : clazz.getDeclaredMethods()) { String desc = createSig(m); if (!methods.containsKey(desc)) { methods.put(desc, m); } } Class superclass = clazz.getSuperclass(); if (superclass != null) { gatherAllDeclaredMethods(methods, superclass); } for (Class c : clazz.getInterfaces()) { gatherAllDeclaredMethods(methods, c); } } /** * Create a representation of the signature which includes the method name. * This uniquely identifies the method. * * @param m method to describe */ private static String createSig(Method m) { return m.getName() + ':' + MethodDesc.forMethod(m).getDescriptor(); } private static final class Info<S extends Storable> implements StorableInfo<S> { private final Class<S> mType; private final String[] mAliases; private final StorableIndex<S>[] mIndexes; private final Map<String, StorableProperty<S>> mAllProperties; private final StorableKey<S> mPrimaryKey; private final StorableKey<S>[] mAltKeys; private final StorableKey<S> mPartitionKey; private final boolean mIndependent; private final boolean mAuthoritative; private transient String mName; private transient Map<String, StorableProperty<S>> mPrimaryKeyProperties; private transient Map<String, StorableProperty<S>> mDataProperties; private transient StorableProperty<S> mVersionProperty; Info(Class<S> type, String[] aliases, StorableIndex<S>[] indexes, Map<String, StorableProperty<S>> properties, StorableKey<S> primaryKey, StorableKey<S>[] altKeys, StorableKey<S> partitionKey, boolean independent, boolean authoritative) { mType = type; mAliases = aliases; mIndexes = indexes; mAllProperties = properties; mPrimaryKey = primaryKey; mAltKeys = altKeys; mPartitionKey = partitionKey; mIndependent = independent; mAuthoritative = authoritative; } public String getName() { String name = mName; if (name == null) { name = getStorableType().getName(); int index = name.lastIndexOf('.'); if (index >= 0) { name = name.substring(index + 1); } mName = name; } return name; } public Class<S> getStorableType() { return mType; } public Map<String, StorableProperty<S>> getAllProperties() { return mAllProperties; } public Map<String, StorableProperty<S>> getPrimaryKeyProperties() { if (mPrimaryKeyProperties == null) { Set<? extends OrderedProperty<S>> pkSet = mPrimaryKey.getProperties(); Map<String, StorableProperty<S>> pkProps = new LinkedHashMap<String, StorableProperty<S>>(pkSet.size()); for (OrderedProperty<S> prop : pkSet) { StorableProperty<S> prime = prop.getChainedProperty().getPrimeProperty(); pkProps.put(prime.getName(), prime); } mPrimaryKeyProperties = Collections.unmodifiableMap(pkProps); } return mPrimaryKeyProperties; } public Map<String, StorableProperty<S>> getDataProperties() { if (mDataProperties == null) { Map<String, StorableProperty<S>> dataProps = new LinkedHashMap<String, StorableProperty<S>>(mAllProperties.size()); for (Map.Entry<String, StorableProperty<S>> entry : mAllProperties.entrySet()) { StorableProperty<S> property = entry.getValue(); if (!property.isPrimaryKeyMember() && !property.isJoin()) { dataProps.put(entry.getKey(), property); } } mDataProperties = Collections.unmodifiableMap(dataProps); } return mDataProperties; } public StorableProperty<S> getVersionProperty() { if (mVersionProperty == null) { for (StorableProperty<S> property : mAllProperties.values()) { if (property.isVersion()) { mVersionProperty = property; break; } } } return mVersionProperty; } public StorableKey<S> getPrimaryKey() { return mPrimaryKey; } public int getAlternateKeyCount() { StorableKey<S>[] keys = mAltKeys; return keys == null ? 0 : keys.length; } public StorableKey<S> getAlternateKey(int index) { StorableKey<S>[] keys = mAltKeys; if (keys == null) { throw new IndexOutOfBoundsException(); } else { return keys[index]; } } @SuppressWarnings("unchecked") public StorableKey<S>[] getAlternateKeys() { StorableKey<S>[] keys = mAltKeys; if (keys == null) { return new StorableKey[0]; } else { return keys.clone(); } } public int getAliasCount() { String[] aliases = mAliases; return aliases == null ? 0 : aliases.length; } public String getAlias(int index) { String[] aliases = mAliases; if (aliases == null) { throw new IndexOutOfBoundsException(); } else { return aliases[index]; } } public String[] getAliases() { String[] aliases = mAliases; if (aliases == null) { return new String[0]; } else { return aliases.clone(); } } public int getIndexCount() { StorableIndex<S>[] indexes = mIndexes; return indexes == null ? 0 : indexes.length; } public StorableIndex<S> getIndex(int index) { StorableIndex<S>[] indexes = mIndexes; if (indexes == null) { throw new IndexOutOfBoundsException(); } else { return indexes[index]; } } @SuppressWarnings("unchecked") public StorableIndex<S>[] getIndexes() { StorableIndex<S>[] indexes = mIndexes; if (indexes == null) { return new StorableIndex[0]; } else { return indexes.clone(); } } public StorableKey<S> getPartitionKey() { return mPartitionKey; } public final boolean isIndependent() { return mIndependent; } public final boolean isAuthoritative() { return mAuthoritative; } } private static class SKey<S extends Storable> implements StorableKey<S> { private final boolean mPrimary; private final Set<OrderedProperty<S>> mProperties; SKey(boolean primary, Set<OrderedProperty<S>> properties) { mPrimary = primary; mProperties = properties; } public boolean isPrimary() { return mPrimary; } public Set<OrderedProperty<S>> getProperties() { return mProperties; } @Override public int hashCode() { return mProperties.hashCode(); } @SuppressWarnings("unchecked") @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof StorableKey) { StorableKey<S> other = (StorableKey<S>) obj; return isPrimary() == other.isPrimary() && getProperties().equals(other.getProperties()); } return false; } @Override public String toString() { StringBuilder b = new StringBuilder(); b.append("StorableKey "); try { appendTo(b); } catch (IOException e) { // Not gonna happen. } return b.toString(); } /** * Appends the same results as toString, but without the "StorableKey" * prefix. */ public void appendTo(Appendable app) throws IOException { app.append("{properties=["); int i = 0; for (OrderedProperty<S> prop : mProperties) { if (i++ > 0) { app.append(", "); } prop.appendTo(app); } app.append(']'); app.append(", primary="); app.append(String.valueOf(isPrimary())); app.append('}'); } } private static class SimpleProperty<S extends Storable> implements StorableProperty<S> { private static final long serialVersionUID = 6599542401516624863L; private static final ChainedProperty[] EMPTY_CHAIN_ARRAY = new ChainedProperty[0]; private final BeanProperty mBeanProperty; private final Class<S> mEnclosingType; private final boolean mNullable; private final boolean mPrimaryKey; private final boolean mAlternateKey; private final boolean mPartitionKey; private final String[] mAliases; private final StorablePropertyConstraint[] mConstraints; private final StorablePropertyAdapter mAdapter; private final boolean mIsVersion; private final String mSequence; private final boolean mIndependent; private final boolean mAutomatic; private final boolean mIsDerived; private final boolean mShouldCopyDerived; private final String mName; private final String mBeanName; // Temporary reference until derived from is resolved. private Derived mDerived; // Resolved derived from properties. private ChainedProperty<S>[] mDerivedFrom; // Resolved derived to properties. private ChainedProperty<S>[] mDerivedTo; // Resolved number. private int mNumber = -1; // Reference to enclosing StorableInfo. This reference exists to // prevent the StorableInfo from being uncached so as long as a // reference from a property exists. protected StorableInfo<S> mEnclosingInfo; SimpleProperty(BeanProperty property, Class<S> enclosing, boolean nullable, boolean primaryKey, boolean alternateKey, boolean partitionKey, String[] aliases, StorablePropertyConstraint[] constraints, StorablePropertyAdapter adapter, boolean isVersion, String sequence, boolean independent, boolean automatic, Derived derived, String name) { mBeanProperty = property; mEnclosingType = enclosing; mNullable = property.getType().isPrimitive() ? false : nullable; mPrimaryKey = primaryKey; mAlternateKey = alternateKey; mPartitionKey = partitionKey; mAliases = aliases; mConstraints = constraints; mAdapter = adapter; mIsVersion = isVersion; mSequence = sequence; mIndependent = independent; mAutomatic = automatic; mIsDerived = derived != null; mShouldCopyDerived = (mIsDerived ? derived.shouldCopy() : false); mDerived = derived; mBeanName = mBeanProperty.getName(); mName = name == null ? mBeanName : name; } public final String getName() { return mName; } public final String getBeanName() { return mBeanName; } public final Class<?> getType() { return mBeanProperty.getType(); } public Class<?>[] getCovariantTypes() { return StorableIntrospector.getCovariantTypes(mBeanProperty); } public final int getNumber() { return mNumber; } public final Class<S> getEnclosingType() { return mEnclosingType; } public final Method getReadMethod() { return mBeanProperty.getReadMethod(); } public final String getReadMethodName() { Method m = mBeanProperty.getReadMethod(); if (m != null) { return m.getName(); } // Return synthetic name. return "get" + getWriteMethod().getName().substring(3); } public final Method getWriteMethod() { return mBeanProperty.getWriteMethod(); } public final String getWriteMethodName() { Method m = mBeanProperty.getWriteMethod(); if (m != null) { return m.getName(); } // Return synthetic name. String readName = getReadMethod().getName(); return "set" + readName.substring(readName.startsWith("is") ? 2 : 3); } public final boolean isNullable() { return mNullable; } public final boolean isPrimaryKeyMember() { return mPrimaryKey; } public final boolean isAlternateKeyMember() { return mAlternateKey; } public final boolean isPartitionKeyMember() { return mPartitionKey; } public final int getAliasCount() { String[] aliases = mAliases; return aliases == null ? 0 : aliases.length; } public final String getAlias(int index) { String[] aliases = mAliases; if (aliases == null) { throw new IndexOutOfBoundsException(); } else { return aliases[index]; } } public final String[] getAliases() { String[] aliases = mAliases; if (aliases == null) { return new String[0]; } else { return aliases.clone(); } } public final String getSequenceName() { return mSequence; } public final boolean isAutomatic() { return mAutomatic; } public final boolean isIndependent() { return mIndependent; } public final boolean isVersion() { return mIsVersion; } public final boolean isDerived() { return mIsDerived; } public final ChainedProperty<S>[] getDerivedFromProperties() { return (!mIsDerived || mDerivedFrom == null) ? EMPTY_CHAIN_ARRAY : mDerivedFrom.clone(); } public final ChainedProperty<?>[] getDerivedToProperties() { if (mDerivedTo == null) { // Derived-to properties must be determined on demand because // introspection might have been initiated by a dependency. If // that dependency is asked for derived properties, it will not // yet have resolved derived-from properties. Set<ChainedProperty<?>> derivedToSet = new LinkedHashSet<ChainedProperty<?>>(); Set<Class<?>> examinedSet = new HashSet<Class<?>>(); addToDerivedToSet(derivedToSet, examinedSet, examine(getEnclosingType())); if (derivedToSet.size() > 0) { mDerivedTo = derivedToSet.toArray(new ChainedProperty[derivedToSet.size()]); } else { mDerivedTo = EMPTY_CHAIN_ARRAY; } } return mDerivedTo.clone(); } public final boolean shouldCopyDerived() { return mShouldCopyDerived; } public boolean isJoin() { return false; } public boolean isOneToOneJoin() { return false; } public Class<? extends Storable> getJoinedType() { return null; } public int getJoinElementCount() { return 0; } public StorableProperty<S> getInternalJoinElement(int index) { throw new IndexOutOfBoundsException(); } @SuppressWarnings("unchecked") public StorableProperty<S>[] getInternalJoinElements() { return new StorableProperty[0]; } public StorableProperty<?> getExternalJoinElement(int index) { throw new IndexOutOfBoundsException(); } public StorableProperty<?>[] getExternalJoinElements() { return new StorableProperty[0]; } public boolean isQuery() { return false; } public int getConstraintCount() { StorablePropertyConstraint[] constraints = mConstraints; return constraints == null ? 0 : constraints.length; } public StorablePropertyConstraint getConstraint(int index) { StorablePropertyConstraint[] constraints = mConstraints; if (constraints == null) { throw new IndexOutOfBoundsException(); } else { return constraints[index]; } } public StorablePropertyConstraint[] getConstraints() { StorablePropertyConstraint[] constraints = mConstraints; if (constraints == null) { return new StorablePropertyConstraint[0]; } else { return constraints.clone(); } } public StorablePropertyAdapter getAdapter() { return mAdapter; } @Override public int hashCode() { return (getName().hashCode() * 31 + getType().getName().hashCode()) * 31 + getEnclosingType().getName().hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof StorableProperty) { StorableProperty other = (StorableProperty) obj; return getName().equals(other.getName()) && getType().equals(other.getType()) && getEnclosingType().equals(other.getEnclosingType()); } return false; } @Override public String toString() { StringBuilder b = new StringBuilder(); try { appendTo(b); } catch (IOException e) { // Not gonna happen } return b.toString(); } public void appendTo(Appendable app) throws IOException { app.append("StorableProperty {name="); app.append(getName()); app.append(", type="); app.append(TypeDesc.forClass(getType()).getFullName()); app.append(", enclosing="); app.append(getEnclosingType().getName()); app.append('}'); } void setNumber(int number) { mNumber = number; } void setEnclosingInfo(StorableInfo<S> info) { mEnclosingInfo = info; } void resolveDerivedFrom(List<String> errorMessages) { Derived derived = mDerived; // Don't need this anymore. mDerived = null; if (!mIsDerived || derived == null) { return; } String[] fromNames = derived.from(); if (fromNames == null || fromNames.length == 0) { return; } Set<ChainedProperty<S>> derivedFromSet = new LinkedHashSet<ChainedProperty<S>>(); for (String fromName : fromNames) { ChainedProperty<S> from; try { from = ChainedProperty.parse(mEnclosingInfo, fromName); } catch (IllegalArgumentException e) { errorMessages.add ("Cannot find derived-from property: \"" + getName() + "\" reports being derived from \"" + fromName + '"'); continue; } addToDerivedFromSet(errorMessages, derivedFromSet, from); } if (derivedFromSet.size() > 0) { if (derivedFromSet.contains(ChainedProperty.get(this))) { errorMessages.add ("Derived-from dependency cycle detected: \"" + getName() + '"'); } mDerivedFrom = derivedFromSet .toArray(new ChainedProperty[derivedFromSet.size()]); } else { mDerivedFrom = null; } } private boolean addToDerivedFromSet(List<String> errorMessages, Set<ChainedProperty<S>> derivedFromSet, ChainedProperty<S> from) { if (derivedFromSet.contains(from)) { return false; } derivedFromSet.add(from); ChainedProperty<S> trimmed = from.getChainCount() == 0 ? null : from.trim(); if (trimmed != null) { // Include all join properties as dependencies. addToDerivedFromSet(errorMessages, derivedFromSet, trimmed); } StorableProperty<?> lastInChain = from.getLastProperty(); if (lastInChain.isDerived()) { // Expand derived dependencies. ((SimpleProperty) lastInChain).resolveDerivedFrom(errorMessages); for (ChainedProperty<?> lastFrom : lastInChain.getDerivedFromProperties()) { ChainedProperty<S> dep; if (trimmed == null) { dep = (ChainedProperty<S>) lastFrom; } else { dep = trimmed.append(lastFrom); } addToDerivedFromSet(errorMessages, derivedFromSet, dep); } } if (lastInChain.isJoin() && errorMessages.size() == 0) { // Make sure that join is doubly specified. Why? Consider the // case where the derived property is a member of an index or // key. If the joined Storable class gets loaded first, it will // not know that an index exists that it should keep // up-to-date. With the double join, it can check to see if // there are any foreign indexes. This check could probably be // skipped if the derived property doesn't belong to an index // or key, but consistent error checking behavior is desirable. Class<? extends Storable> joined = lastInChain.getJoinedType(); doubly: { for (StorableProperty<?> prop : examine(joined).getAllProperties().values()) { if (prop.isJoin() && prop.getJoinedType() == lastInChain.getEnclosingType()) { break doubly; } } StringBuilder suggest = new StringBuilder(); suggest.append("@Join"); int count = lastInChain.getJoinElementCount(); boolean naturalJoin = true; for (int i=0; i<count; i++) { if (!lastInChain.getInternalJoinElement(i).getName().equals (lastInChain.getExternalJoinElement(i).getName())) { naturalJoin = false; break; } } if (!naturalJoin) { suggest.append("(internal="); if (count > 1) { suggest.append('{'); } for (int i=0; i<count; i++) { if (i > 0) { suggest.append(", "); } suggest.append('"'); // This property's external is other's internal. suggest.append(lastInChain.getExternalJoinElement(i).getName()); suggest.append('"'); } if (count > 1) { suggest.append('}'); } suggest.append(", external="); if (count > 1) { suggest.append('{'); } for (int i=0; i<count; i++) { if (i > 0) { suggest.append(", "); } suggest.append('"'); // This property's internal is other's external. suggest.append(lastInChain.getInternalJoinElement(i).getName()); suggest.append('"'); } if (count > 1) { suggest.append('}'); } suggest.append(")"); } suggest.append(' '); if (!joined.isInterface()) { suggest.append("public abstract "); } if (lastInChain.isOneToOneJoin() || lastInChain.isQuery()) { suggest.append(lastInChain.getEnclosingType().getName()); } else { suggest.append("Query<"); suggest.append(lastInChain.getEnclosingType().getName()); suggest.append('>'); } suggest.append(" getXxx() throws FetchException"); errorMessages.add ("Derived-from property is a join, but it is not doubly joined: \"" + getName() + "\" is derived from \"" + from + "\". Consider defining a join property in " + joined + " as: " + suggest); } } return true; } private boolean addToDerivedToSet(Set<ChainedProperty<?>> derivedToSet, Set<Class<?>> examinedSet, StorableInfo<?> info) { if (examinedSet.contains(info.getStorableType())) { return false; } // Prevent infinite loop while following join paths. examinedSet.add(info.getStorableType()); final int originalSize = derivedToSet.size(); for (StorableProperty<?> property : info.getAllProperties().values()) { if (property.isDerived()) { for (ChainedProperty<?> from : property.getDerivedFromProperties()) { if (from.getLastProperty().equals(this)) { ChainedProperty<?> path = ChainedProperty.get(property); if (from.getChainCount() > 0) { path = path.append(from.trim()); } derivedToSet.add(path); } } } if (property.isJoin()) { addToDerivedToSet(derivedToSet, examinedSet, examine(property.getJoinedType())); } } return derivedToSet.size() > originalSize; } // Package-private in order to be inherited by subclasses. Object writeReplace() { return new NaET(mName, mEnclosingType); } // Name and Enclosing Type private static class NaET implements Externalizable { private static final long serialVersionUID = 1L; private String mName; private Class<? extends Storable> mEnclosingType; // Required for Externalizable. public NaET() { } NaET(String name, Class<? extends Storable> enclosingType) { mName = name; mEnclosingType = enclosingType; } public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(mName); out.writeObject(mEnclosingType); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { mName = (String) in.readObject(); mEnclosingType = (Class<? extends Storable>) in.readObject(); } private Object readResolve() { return StorableIntrospector.examine(mEnclosingType).getAllProperties().get(mName); } } } private static final class JoinProperty<S extends Storable> extends SimpleProperty<S> { private static final long serialVersionUID = 5617446241872193369L; private final Class<? extends Storable> mJoinedType; // Just the names of the join properties, held here until properties // are fully resolved. After properties are resolved, arrays are thrown away. private String[] mInternalNames; private String[] mExternalNames; // Resolved join properties. private StorableProperty<S>[] mInternal; private StorableProperty<?>[] mExternal; private boolean mOneToOne; JoinProperty(BeanProperty property, Class<S> enclosing, boolean nullable, String[] aliases, StorablePropertyConstraint[] constraints, StorablePropertyAdapter adapter, String sequence, boolean independent, boolean automatic, Derived derived, Class<? extends Storable> joinedType, String[] internal, String[] external, String name) { super(property, enclosing, nullable, false, false, false, aliases, constraints, adapter, false, sequence, independent, automatic, derived, name); mJoinedType = joinedType; int length = internal.length; if (length != external.length) { throw new IllegalArgumentException(); } mInternalNames = internal; mExternalNames = external; } @Override public boolean isJoin() { return true; } @Override public boolean isOneToOneJoin() { return mOneToOne; } @Override public Class<? extends Storable> getJoinedType() { return mJoinedType; } @Override public int getJoinElementCount() { return mInternal.length; } @Override public StorableProperty<S> getInternalJoinElement(int index) { return mInternal[index]; } @Override public StorableProperty<S>[] getInternalJoinElements() { return mInternal.clone(); } @Override public StorableProperty<?> getExternalJoinElement(int index) { return mExternal[index]; } @Override public StorableProperty<?>[] getExternalJoinElements() { return mExternal.clone(); } @Override public boolean isQuery() { return getType() == Query.class; } /** * Finishes the definition of this join property. Can only be called once. */ @SuppressWarnings("unchecked") void resolveJoin(List<String> errorMessages) { StorableInfo<?> joinedInfo; try { joinedInfo = examine(getJoinedType()); if (mInternalNames.length == 0) { // Since no join elements specified, perform a natural join. // If the joined type is a list, then the join elements are // defined by this enclosing type's primary keys. Otherwise, // they are defined by the joined type's primary keys. Map<String, ? extends StorableProperty<?>> primaryKeys; if (isQuery()) { primaryKeys = examine(getEnclosingType()).getPrimaryKeyProperties(); } else { primaryKeys = joinedInfo.getPrimaryKeyProperties(); } mInternalNames = new String[primaryKeys.size()]; mExternalNames = new String[primaryKeys.size()]; int i = 0; for (String name : primaryKeys.keySet()) { mInternalNames[i] = name; mExternalNames[i] = name; i++; } } } catch (MalformedTypeException e) { mInternal = new StorableProperty[0]; mExternal = new StorableProperty[0]; throw e; } mInternal = new StorableProperty[mInternalNames.length]; mExternal = new StorableProperty[mExternalNames.length]; // Verify that internal properties exist and are not themselves joins. for (int i=0; i<mInternalNames.length; i++) { String internalName = mInternalNames[i]; StorableProperty property = mEnclosingInfo.getAllProperties().get(internalName); if (property == null) { errorMessages.add ("Cannot find internal join element: \"" + getName() + "\" internally joins to property \"" + internalName + '"'); continue; } if (property.isJoin()) { errorMessages.add ("Join properties cannot join to other join properties: \"" + getName() + "\" internally joins to property \"" + internalName + '"'); continue; } if (Lob.class.isAssignableFrom(property.getType())) { errorMessages.add ("Join properties cannot join to LOB properties: \"" + getName() + "\" internally joins to LOB property \"" + internalName + '"'); continue; } /* this check is too restrictive if (property.isNullable() && !isNullable()) { errorMessages.add ("Join must be declared nullable since internal element " + "is nullable: \"" + getName() + "\" internally joins to nullable property \"" + internalName + '"'); } */ mInternal[i] = property; } // Verify that external properties exist and are not themselves joins. Map<String, ? extends StorableProperty<?>> externalProperties = joinedInfo.getAllProperties(); // Track whether join property is allowed to have a set method. boolean mutatorAllowed = !isQuery(); for (int i=0; i<mExternalNames.length; i++) { String externalName = mExternalNames[i]; StorableProperty property = externalProperties.get(externalName); if (property == null) { errorMessages.add ("Cannot find external join element: \"" + getName() + "\" externally joins to property \"" + externalName + '"'); continue; } if (property.isJoin()) { errorMessages.add ("Join properties cannot join to other join properties: \"" + getName() + "\" externally joins to property \"" + externalName + '"'); continue; } if (Lob.class.isAssignableFrom(property.getType())) { errorMessages.add ("Join properties cannot join to LOB properties: \"" + getName() + "\" externally joins to LOB property \"" + externalName + '"'); continue; } if (property.getReadMethod() == null) { mutatorAllowed = false; if (getWriteMethod() != null) { errorMessages.add ("Join property cannot have a mutator if external property " + "has no accessor: Mutator = \"" + getWriteMethod() + "\", external property = \"" + property.getName() + '"'); continue; } } mExternal[i] = property; } if (errorMessages.size() > 0) { return; } // Verify that join types match type. for (int i=0; i<mInternal.length; i++) { StorableProperty internalProperty = getInternalJoinElement(i); StorableProperty externalProperty = getExternalJoinElement(i); if (!internalProperty.isNullable() && externalProperty.isNullable()) { mutatorAllowed = false; if (getWriteMethod() != null) { errorMessages.add ("Join property cannot have a mutator if internal property " + "is required, but external property is nullable: Mutator = \"" + getWriteMethod() + "\", internal property = \"" + internalProperty.getName() + "\", external property = \"" + externalProperty.getName() + '"'); } } Class internalClass = internalProperty.getType(); Class externalClass = externalProperty.getType(); if (internalClass == externalClass) { continue; } TypeDesc internalType = TypeDesc.forClass(internalClass).toObjectType(); TypeDesc externalType = TypeDesc.forClass(externalClass).toObjectType(); if (internalType == externalType) { continue; } compatibilityCheck: { // Conversion to String, CharSequence, or Object is always // allowed. if (externalClass == String.class || externalClass == Object.class || externalClass == CharSequence.class) { break compatibilityCheck; } // Allow internal type to be "narrower" than external type. // (byte) ==> (Number | short | int | long) // (byte | short) ==> (Number | int | long) // (byte | short | int) ==> (Number | long) // (float) ==> (Number | double) TypeDesc primInternal = internalType.toPrimitiveType(); TypeDesc primExternal = externalType.toPrimitiveType(); if (primInternal != null) { switch (primInternal.getTypeCode()) { case TypeDesc.BYTE_CODE: if (primExternal == null) { if (externalType.toClass() == Number.class) { break compatibilityCheck; } } else { switch (primExternal.getTypeCode()) { case TypeDesc.SHORT_CODE: case TypeDesc.INT_CODE: case TypeDesc.LONG_CODE: break compatibilityCheck; } } break; case TypeDesc.SHORT_CODE: if (primExternal == null) { if (externalType.toClass() == Number.class) { break compatibilityCheck; } } else { switch (primExternal.getTypeCode()) { case TypeDesc.INT_CODE: case TypeDesc.LONG_CODE: break compatibilityCheck; } } break; case TypeDesc.INT_CODE: if (primExternal == null) { if (externalType.toClass() == Number.class) { break compatibilityCheck; } } else { if (primExternal == TypeDesc.LONG) { break compatibilityCheck; } } break; case TypeDesc.FLOAT_CODE: if (primExternal == null) { if (externalType.toClass() == Number.class) { break compatibilityCheck; } } else { if (primExternal == TypeDesc.DOUBLE) { break compatibilityCheck; } } break; } } errorMessages.add ("Join property internal/external type mismatch for \"" + getName() + "\": internal join \"" + getInternalJoinElement(i).getName() + "\" is of type \"" + getInternalJoinElement(i).getType() + "\" and external join \"" + getExternalJoinElement(i).getName() + "\" is of type \"" + getExternalJoinElement(i).getType() + '"'); continue; } // If this point is reached, then types differ, but they are // compatible. Still, a mutator on this join property is not // allowed due to the difference. if (getWriteMethod() != null) { mutatorAllowed = false; errorMessages.add ("Join property cannot have a mutator if external type cannot " + "be reliably converted to internal type: Mutator = \"" + getWriteMethod() + "\", internal join \"" + getInternalJoinElement(i).getName() + "\" is of type \"" + getInternalJoinElement(i).getType() + "\" and external join \"" + getExternalJoinElement(i).getName() + "\" is of type \"" + getExternalJoinElement(i).getType() + '"'); } } if (errorMessages.size() > 0) { return; } // Test which keys of joined object are specified. // Create a copy of all the primary keys of joined object. Set<StorableProperty> primaryKey = new HashSet<StorableProperty>(joinedInfo.getPrimaryKeyProperties().values()); if (primaryKey.size() == 0) { // Assume another error prevented primary key from being defined. primaryKey = null; } else { // Remove external properties from the primary key set. for (int i=0; i<mInternal.length; i++) { primaryKey.remove(getExternalJoinElement(i)); } } // Do similar test for alternate keys. int altKeyCount = joinedInfo.getAlternateKeyCount(); List<Set<StorableProperty>> altKeys = new ArrayList<Set<StorableProperty>>(altKeyCount); altKeyScan: for (int i=0; i<altKeyCount; i++) { Set<StorableProperty> altKey = new HashSet<StorableProperty>(); for (OrderedProperty op : joinedInfo.getAlternateKey(i).getProperties()) { ChainedProperty chained = op.getChainedProperty(); if (chained.getChainCount() > 0) { // Funny alt key. Pretend it does not exist. continue altKeyScan; } altKey.add(chained.getPrimeProperty()); } if (altKey.size() > 0) { altKeys.add(altKey); // Remove external properties from the alternate key set. for (int j=0; j<mInternal.length; j++) { altKey.remove(getExternalJoinElement(j)); } } } if (isQuery()) { // Key of joined object must not be completely specified. if (primaryKey != null && primaryKey.size() <= 0) { errorMessages.add ("Join property \"" + getName() + "\" completely specifies primary key of joined object; " + "consider declaring the property type as just " + getJoinedType().getName()); } for (Set<StorableProperty> altKey : altKeys) { if (altKey.size() <= 0) { errorMessages.add ("Join property \"" + getName() + "\" completely specifies an alternate key of joined object; " + "consider declaring the property type as just " + getJoinedType().getName()); break; } } } else { // Key of joined object must be completely specified. fullKeyCheck: { if (primaryKey == null || primaryKey.size() <= 0) { break fullKeyCheck; } for (Set<StorableProperty> altKey : altKeys) { if (altKey.size() <= 0) { break fullKeyCheck; } } errorMessages.add ("Join property \"" + getName() + "\" doesn't completely specify any key of joined object; consider " + "declaring the property type as Query<" + getJoinedType().getName() + '>'); } // Determine if one-to-one join. If internal properties // completely specify any key, then it is one-to-one. boolean oneToOne = false; oneToOneCheck: { Set<StorableProperty> internalPrimaryKey = new HashSet<StorableProperty> (mEnclosingInfo.getPrimaryKeyProperties().values()); for (int i=0; i<mInternal.length; i++) { internalPrimaryKey.remove(getInternalJoinElement(i)); if (internalPrimaryKey.size() == 0) { oneToOne = true; break oneToOneCheck; } } altKeyScan: for (int i=0; i<mEnclosingInfo.getAlternateKeyCount(); i++) { Set<StorableProperty> altKey = new HashSet<StorableProperty>(); for (OrderedProperty op : mEnclosingInfo.getAlternateKey(i).getProperties()) { ChainedProperty chained = op.getChainedProperty(); if (chained.getChainCount() > 0) { // Funny alt key. Pretend it does not exist. continue altKeyScan; } altKey.add(chained.getPrimeProperty()); } for (int j=0; j<mInternal.length; j++) { altKey.remove(getInternalJoinElement(j)); if (altKey.size() == 0) { oneToOne = true; break oneToOneCheck; } } } } mOneToOne = oneToOne; } if (mutatorAllowed && getWriteMethod() == null) { // Require set method to aid join query optimizations. Without // set method, inner join result can be needlessly tossed away. errorMessages.add ("Must define proper 'set' method for join property: " + getName()); } if (errorMessages.size() == 0) { // No errors, throw away names arrays. mInternalNames = null; mExternalNames = null; } } } }