/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2011, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate.metamodel.source.annotations.entity;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javax.persistence.AccessType;
import com.fasterxml.classmate.ResolvedTypeWithMembers;
import com.fasterxml.classmate.members.HierarchicType;
import com.fasterxml.classmate.members.ResolvedMember;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.MethodInfo;
import org.jboss.logging.Logger;
import org.hibernate.AnnotationException;
import org.hibernate.AssertionFailure;
import org.hibernate.EntityMode;
import org.hibernate.HibernateException;
import org.hibernate.cfg.NotYetImplementedException;
import org.hibernate.metamodel.source.MappingException;
import org.hibernate.metamodel.source.annotations.AnnotationBindingContext;
import org.hibernate.metamodel.source.annotations.HibernateDotNames;
import org.hibernate.metamodel.source.annotations.JPADotNames;
import org.hibernate.metamodel.source.annotations.JandexHelper;
import org.hibernate.metamodel.source.annotations.ReflectionHelper;
import org.hibernate.metamodel.source.annotations.attribute.AssociationAttribute;
import org.hibernate.metamodel.source.annotations.attribute.AttributeNature;
import org.hibernate.metamodel.source.annotations.attribute.AttributeOverride;
import org.hibernate.metamodel.source.annotations.attribute.BasicAttribute;
/**
* Base class for a configured entity, mapped super class or embeddable
*
* @author Hardy Ferentschik
*/
public class ConfiguredClass {
public static final Logger LOG = Logger.getLogger( ConfiguredClass.class.getName() );
/**
* The parent of this configured class or {@code null} in case this configured class is the root of a hierarchy.
*/
private final ConfiguredClass parent;
/**
* The Jandex class info for this configured class. Provides access to the annotation defined on this configured class.
*/
private final ClassInfo classInfo;
/**
* The actual java type.
*/
private final Class<?> clazz;
/**
* The default access type for this entity
*/
private final AccessType classAccessType;
/**
* The type of configured class, entity, mapped super class, embeddable, ...
*/
private final ConfiguredClassType configuredClassType;
/**
* The id attributes
*/
private final Map<String, BasicAttribute> idAttributeMap;
/**
* The mapped association attributes for this entity
*/
private final Map<String, AssociationAttribute> associationAttributeMap;
/**
* The mapped simple attributes for this entity
*/
private final Map<String, BasicAttribute> simpleAttributeMap;
/**
* The version attribute or {@code null} in case none exists.
*/
private BasicAttribute versionAttribute;
/**
* The embedded classes for this entity
*/
private final Map<String, EmbeddableClass> embeddedClasses = new HashMap<String, EmbeddableClass>();
/**
* A map of all attribute overrides defined in this class. The override name is "normalised", meaning as if specified
* on class level. If the override is specified on attribute level the attribute name is used as prefix.
*/
private final Map<String, AttributeOverride> attributeOverrideMap;
private final Set<String> transientFieldNames = new HashSet<String>();
private final Set<String> transientMethodNames = new HashSet<String>();
/**
* Fully qualified name of a custom tuplizer
*/
private final String customTuplizer;
private final EntityBindingContext localBindingContext;
public ConfiguredClass(
ClassInfo classInfo,
AccessType defaultAccessType,
ConfiguredClass parent,
AnnotationBindingContext context) {
this.parent = parent;
this.classInfo = classInfo;
this.clazz = context.locateClassByName( classInfo.toString() );
this.configuredClassType = determineType();
this.classAccessType = determineClassAccessType( defaultAccessType );
this.customTuplizer = determineCustomTuplizer();
this.simpleAttributeMap = new TreeMap<String, BasicAttribute>();
this.idAttributeMap = new TreeMap<String, BasicAttribute>();
this.associationAttributeMap = new TreeMap<String, AssociationAttribute>();
this.localBindingContext = new EntityBindingContext( context, this );
collectAttributes();
attributeOverrideMap = Collections.unmodifiableMap( findAttributeOverrides() );
}
public String getName() {
return clazz.getName();
}
public Class<?> getConfiguredClass() {
return clazz;
}
public ClassInfo getClassInfo() {
return classInfo;
}
public ConfiguredClass getParent() {
return parent;
}
public EntityBindingContext getLocalBindingContext() {
return localBindingContext;
}
public Iterable<BasicAttribute> getSimpleAttributes() {
return simpleAttributeMap.values();
}
public Iterable<BasicAttribute> getIdAttributes() {
return idAttributeMap.values();
}
public BasicAttribute getVersionAttribute() {
return versionAttribute;
}
public Iterable<AssociationAttribute> getAssociationAttributes() {
return associationAttributeMap.values();
}
public Map<String, EmbeddableClass> getEmbeddedClasses() {
return embeddedClasses;
}
public Map<String, AttributeOverride> getAttributeOverrideMap() {
return attributeOverrideMap;
}
public AccessType getClassAccessType() {
return classAccessType;
}
public String getCustomTuplizer() {
return customTuplizer;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append( "ConfiguredClass" );
sb.append( "{clazz=" ).append( clazz.getSimpleName() );
sb.append( '}' );
return sb.toString();
}
private ConfiguredClassType determineType() {
if ( classInfo.annotations().containsKey( JPADotNames.ENTITY ) ) {
return ConfiguredClassType.ENTITY;
}
else if ( classInfo.annotations().containsKey( JPADotNames.MAPPED_SUPERCLASS ) ) {
return ConfiguredClassType.MAPPED_SUPERCLASS;
}
else if ( classInfo.annotations().containsKey( JPADotNames.EMBEDDABLE ) ) {
return ConfiguredClassType.EMBEDDABLE;
}
else {
return ConfiguredClassType.NON_ENTITY;
}
}
private AccessType determineClassAccessType(AccessType defaultAccessType) {
// default to the hierarchy access type to start with
AccessType accessType = defaultAccessType;
AnnotationInstance accessAnnotation = JandexHelper.getSingleAnnotation( classInfo, JPADotNames.ACCESS );
if ( accessAnnotation != null && accessAnnotation.target().getClass().equals( ClassInfo.class ) ) {
accessType = JandexHelper.getEnumValue( accessAnnotation, "value", AccessType.class );
}
return accessType;
}
/**
* Find all attributes for this configured class and add them to the corresponding map
*/
private void collectAttributes() {
// find transient field and method names
findTransientFieldAndMethodNames();
// use the class mate library to generic types
ResolvedTypeWithMembers resolvedType = localBindingContext.resolveMemberTypes(
localBindingContext.getResolvedType(
clazz
)
);
for ( HierarchicType hierarchicType : resolvedType.allTypesAndOverrides() ) {
if ( hierarchicType.getType().getErasedType().equals( clazz ) ) {
resolvedType = localBindingContext.resolveMemberTypes( hierarchicType.getType() );
break;
}
}
if ( resolvedType == null ) {
throw new AssertionFailure( "Unable to resolve types for " + clazz.getName() );
}
Set<String> explicitlyConfiguredMemberNames = createExplicitlyConfiguredAccessProperties( resolvedType );
if ( AccessType.FIELD.equals( classAccessType ) ) {
Field fields[] = clazz.getDeclaredFields();
Field.setAccessible( fields, true );
for ( Field field : fields ) {
if ( isPersistentMember( transientFieldNames, explicitlyConfiguredMemberNames, field ) ) {
createMappedAttribute( field, resolvedType, AccessType.FIELD );
}
}
}
else {
Method[] methods = clazz.getDeclaredMethods();
Method.setAccessible( methods, true );
for ( Method method : methods ) {
if ( isPersistentMember( transientMethodNames, explicitlyConfiguredMemberNames, method ) ) {
createMappedAttribute( method, resolvedType, AccessType.PROPERTY );
}
}
}
}
private boolean isPersistentMember(Set<String> transientNames, Set<String> explicitlyConfiguredMemberNames, Member member) {
if ( !ReflectionHelper.isProperty( member ) ) {
return false;
}
if ( transientNames.contains( member.getName() ) ) {
return false;
}
if ( explicitlyConfiguredMemberNames.contains( ReflectionHelper.getPropertyName( member ) ) ) {
return false;
}
return true;
}
/**
* Creates {@code MappedProperty} instances for the explicitly configured persistent properties
*
* @param resolvedMembers the resolved type parameters for this class
*
* @return the property names of the explicitly configured attribute names in a set
*/
private Set<String> createExplicitlyConfiguredAccessProperties(ResolvedTypeWithMembers resolvedMembers) {
Set<String> explicitAccessPropertyNames = new HashSet<String>();
List<AnnotationInstance> accessAnnotations = classInfo.annotations().get( JPADotNames.ACCESS );
if ( accessAnnotations == null ) {
return explicitAccessPropertyNames;
}
// iterate over all @Access annotations defined on the current class
for ( AnnotationInstance accessAnnotation : accessAnnotations ) {
// we are only interested at annotations defined on fields and methods
AnnotationTarget annotationTarget = accessAnnotation.target();
if ( !( annotationTarget.getClass().equals( MethodInfo.class ) || annotationTarget.getClass()
.equals( FieldInfo.class ) ) ) {
continue;
}
AccessType accessType = JandexHelper.getEnumValue( accessAnnotation, "value", AccessType.class );
if ( !isExplicitAttributeAccessAnnotationPlacedCorrectly( annotationTarget, accessType ) ) {
continue;
}
// the placement is correct, get the member
Member member;
if ( annotationTarget instanceof MethodInfo ) {
Method m;
try {
m = clazz.getMethod( ( (MethodInfo) annotationTarget ).name() );
}
catch ( NoSuchMethodException e ) {
throw new HibernateException(
"Unable to load method "
+ ( (MethodInfo) annotationTarget ).name()
+ " of class " + clazz.getName()
);
}
member = m;
accessType = AccessType.PROPERTY;
}
else {
Field f;
try {
f = clazz.getField( ( (FieldInfo) annotationTarget ).name() );
}
catch ( NoSuchFieldException e ) {
throw new HibernateException(
"Unable to load field "
+ ( (FieldInfo) annotationTarget ).name()
+ " of class " + clazz.getName()
);
}
member = f;
accessType = AccessType.FIELD;
}
if ( ReflectionHelper.isProperty( member ) ) {
createMappedAttribute( member, resolvedMembers, accessType );
explicitAccessPropertyNames.add( ReflectionHelper.getPropertyName( member ) );
}
}
return explicitAccessPropertyNames;
}
private boolean isExplicitAttributeAccessAnnotationPlacedCorrectly(AnnotationTarget annotationTarget, AccessType accessType) {
// when the access type of the class is FIELD
// overriding access annotations must be placed on properties AND have the access type PROPERTY
if ( AccessType.FIELD.equals( classAccessType ) ) {
if ( !( annotationTarget instanceof MethodInfo ) ) {
LOG.tracef(
"The access type of class %s is AccessType.FIELD. To override the access for an attribute " +
"@Access has to be placed on the property (getter)", classInfo.name().toString()
);
return false;
}
if ( !AccessType.PROPERTY.equals( accessType ) ) {
LOG.tracef(
"The access type of class %s is AccessType.FIELD. To override the access for an attribute " +
"@Access has to be placed on the property (getter) with an access type of AccessType.PROPERTY. " +
"Using AccessType.FIELD on the property has no effect",
classInfo.name().toString()
);
return false;
}
}
// when the access type of the class is PROPERTY
// overriding access annotations must be placed on fields and have the access type FIELD
if ( AccessType.PROPERTY.equals( classAccessType ) ) {
if ( !( annotationTarget instanceof FieldInfo ) ) {
LOG.tracef(
"The access type of class %s is AccessType.PROPERTY. To override the access for a field " +
"@Access has to be placed on the field ", classInfo.name().toString()
);
return false;
}
if ( !AccessType.FIELD.equals( accessType ) ) {
LOG.tracef(
"The access type of class %s is AccessType.PROPERTY. To override the access for a field " +
"@Access has to be placed on the field with an access type of AccessType.FIELD. " +
"Using AccessType.PROPERTY on the field has no effect",
classInfo.name().toString()
);
return false;
}
}
return true;
}
private void createMappedAttribute(Member member, ResolvedTypeWithMembers resolvedType, AccessType accessType) {
final String attributeName = ReflectionHelper.getPropertyName( member );
ResolvedMember[] resolvedMembers;
if ( member instanceof Field ) {
resolvedMembers = resolvedType.getMemberFields();
}
else {
resolvedMembers = resolvedType.getMemberMethods();
}
Class<?> attributeType = (Class<?>) findResolvedType( member.getName(), resolvedMembers );
final Map<DotName, List<AnnotationInstance>> annotations = JandexHelper.getMemberAnnotations(
classInfo, member.getName()
);
AttributeNature attributeNature = determineAttributeNature( annotations );
String accessTypeString = accessType.toString().toLowerCase();
switch ( attributeNature ) {
case BASIC: {
BasicAttribute attribute = BasicAttribute.createSimpleAttribute(
attributeName, attributeType, annotations, accessTypeString, getLocalBindingContext()
);
if ( attribute.isId() ) {
idAttributeMap.put( attributeName, attribute );
}
else if ( attribute.isVersioned() ) {
if ( versionAttribute == null ) {
versionAttribute = attribute;
}
else {
throw new MappingException( "Multiple version attributes", localBindingContext.getOrigin() );
}
}
else {
simpleAttributeMap.put( attributeName, attribute );
}
break;
}
case ELEMENT_COLLECTION: {
throw new NotYetImplementedException( "Element collections must still be implemented." );
}
case EMBEDDED_ID: {
throw new NotYetImplementedException( "Embedded ids must still be implemented." );
}
case EMBEDDED: {
AnnotationInstance targetAnnotation = JandexHelper.getSingleAnnotation(
getClassInfo(),
HibernateDotNames.TARGET
);
if ( targetAnnotation != null ) {
attributeType = localBindingContext.locateClassByName(
JandexHelper.getValue( targetAnnotation, "value", String.class )
);
}
resolveEmbeddable( attributeName, attributeType );
break;
}
// OneToOne, OneToMany, ManyToOne, ManyToMany
default: {
AssociationAttribute attribute = AssociationAttribute.createAssociationAttribute(
attributeName,
attributeType,
attributeNature,
accessTypeString,
annotations,
getLocalBindingContext()
);
associationAttributeMap.put( attributeName, attribute );
}
}
}
private void resolveEmbeddable(String attributeName, Class<?> type) {
ClassInfo embeddableClassInfo = localBindingContext.getClassInfo( type.getName() );
if ( embeddableClassInfo == null ) {
String msg = String.format(
"Attribute '%s#%s' is annotated with @Embedded, but '%s' does not seem to be annotated " +
"with @Embeddable. Are all annotated classes added to the configuration?",
getConfiguredClass().getSimpleName(),
attributeName,
type.getSimpleName()
);
throw new AnnotationException( msg );
}
localBindingContext.resolveAllTypes( type.getName() );
EmbeddableHierarchy hierarchy = EmbeddableHierarchy.createEmbeddableHierarchy(
localBindingContext.<Object>locateClassByName( embeddableClassInfo.toString() ),
attributeName,
classAccessType,
localBindingContext
);
embeddedClasses.put( attributeName, hierarchy.getLeaf() );
}
/**
* Given the annotations defined on a persistent attribute this methods determines the attribute type.
*
* @param annotations the annotations defined on the persistent attribute
*
* @return an instance of the {@code AttributeType} enum
*/
private AttributeNature determineAttributeNature(Map<DotName, List<AnnotationInstance>> annotations) {
EnumMap<AttributeNature, AnnotationInstance> discoveredAttributeTypes =
new EnumMap<AttributeNature, AnnotationInstance>( AttributeNature.class );
AnnotationInstance oneToOne = JandexHelper.getSingleAnnotation( annotations, JPADotNames.ONE_TO_ONE );
if ( oneToOne != null ) {
discoveredAttributeTypes.put( AttributeNature.ONE_TO_ONE, oneToOne );
}
AnnotationInstance oneToMany = JandexHelper.getSingleAnnotation( annotations, JPADotNames.ONE_TO_MANY );
if ( oneToMany != null ) {
discoveredAttributeTypes.put( AttributeNature.ONE_TO_MANY, oneToMany );
}
AnnotationInstance manyToOne = JandexHelper.getSingleAnnotation( annotations, JPADotNames.MANY_TO_ONE );
if ( manyToOne != null ) {
discoveredAttributeTypes.put( AttributeNature.MANY_TO_ONE, manyToOne );
}
AnnotationInstance manyToMany = JandexHelper.getSingleAnnotation( annotations, JPADotNames.MANY_TO_MANY );
if ( manyToMany != null ) {
discoveredAttributeTypes.put( AttributeNature.MANY_TO_MANY, manyToMany );
}
AnnotationInstance embedded = JandexHelper.getSingleAnnotation( annotations, JPADotNames.EMBEDDED );
if ( embedded != null ) {
discoveredAttributeTypes.put( AttributeNature.EMBEDDED, embedded );
}
AnnotationInstance embeddedId = JandexHelper.getSingleAnnotation( annotations, JPADotNames.EMBEDDED_ID );
if ( embeddedId != null ) {
discoveredAttributeTypes.put( AttributeNature.EMBEDDED_ID, embeddedId );
}
AnnotationInstance elementCollection = JandexHelper.getSingleAnnotation(
annotations,
JPADotNames.ELEMENT_COLLECTION
);
if ( elementCollection != null ) {
discoveredAttributeTypes.put( AttributeNature.ELEMENT_COLLECTION, elementCollection );
}
if ( discoveredAttributeTypes.size() == 0 ) {
return AttributeNature.BASIC;
}
else if ( discoveredAttributeTypes.size() == 1 ) {
return discoveredAttributeTypes.keySet().iterator().next();
}
else {
throw new AnnotationException( "More than one association type configured for property " + getName() + " of class " + getName() );
}
}
private Type findResolvedType(String name, ResolvedMember[] resolvedMembers) {
for ( ResolvedMember resolvedMember : resolvedMembers ) {
if ( resolvedMember.getName().equals( name ) ) {
return resolvedMember.getType().getErasedType();
}
}
throw new AssertionFailure(
String.format(
"Unable to resolve type of attribute %s of class %s",
name,
classInfo.name().toString()
)
);
}
/**
* Populates the sets of transient field and method names.
*/
private void findTransientFieldAndMethodNames() {
List<AnnotationInstance> transientMembers = classInfo.annotations().get( JPADotNames.TRANSIENT );
if ( transientMembers == null ) {
return;
}
for ( AnnotationInstance transientMember : transientMembers ) {
AnnotationTarget target = transientMember.target();
if ( target instanceof FieldInfo ) {
transientFieldNames.add( ( (FieldInfo) target ).name() );
}
else {
transientMethodNames.add( ( (MethodInfo) target ).name() );
}
}
}
private Map<String, AttributeOverride> findAttributeOverrides() {
Map<String, AttributeOverride> attributeOverrideList = new HashMap<String, AttributeOverride>();
AnnotationInstance attributeOverrideAnnotation = JandexHelper.getSingleAnnotation(
classInfo,
JPADotNames.ATTRIBUTE_OVERRIDE
);
if ( attributeOverrideAnnotation != null ) {
String prefix = createPathPrefix( attributeOverrideAnnotation.target() );
AttributeOverride override = new AttributeOverride( prefix, attributeOverrideAnnotation );
attributeOverrideList.put( override.getAttributePath(), override );
}
AnnotationInstance attributeOverridesAnnotation = JandexHelper.getSingleAnnotation(
classInfo,
JPADotNames.ATTRIBUTE_OVERRIDES
);
if ( attributeOverridesAnnotation != null ) {
AnnotationInstance[] annotationInstances = attributeOverridesAnnotation.value().asNestedArray();
for ( AnnotationInstance annotationInstance : annotationInstances ) {
String prefix = createPathPrefix( attributeOverridesAnnotation.target() );
AttributeOverride override = new AttributeOverride( prefix, annotationInstance );
attributeOverrideList.put( override.getAttributePath(), override );
}
}
return attributeOverrideList;
}
private String createPathPrefix(AnnotationTarget target) {
String prefix = null;
if ( target instanceof FieldInfo || target instanceof MethodInfo ) {
prefix = JandexHelper.getPropertyName( target );
}
return prefix;
}
private List<AnnotationInstance> findAssociationOverrides() {
List<AnnotationInstance> associationOverrideList = new ArrayList<AnnotationInstance>();
AnnotationInstance associationOverrideAnnotation = JandexHelper.getSingleAnnotation(
classInfo,
JPADotNames.ASSOCIATION_OVERRIDE
);
if ( associationOverrideAnnotation != null ) {
associationOverrideList.add( associationOverrideAnnotation );
}
AnnotationInstance associationOverridesAnnotation = JandexHelper.getSingleAnnotation(
classInfo,
JPADotNames.ASSOCIATION_OVERRIDES
);
if ( associationOverrideAnnotation != null ) {
AnnotationInstance[] attributeOverride = associationOverridesAnnotation.value().asNestedArray();
Collections.addAll( associationOverrideList, attributeOverride );
}
return associationOverrideList;
}
private String determineCustomTuplizer() {
final AnnotationInstance tuplizersAnnotation = JandexHelper.getSingleAnnotation(
classInfo, HibernateDotNames.TUPLIZERS
);
if ( tuplizersAnnotation == null ) {
return null;
}
AnnotationInstance[] annotations = JandexHelper.getValue(
tuplizersAnnotation,
"value",
AnnotationInstance[].class
);
AnnotationInstance pojoTuplizerAnnotation = null;
for ( AnnotationInstance tuplizerAnnotation : annotations ) {
if ( EntityMode.valueOf( tuplizerAnnotation.value( "entityModeType" ).asEnum() ) == EntityMode.POJO ) {
pojoTuplizerAnnotation = tuplizerAnnotation;
break;
}
}
String customTuplizer = null;
if ( pojoTuplizerAnnotation != null ) {
customTuplizer = pojoTuplizerAnnotation.value( "impl" ).asString();
}
return customTuplizer;
}
}