/*- * See the file LICENSE for redistribution information. * * Copyright (c) 2002, 2010 Oracle and/or its affiliates. All rights reserved. * */ package com.sleepycat.persist.model; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import com.sleepycat.compat.DbCompat; /** * The default annotation-based entity model. An AnnotationModel * is based on annotations that are specified for entity classes and their key * fields. * *

{@code AnnotationModel} objects are thread-safe. Multiple threads may * safely call the methods of a shared {@code AnnotationModel} object.

* *

The set of persistent classes in the annotation model is the set of all * classes with the {@link Persistent} or {@link Entity} annotation.

* *

The annotations used to define persistent classes are: {@link Entity}, * {@link Persistent}, {@link PrimaryKey}, {@link SecondaryKey} and {@link * KeyField}. A good starting point is {@link Entity}.

* * @author Mark Hayes */ public class AnnotationModel extends EntityModel { private static class EntityInfo { PrimaryKeyMetadata priKey; Map secKeys = new HashMap(); } private Map classMap; private Map entityMap; /** * Constructs a model for annotated entity classes. */ public AnnotationModel() { super(); classMap = new HashMap(); entityMap = new HashMap(); } /* EntityModel methods */ @Override public synchronized Set getKnownClasses() { return Collections.unmodifiableSet (new HashSet(classMap.keySet())); } @Override public synchronized EntityMetadata getEntityMetadata(String className) { /* Call getClassMetadata to collect metadata. */ getClassMetadata(className); /* Return the collected entity metadata. */ EntityInfo info = entityMap.get(className); if (info != null) { return new EntityMetadata (className, info.priKey, Collections.unmodifiableMap(info.secKeys)); } else { return null; } } @Override public synchronized ClassMetadata getClassMetadata(String className) { ClassMetadata metadata = classMap.get(className); if (metadata == null) { Class type; try { type = EntityModel.classForName(className); } catch (ClassNotFoundException e) { return null; } /* Get class annotation. */ Entity entity = type.getAnnotation(Entity.class); Persistent persistent = type.getAnnotation(Persistent.class); if (entity == null && persistent == null) { return null; } if (type.isEnum() || type.isInterface() || type.isPrimitive()) { throw new IllegalArgumentException ("@Entity and @Persistent not allowed for enum, " + "interface, or primitive type: " + type.getName()); } if (entity != null && persistent != null) { throw new IllegalArgumentException ("Both @Entity and @Persistent are not allowed: " + type.getName()); } boolean isEntity; int version; String proxiedClassName; if (entity != null) { isEntity = true; version = entity.version(); proxiedClassName = null; } else { isEntity = false; version = persistent.version(); Class proxiedClass = persistent.proxyFor(); proxiedClassName = (proxiedClass != void.class) ? proxiedClass.getName() : null; } /* Get instance fields. */ List fields = new ArrayList(); boolean nonDefaultRules = getInstanceFields(fields, type); Collection nonDefaultFields = null; if (nonDefaultRules) { nonDefaultFields = new ArrayList(fields.size()); for (Field field : fields) { nonDefaultFields.add(new FieldMetadata (field.getName(), field.getType().getName(), type.getName())); } nonDefaultFields = Collections.unmodifiableCollection(nonDefaultFields); } /* Get the rest of the metadata and save it. */ metadata = new ClassMetadata (className, version, proxiedClassName, isEntity, getPrimaryKey(type, fields), getSecondaryKeys(type, fields), getCompositeKeyFields(type, fields), nonDefaultFields); classMap.put(className, metadata); /* Add any new information about entities. */ updateEntityInfo(metadata); } return metadata; } /** * Fills in the fields array and returns true if the default rules for * field persistence were overridden. */ private boolean getInstanceFields(List fields, Class type) { boolean nonDefaultRules = false; for (Field field : type.getDeclaredFields()) { boolean notPersistent = (field.getAnnotation(NotPersistent.class) != null); boolean notTransient = (field.getAnnotation(NotTransient.class) != null); if (notPersistent && notTransient) { throw new IllegalArgumentException ("Both @NotTransient and @NotPersistent not allowed"); } if (notPersistent || notTransient) { nonDefaultRules = true; } int mods = field.getModifiers(); if (!Modifier.isStatic(mods) && !notPersistent && (!Modifier.isTransient(mods) || notTransient)) { /* Field is DPL persistent. */ fields.add(field); } else { /* If non-persistent, no other annotations should be used. */ if (field.getAnnotation(PrimaryKey.class) != null || field.getAnnotation(SecondaryKey.class) != null || field.getAnnotation(KeyField.class) != null) { throw new IllegalArgumentException ("@PrimaryKey, @SecondaryKey and @KeyField not " + "allowed on non-persistent field"); } } } return nonDefaultRules; } private PrimaryKeyMetadata getPrimaryKey(Class type, List fields) { Field foundField = null; String sequence = null; for (Field field : fields) { PrimaryKey priKey = field.getAnnotation(PrimaryKey.class); if (priKey != null) { if (foundField != null) { throw new IllegalArgumentException ("Only one @PrimaryKey allowed: " + type.getName()); } else { foundField = field; sequence = priKey.sequence(); if (sequence.length() == 0) { sequence = null; } } } } if (foundField != null) { return new PrimaryKeyMetadata (foundField.getName(), foundField.getType().getName(), type.getName(), sequence); } else { return null; } } private Map getSecondaryKeys(Class type, List fields) { Map map = null; for (Field field : fields) { SecondaryKey secKey = field.getAnnotation(SecondaryKey.class); if (secKey != null) { Relationship rel = secKey.relate(); String elemClassName = null; if (rel == Relationship.ONE_TO_MANY || rel == Relationship.MANY_TO_MANY) { elemClassName = getElementClass(field); } String keyName = secKey.name(); if (keyName.length() == 0) { keyName = field.getName(); } Class relatedClass = secKey.relatedEntity(); String relatedEntity = (relatedClass != void.class) ? relatedClass.getName() : null; DeleteAction deleteAction = (relatedEntity != null) ? secKey.onRelatedEntityDelete() : null; SecondaryKeyMetadata metadata = new SecondaryKeyMetadata (field.getName(), field.getType().getName(), type.getName(), elemClassName, keyName, rel, relatedEntity, deleteAction); if (map == null) { map = new HashMap(); } if (map.put(keyName, metadata) != null) { throw new IllegalArgumentException ("Only one @SecondaryKey with the same name allowed: " + type.getName() + '.' + keyName); } } } if (map != null) { map = Collections.unmodifiableMap(map); } return map; } private String getElementClass(Field field) { Class cls = field.getType(); if (cls.isArray()) { return cls.getComponentType().getName(); } if (Collection.class.isAssignableFrom(cls)) { Type[] typeArgs = null; if (field.getGenericType() instanceof ParameterizedType) { typeArgs = ((ParameterizedType) field.getGenericType()). getActualTypeArguments(); } if (typeArgs == null || typeArgs.length != 1 || !(typeArgs[0] instanceof Class)) { throw new IllegalArgumentException ("Collection typed secondary key field must have a" + " single generic type argument and a wildcard or" + " type bound is not allowed: " + field.getDeclaringClass().getName() + '.' + field.getName()); } return ((Class) typeArgs[0]).getName(); } throw new IllegalArgumentException ("ONE_TO_MANY or MANY_TO_MANY secondary key field must have" + " an array or Collection type: " + field.getDeclaringClass().getName() + '.' + field.getName()); } private List getCompositeKeyFields(Class type, List fields) { List list = null; for (Field field : fields) { KeyField keyField = field.getAnnotation(KeyField.class); if (keyField != null) { int value = keyField.value(); if (value < 1 || value > fields.size()) { throw new IllegalArgumentException ("Unreasonable @KeyField index value " + value + ": " + type.getName()); } if (list == null) { list = new ArrayList(fields.size()); } if (value <= list.size() && list.get(value - 1) != null) { throw new IllegalArgumentException ("@KeyField index value " + value + " is used more than once: " + type.getName()); } while (value > list.size()) { list.add(null); } FieldMetadata metadata = new FieldMetadata (field.getName(), field.getType().getName(), type.getName()); list.set(value - 1, metadata); } } if (list != null) { if (list.size() < fields.size()) { throw new IllegalArgumentException ("@KeyField is missing on one or more instance fields: " + type.getName()); } for (int i = 0; i < list.size(); i += 1) { if (list.get(i) == null) { throw new IllegalArgumentException ("@KeyField is missing for index value " + (i + 1) + ": " + type.getName()); } } } if (list != null) { list = Collections.unmodifiableList(list); } return list; } /** * Add newly discovered metadata to our stash of entity info. This info * is maintained as it is discovered because it would be expensive to * create it on demand -- all class metadata would have to be traversed. */ private void updateEntityInfo(ClassMetadata metadata) { /* * Find out whether this class or its superclass is an entity. In the * process, traverse all superclasses to load their metadata -- this * will populate as much entity info as possible. */ String entityClass = null; PrimaryKeyMetadata priKey = null; Map secKeys = new HashMap(); for (ClassMetadata data = metadata; data != null;) { if (data.isEntityClass()) { if (entityClass != null) { throw new IllegalArgumentException ("An entity class may not be derived from another" + " entity class: " + entityClass + ' ' + data.getClassName()); } entityClass = data.getClassName(); } /* Save first primary key encountered. */ if (priKey == null) { priKey = data.getPrimaryKey(); } /* Save all secondary keys encountered by key name. */ Map classSecKeys = data.getSecondaryKeys(); if (classSecKeys != null) { for (SecondaryKeyMetadata secKey : classSecKeys.values()) { secKeys.put(secKey.getKeyName(), secKey); } } /* Load superclass metadata. */ Class cls; try { cls = EntityModel.classForName(data.getClassName()); } catch (ClassNotFoundException e) { throw DbCompat.unexpectedException(e); } cls = cls.getSuperclass(); if (cls != Object.class) { data = getClassMetadata(cls.getName()); if (data == null) { throw new IllegalArgumentException ("Persistent class has non-persistent superclass: " + cls.getName()); } } else { data = null; } } /* Add primary and secondary key entity info. */ if (entityClass != null) { EntityInfo info = entityMap.get(entityClass); if (info == null) { info = new EntityInfo(); entityMap.put(entityClass, info); } if (priKey == null) { throw new IllegalArgumentException ("Entity class has no primary key: " + entityClass); } info.priKey = priKey; info.secKeys.putAll(secKeys); } } }