// $HeadURL$
// $Id$
//
// Copyright © 2006, 2010, 2011, 2012 by the President and Fellows of Harvard College.
//
// Screensaver is an open-source project developed by the ICCB-L and NSRB labs
// at Harvard Medical School. This software is distributed under the terms of
// the GNU General Public License.
package edu.harvard.med.screensaver.test.model.meta;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Map;
import javax.persistence.Embeddable;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Transient;
import org.apache.log4j.Logger;
import org.hibernate.annotations.Immutable;
import edu.harvard.med.screensaver.model.AbstractEntity;
import edu.harvard.med.screensaver.model.SemanticIDAbstractEntity;
import edu.harvard.med.screensaver.model.annotations.Column;
import edu.harvard.med.screensaver.model.annotations.ContainedEntity;
import edu.harvard.med.screensaver.model.annotations.Derived;
import edu.harvard.med.screensaver.model.annotations.ElementCollection;
import edu.harvard.med.screensaver.model.annotations.IgnoreImmutabilityTest;
import edu.harvard.med.screensaver.model.annotations.ToMany;
import edu.harvard.med.screensaver.model.annotations.ToOne;
import edu.harvard.med.screensaver.util.DevelopmentException;
import edu.harvard.med.screensaver.util.StringUtils;
public class ModelIntrospectionUtil
{
private static Logger log = Logger.getLogger(ModelIntrospectionUtil.class);
/**
* Get the getter method for a property based on the property name.
*
* @param propertyName the property name
* @param entityClass
* @return the getter method
*/
public static Method getGetterMethodForPropertyName(Class entityClass, String propertyName)
{
String getterName = "get" + StringUtils.capitalize(propertyName);
try {
return entityClass.getDeclaredMethod(getterName);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Get the setter method for a property based on the property name and the property type.
* @param propertyName the property name
* @param propertyType the property type
* @return the setter method
*/
public static Method getSetterMethodForPropertyName(Class entityClass, String propertyName, Class propertyType)
{
String setterName = "set" + StringUtils.capitalize(propertyName);
try {
return entityClass.getDeclaredMethod(setterName, propertyType);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Returns true iff the property corresponds to the entity's ID. Such methods
* include: getEntityId(), getFooId() (for _bean of type Foo), and any
* properties that may be used to define the ID (for cases where the entity ID
* is not an auto-generated database ID, but instead correspdonds to the
* entity's business key).
*
* @param propertyDescriptor the property
* @return true iff property is "entityId" or the property that is named the
* same as the entity, but with an "Id" suffix; otherwise false
*/
@SuppressWarnings("unchecked")
public static boolean isEntityIdProperty(Class<? extends AbstractEntity> beanClass,
PropertyDescriptor propertyDescriptor)
{
// legacy logic for finding the standard entity-ID related methods below...
if (propertyDescriptor.getName().equals("entityId")) {
log.debug("isEntityIdProperty(): property participates in defining entity ID: " + propertyDescriptor.getName());
return true;
}
// Check whether property corresponds to the _bean's Hibernate ID method, which is named similarly to the _bean.
// We also check the parent classes, to handle the case where the property
// has been inherited, as the property name will depend upon the class it
// was declared in.
String capitalizedPropertyName = propertyDescriptor.getName().substring(0, 1).toUpperCase() + propertyDescriptor.getName().substring(1);
while (!AbstractEntity.class.equals(beanClass)) {
if (capitalizedPropertyName.endsWith(beanClass.getSimpleName() + "Id")) {
log.debug("isEntityIdProperty(): property participates in defining entity ID: " + propertyDescriptor.getName() +
" in " + beanClass.getSimpleName());
return true;
}
beanClass = (Class<? extends AbstractEntity>) beanClass.getSuperclass();
}
return false;
}
public static Class<?> getCollectionElementType(PropertyDescriptor propertyDescriptor)
{
assert isCollectionBasedProperty(propertyDescriptor);
Type[] actualTypeArguments = ((ParameterizedType) propertyDescriptor.getReadMethod().getGenericReturnType()).getActualTypeArguments();
return (Class<?>) actualTypeArguments[0];
}
public static Class<?> getMapKeyType(PropertyDescriptor propertyDescriptor)
{
assert isMapBasedProperty(propertyDescriptor);
Type[] actualTypeArguments = ((ParameterizedType) propertyDescriptor.getReadMethod().getGenericReturnType()).getActualTypeArguments();
return (Class<?>) actualTypeArguments[0];
}
public static Class<?> getMapValueType(PropertyDescriptor propertyDescriptor)
{
assert isMapBasedProperty(propertyDescriptor);
Type[] actualTypeArguments = ((ParameterizedType) propertyDescriptor.getReadMethod().getGenericReturnType()).getActualTypeArguments();
return (Class<?>) actualTypeArguments[1];
}
/**
* Determine whether this property represents a *-to-1 relationship, based
* upon the return type of the property (and not based upon annotations)
*/
public static boolean isToOneEntityRelationship(PropertyDescriptor propertyDescriptor)
{
return AbstractEntity.class.isAssignableFrom(propertyDescriptor.getPropertyType());
}
/**
* Determine whether this property represents a *-to-N relationship, based
* upon the return type of the property (and not based upon annotations)
*/
public static boolean isToManyEntityRelationship(PropertyDescriptor propertyDescriptor)
{
if (isCollectionBasedProperty(propertyDescriptor)) {
return AbstractEntity.class.isAssignableFrom(getCollectionElementType(propertyDescriptor));
}
else if (isMapBasedProperty(propertyDescriptor)) {
return AbstractEntity.class.isAssignableFrom(getMapValueType(propertyDescriptor));
}
return false;
}
public static boolean isUnidirectionalRelationship(Class<? extends AbstractEntity> beanClass,
PropertyDescriptor propertyDescriptor)
{
try {
return isUnidirectionalRelationshipMethod(propertyDescriptor.getReadMethod());
}
catch (SecurityException e) {
throw e;
}
}
public static boolean isSetterMethodNotExpected(Class<? extends AbstractEntity> beanClass, PropertyDescriptor propertyDescriptor)
{
String propFullName = beanClass.getSimpleName() + "." + propertyDescriptor.getName();
if (isTransientProperty(propertyDescriptor)) {
log.info("setter method not expected for transient property: " + propFullName);
return true;
}
if (isPropertyWithNonconventionalSetterMethod(propertyDescriptor)) {
log.info("setter method not expected for transient property: " + propFullName);
return true;
}
if (isImmutableProperty(beanClass, propertyDescriptor)) {
log.info("setter method not expected for immutable property: " + propFullName);
return true;
}
if (isToOneRelationshipRequired(propertyDescriptor)) {
return true;
}
// no setter expected if property participates in defining the entity ID
if (isEntityIdProperty(beanClass, propertyDescriptor)) {
log.info("setter method not expected for property that participates in defining the entity ID: " +
propFullName);
return true;
}
if (isEmbeddableProperty(beanClass, propertyDescriptor)) {
return true;
}
return false;
}
public static boolean isToOneRelationshipRequired(PropertyDescriptor propertyDescriptor)
{
if (isToOneEntityRelationship(propertyDescriptor)) {
return isNonNullableProperty(propertyDescriptor);
}
return false;
}
/**
* @return true if the many-side of this one-to-many relationship is required (non-nullable), so that the related entity must always have beanClass as (one of) its parents
*/
public static boolean isOneToManyRelationshipRequired(Class<? extends AbstractEntity> beanClass,
PropertyDescriptor propertyDescriptor)
{
if (isToManyEntityRelationship(propertyDescriptor)) {
// ignore many-to-many, which can never be required, containment relationship
if (hasAnnotation(OneToMany.class, propertyDescriptor)) {
RelatedProperty relatedProperty = new RelatedProperty(beanClass, propertyDescriptor);
PropertyDescriptor relatedPropertyDescriptor = relatedProperty.getPropertyDescriptor();
return isToOneRelationshipRequired(relatedPropertyDescriptor);
}
}
return false;
}
public static boolean isNonNullableProperty(PropertyDescriptor propertyDescriptor)
{
Method getter = propertyDescriptor.getReadMethod();
javax.persistence.Column jpaColumn = getter.getAnnotation(javax.persistence.Column.class);
JoinColumn jpaJoinColumn = getter.getAnnotation(JoinColumn.class);
ManyToOne manyToOne = getter.getAnnotation(ManyToOne.class);
OneToOne oneToOne = getter.getAnnotation(OneToOne.class);
return (jpaColumn != null && !jpaColumn.nullable()) ||
(jpaJoinColumn != null && !jpaJoinColumn.nullable()) ||
(manyToOne != null && !manyToOne.optional()) ||
(oneToOne != null && !oneToOne.optional());
}
public static boolean isTransientProperty(PropertyDescriptor propertyDescriptor)
{
return hasAnnotation(Transient.class, propertyDescriptor);
}
public static boolean isImmutableIgnoreTests(Class<? extends AbstractEntity> beanClass)
{
return beanClass.getAnnotation(Immutable.class) != null && beanClass.getAnnotation(IgnoreImmutabilityTest.class) != null;
}
public static boolean isImmutableProperty(Class<? extends AbstractEntity> beanClass, PropertyDescriptor propertyDescriptor)
{
org.hibernate.annotations.Entity entityAnnotation =
beanClass.getAnnotation(org.hibernate.annotations.Entity.class);
if (entityAnnotation != null && ! entityAnnotation.mutable()) {
return true;
}
if (beanClass.getAnnotation(Immutable.class) != null) {
return true;
}
Method getter = propertyDescriptor.getReadMethod();
javax.persistence.Column columnAnnot = getter.getAnnotation(javax.persistence.Column.class);
return columnAnnot != null && !columnAnnot.updatable();
}
public static boolean isEmbeddableProperty(Class<? extends AbstractEntity> beanClass, PropertyDescriptor propertyDescriptor)
{
Class propertyType = (Class) propertyDescriptor.getPropertyType();
if (!AbstractEntity.class.isAssignableFrom(propertyType)) {
Embeddable embeddable =
(Embeddable) propertyType.getAnnotation(Embeddable.class);
return embeddable != null;
}
return false;
}
/**
* Determine whether this property represents a collection-based property, based
* upon the return type of the property (and not based upon annotations). Note
* that a collection property may either represent an entity relationship (
* {@link #isToManyEntityRelationship(PropertyDescriptor)} or
* {@link #isToOneEntityRelationship(PropertyDescriptor)}) or a collection of
* elements ({@link #isCollectionOfElements(PropertyDescriptor)}.
*/
public static boolean isCollectionBasedProperty(PropertyDescriptor propertyDescriptor)
{
Class<?> propertyType = propertyDescriptor.getPropertyType();
return Collection.class.isAssignableFrom(propertyType);
}
/**
* Determine whether this property represents a map-based property, based
* upon the return type of the property (and not based upon annotations). Note
* that a collection property may either represent an entity relationship (
* {@link #isToManyEntityRelationship(PropertyDescriptor)} or
* {@link #isToOneEntityRelationship(PropertyDescriptor)}) or a collection of
* elements ({@link #isCollectionOfElements(PropertyDescriptor)}.
*/
public static boolean isMapBasedProperty(PropertyDescriptor propertyDescriptor)
{
Class<?> propertyType = propertyDescriptor.getPropertyType();
return Map.class.isAssignableFrom(propertyType);
}
public static boolean isCollectionOrMapBasedProperty(PropertyDescriptor propertyDescriptor)
{
return isCollectionBasedProperty(propertyDescriptor) || isMapBasedProperty(propertyDescriptor);
}
/**
* Determine whether this property represents a collection-of-elements, based
* upon the return type of the property (and not based upon annotations),
* which must be a collection- or map-based property with element/value type
* that is not AbstractEntity (in which case it would be an entity
* relationship property: {@link #isToManyEntityRelationship(PropertyDescriptor)} or
* {@link #isToOneEntityRelationship(PropertyDescriptor)}).
*/
public static boolean isCollectionOfElements(PropertyDescriptor propertyDescriptor)
{
return (isCollectionBasedProperty(propertyDescriptor) && !AbstractEntity.class.isAssignableFrom(getCollectionElementType(propertyDescriptor))) ||
(isMapBasedProperty(propertyDescriptor) && !AbstractEntity.class.isAssignableFrom(getMapValueType(propertyDescriptor)));
}
public static Class<?> getCollectionOfElementsType(PropertyDescriptor propertyDescriptor)
{
assert isCollectionOfElements(propertyDescriptor);
if (isCollectionBasedProperty(propertyDescriptor)) {
return getCollectionElementType(propertyDescriptor);
}
else if (isMapBasedProperty(propertyDescriptor)) {
return getMapValueType(propertyDescriptor);
}
throw new DomainModelDefinitionException("not sure how to find the element type for: " + propertyDescriptor.getDisplayName());
}
public static Class<? extends AbstractEntity> getRelationshipEntityType(PropertyDescriptor propertyDescriptor)
{
Class<? extends AbstractEntity> relatedEntityType = null;
if (isToManyEntityRelationship(propertyDescriptor)) {
if (isCollectionBasedProperty(propertyDescriptor)) {
relatedEntityType = (Class<? extends AbstractEntity>) getCollectionElementType(propertyDescriptor);
}
else if (isMapBasedProperty(propertyDescriptor)) {
relatedEntityType = (Class<? extends AbstractEntity>) getMapValueType(propertyDescriptor);
}
}
else if (isToOneEntityRelationship(propertyDescriptor)) {
relatedEntityType = (Class<? extends AbstractEntity>) propertyDescriptor.getPropertyType();
}
if (relatedEntityType == null) {
throw new DomainModelDefinitionException("not sure how to find the entity type for: " + propertyDescriptor.getDisplayName());
}
return relatedEntityType;
}
public static boolean hasAnnotation(Class<? extends Annotation> annotationClass, PropertyDescriptor propertyDescriptor)
{
Method getter = propertyDescriptor.getReadMethod();
return getter.isAnnotationPresent(annotationClass);
}
public static boolean isUnidirectionalRelationshipMethod(Method getter)
{
ToOne toOne = getter.getAnnotation(ToOne.class);
if (toOne != null && toOne.unidirectional()) {
return true;
}
return false;
}
/**
* Return true iff this entity class is a subclass of another entity class.
* @return true iff this entity class is a subclass of another entity class
*/
public static boolean isEntitySubclass(Class entityClass)
{
Class entitySuperclass = entityClass.getSuperclass();
return ! (
entitySuperclass.equals(AbstractEntity.class) ||
entitySuperclass.equals(SemanticIDAbstractEntity.class));
}
public static boolean isPropertyWithNonconventionalSetterMethod(PropertyDescriptor propertyDescriptor)
{
Method getter = propertyDescriptor.getReadMethod();
Column column = getter.getAnnotation(Column.class);
return column != null && column.hasNonconventionalSetterMethod();
}
public static boolean isCollectionWithNonConventionalMutation(PropertyDescriptor propertyDescriptor)
{
Method getter = propertyDescriptor.getReadMethod();
ElementCollection elementCollection = getter.getAnnotation(ElementCollection.class);
return elementCollection != null && elementCollection.hasNonconventionalMutation();
}
public static boolean isToOneRelationshipWithNonConventionalSetter(PropertyDescriptor propertyDescriptor)
{
Method getter = propertyDescriptor.getReadMethod();
ToOne toOne = getter.getAnnotation(ToOne.class);
return toOne != null && toOne.hasNonconventionalSetterMethod();
}
public static boolean isToManyRelationshipWithNonConventionalMutation(PropertyDescriptor propertyDescriptor)
{
Method getter = propertyDescriptor.getReadMethod();
ToMany toMany = getter.getAnnotation(ToMany.class);
return toMany != null && toMany.hasNonconventionalMutation();
}
/**
* Check whether a method with the given name adheres to its existence requirement.
*
* @return the method, if it exists and is allowed to exist; null if method does not exist and its existence is not
* required
* @throws DevelopmentException if method existence is required and no such method exists OR if method existence is
* not
* allowed and the method exists
*/
public static Method findAndCheckMethod(Class<? extends AbstractEntity> beanClass,
String methodName,
ExistenceRequirement requirement)
{
return findAndCheckMethod(beanClass, methodName, requirement, (Class<?>[]) null);
}
/**
* Check whether a method with the given name and specified parameter types adheres to its existence requirement.
*
* @return the method, if it exists and is allowed to exist; null if method does not exist and its existence is not
* required
* @throws DevelopmentException if method existence is required and no such method exists OR if method existence is
* not
* allowed and the method exists
*/
public static Method findAndCheckMethod(Class<? extends AbstractEntity> beanClass,
String methodName,
ExistenceRequirement requirement,
Class<?>... paramTypes)
{
String fullMethodName = beanClass.getName() + "." + methodName;
Method foundMethod = null;
if (paramTypes != null) {
// match a specific parameter list, if provided
try {
foundMethod = beanClass.getMethod(methodName, paramTypes);
}
catch (NoSuchMethodException e) {}
}
else if (foundMethod == null) {
// perform a name-based search that ignores the parameter list
// note: we're calling getMethods() instead of getDeclaredMethods() to allow
// inherited methods to satisfy our isRequiredMethod constraint
// TODO: getMethods() will only return public methods, is this okay?
for (Method method : beanClass.getMethods()) {
if (method.getName().equals(methodName)) {
foundMethod = method;
break;
}
}
}
if (requirement != ExistenceRequirement.REQUIRED && foundMethod == null) {
log.debug("findAndCheckMethod(): non-required method was not found: " + fullMethodName);
return null;
}
if (!(requirement != ExistenceRequirement.NOT_ALLOWED || foundMethod == null)) {
throw new DevelopmentException("method not allowed: " + fullMethodName);
}
if (!(requirement != ExistenceRequirement.REQUIRED || foundMethod != null)) {
throw new DevelopmentException("method must exist: " + fullMethodName);
}
return foundMethod;
}
public static boolean isDerivedProperty(PropertyDescriptor propertyDescriptor)
{
return hasAnnotation(Derived.class, propertyDescriptor);
}
public static Class<? extends AbstractEntity> getParent(Class<? extends AbstractEntity> beanClass)
{
ContainedEntity containedEntity = beanClass.getAnnotation(ContainedEntity.class);
if (containedEntity != null) {
return containedEntity.containingEntityClass();
}
return null;
}
public static boolean isAutoCreatedByParent(Class<? extends AbstractEntity> beanClass)
{
ContainedEntity containedEntity = beanClass.getAnnotation(ContainedEntity.class);
if (containedEntity != null) {
return containedEntity.autoCreated();
}
return false;
}
// public static boolean isContainmentRelationship(Class<? extends AbstractEntity> beanClass,
// Class<? extends AbstractEntity> relatedEntityType)
// {
// ContainedEntity containedEntity = relatedEntityType.getAnnotation(ContainedEntity.class);
// if (containedEntity != null && containedEntity.containingEntityClass().isAssignableFrom(beanClass)) {
// return true;
// }
// return false;
// }
}