All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.hibernate.metamodel.source.annotations.entity.ConfiguredClass Maven / Gradle / Ivy

There is a newer version: 7.0.0.Alpha1
Show newest version
/*
 * 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 idAttributeMap;

	/**
	 * The mapped association attributes for this entity
	 */
	private final Map associationAttributeMap;

	/**
	 * The mapped simple attributes for this entity
	 */
	private final Map simpleAttributeMap;

	/**
	 * The version attribute or {@code null} in case none exists.
	 */
	private BasicAttribute versionAttribute;

	/**
	 * The embedded classes for this entity
	 */
	private final Map embeddedClasses = new HashMap();

	/**
	 * 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 attributeOverrideMap;

	private final Set transientFieldNames = new HashSet();
	private final Set transientMethodNames = new HashSet();

	/**
	 * 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();
		this.idAttributeMap = new TreeMap();
		this.associationAttributeMap = new TreeMap();

		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 getSimpleAttributes() {
		return simpleAttributeMap.values();
	}

	public Iterable getIdAttributes() {
		return idAttributeMap.values();
	}

	public BasicAttribute getVersionAttribute() {
		return versionAttribute;
	}

	public Iterable getAssociationAttributes() {
		return associationAttributeMap.values();
	}

	public Map getEmbeddedClasses() {
		return embeddedClasses;
	}

	public Map 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 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 transientNames, Set 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 createExplicitlyConfiguredAccessProperties(ResolvedTypeWithMembers resolvedMembers) {
		Set explicitAccessPropertyNames = new HashSet();

		List 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> 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.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> annotations) {
		EnumMap discoveredAttributeTypes =
				new EnumMap( 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 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 findAttributeOverrides() {
		Map attributeOverrideList = new HashMap();

		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 findAssociationOverrides() {
		List associationOverrideList = new ArrayList();

		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;
	}
}