package org.tynamo.model.jpa.services;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.EntityManager;
import javax.persistence.GeneratedValue;
import javax.persistence.IdClass;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.PersistenceException;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EmbeddableType;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.ManagedType;
import javax.persistence.metamodel.Metamodel;
import javax.persistence.metamodel.PluralAttribute;
import javax.persistence.metamodel.SingularAttribute;
import org.apache.tapestry5.ioc.annotations.Autobuild;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.slf4j.Logger;
import org.tynamo.descriptor.CollectionDescriptor;
import org.tynamo.descriptor.EmbeddedDescriptor;
import org.tynamo.descriptor.IdentifierDescriptor;
import org.tynamo.descriptor.IdentifierDescriptorImpl;
import org.tynamo.descriptor.ObjectReferenceDescriptor;
import org.tynamo.descriptor.TynamoClassDescriptor;
import org.tynamo.descriptor.TynamoPropertyDescriptor;
import org.tynamo.descriptor.decorators.DescriptorDecorator;
import org.tynamo.descriptor.factories.DescriptorFactory;
import org.tynamo.exception.TynamoRuntimeException;
import org.tynamo.model.exception.MetadataNotFoundException;
import org.tynamo.model.jpa.TynamoJpaSymbols;
import org.tynamo.model.jpa.internal.ConfigurableEntityManagerProvider;
/**
* This decorator will add metadata information. It will replace simple
* reflection based TynamoPropertyTynamoPropertyDescriptors with appropriate
* Hibernate descriptors <p/> Background... TynamoDescriptorService operates one
* ReflectorDescriptorFactory - TynamoDescriptorService iterates/scans all class
* types encountered - ReflectorDescriptorFactory allocates property descriptor
* instance for the class type - TynamoDescriptorService decorates property
* descriptor by calling this module JPADescriptorDecorator -
* JPADescriptorDecorator caches the decorated property descriptor into a
* decorated descriptor list - decorated descriptor list gets populated into
* class descriptor for class type - TynamoDescriptorService finally populates
* decorated class descriptor and it's aggregated list of decorated property
* descriptors into it's own list/cache of referenced class descriptors
*
* @see TynamoPropertyDescriptor
* @see ObjectReferenceDescriptor
* @see CollectionDescriptor
* @see EmbeddedDescriptor
*/
public class JpaDescriptorDecorator implements DescriptorDecorator
{
private Logger logger;
private DescriptorFactory descriptorFactory;
/** Columns longer than this will have their large property set to true. */
private final int largeColumnLength;
private final boolean ignoreNonEntityTypes;
private EntityManager entityManager;
public JpaDescriptorDecorator(
DescriptorFactory descriptorFactory, ConfigurableEntityManagerProvider entityManagerProvider,
@Symbol(TynamoJpaSymbols.LARGE_COLUMN_LENGTH)
int largeColumnLength,
@Symbol(TynamoJpaSymbols.IGNORE_NON_JPA_TYPES)
boolean ignoreNonEntityTypes,
Logger logger) {
entityManager = entityManagerProvider.getEntityManager();
this.descriptorFactory = descriptorFactory;
this.largeColumnLength = largeColumnLength;
this.ignoreNonEntityTypes = ignoreNonEntityTypes;
this.logger = logger;
}
public TynamoClassDescriptor decorate(TynamoClassDescriptor descriptor) {
ArrayList<TynamoPropertyDescriptor> decoratedPropertyDescriptors = new ArrayList<TynamoPropertyDescriptor>();
Class type = descriptor.getBeanType();
Metamodel metamodel = null;
try {
metamodel = findMetadata(type);
} catch (MetadataNotFoundException e) {
if (ignoreNonEntityTypes) {
logger.warn("MetadataNotFound! Ignoring:" + descriptor.getBeanType().toString());
return descriptor;
} else {
throw new TynamoRuntimeException(e);
}
}
Set<String> idProperties = getIdentifierProperties(type);
for (TynamoPropertyDescriptor propertyDescriptor : descriptor.getPropertyDescriptors()) {
try {
TynamoPropertyDescriptor descriptorReference;
if (idProperties != null && idProperties.contains(propertyDescriptor.getName())) {
// FIXME should we mark an identifier descriptor as part of a composite key?
descriptorReference = createIdentifierDescriptor(type, propertyDescriptor, descriptor);
} else if (notJpaProperty(metamodel, type, propertyDescriptor)) {
// If this is not a jpa property (i.e. marked Transient), it's certainly not searchable
// Are there any other properties like this?
propertyDescriptor.setSearchable(false);
descriptorReference = propertyDescriptor;
} else {
//Attribute mappingProperty = getMapping(type).getAttribute(propertyDescriptor.getName());
descriptorReference = decoratePropertyDescriptor(type, metamodel, propertyDescriptor,
descriptor);
}
decoratedPropertyDescriptors.add(descriptorReference);
} catch (PersistenceException e) {
throw new TynamoRuntimeException(e);
}
}
descriptor.setPropertyDescriptors(decoratedPropertyDescriptors);
descriptor.setSearchable(true);
return descriptor;
}
protected TynamoPropertyDescriptor decoratePropertyDescriptor(Class type, Metamodel metamodel,
TynamoPropertyDescriptor propertyDescriptor, TynamoClassDescriptor parentClassDescriptor) {
ManagedType managedType = metamodel.managedType(parentClassDescriptor.getBeanType());
Attribute attribute;
try {
attribute = managedType.getAttribute(propertyDescriptor.getName());
} catch (IllegalArgumentException ex) { // Attribute not available - read only!
propertyDescriptor.setReadOnly(true);
return propertyDescriptor;
}
TynamoPropertyDescriptor descriptorReference = propertyDescriptor;
if (attribute.isCollection()) {
if (attribute instanceof PluralAttribute) {
PluralAttribute pluralAttribute = (PluralAttribute) attribute;
descriptorReference = decorateCollectionDescriptor(pluralAttribute, propertyDescriptor, parentClassDescriptor);
}
} else if (!attribute.isCollection()) {
switch (attribute.getPersistentAttributeType()) {
case EMBEDDED:
descriptorReference = buildEmbeddedDescriptor(type, metamodel, propertyDescriptor, parentClassDescriptor);
break;
case BASIC:
SingularAttribute singularAttribute = managedType.getSingularAttribute(attribute.getName());
descriptorReference.setLength(findColumnLength(singularAttribute));
descriptorReference.setLarge(isLarge(singularAttribute));
if (!singularAttribute.isOptional()) {
descriptorReference.setRequired(true);
}
if (isNotInsertable(singularAttribute) && isNotUpdateable(singularAttribute)) {
descriptorReference.setReadOnly(true);
}
break;
case ELEMENT_COLLECTION:
break;
case ONE_TO_ONE:
descriptorReference = decorateAssociationDescriptor(attribute, metamodel, propertyDescriptor,
parentClassDescriptor);
break;
case MANY_TO_ONE:
descriptorReference = decorateAssociationDescriptor(attribute, metamodel, propertyDescriptor,
parentClassDescriptor);
break;
}
}
return descriptorReference;
/* TODO: enum handling?
} else if (entityType.getJavaType().isEnum()) {
propertyDescriptor.addExtension(EnumReferenceDescriptor.class.getName(), new EnumReferenceDescriptor(entityType.getJavaType()));
}*/
}
private CollectionDescriptor decorateCollectionDescriptor(PluralAttribute pluralAttribute, TynamoPropertyDescriptor propertyDescriptor, TynamoClassDescriptor parentClassDescriptor) {
CollectionDescriptor collectionDescriptor = new CollectionDescriptor(pluralAttribute.getJavaType(), propertyDescriptor);
collectionDescriptor.setElementType(pluralAttribute.getElementType().getJavaType());
switch (pluralAttribute.getPersistentAttributeType()) {
case ONE_TO_MANY: {
collectionDescriptor.setOneToMany(true);
Annotation a = getAnnotation(pluralAttribute.getJavaMember(), OneToMany.class);
if (a != null) {
OneToMany aOneToMany = (OneToMany) a;
collectionDescriptor.setChildRelationship(aOneToMany.orphanRemoval());
String mappedBy = aOneToMany.mappedBy();
if (!"".equals(mappedBy)) {
collectionDescriptor.setInverseProperty(mappedBy);
parentClassDescriptor.setHasCyclicRelationships(true);
}
}
break;
}
case MANY_TO_MANY:
break;
}
return collectionDescriptor;
}
private boolean isNotInsertable(SingularAttribute singularAttribute) {
Annotation a = singularAttribute.getType().getJavaType().getAnnotation(Column.class);
if (a != null) {
Column column = (Column) a;
return !column.insertable();
} else {
return false;
}
}
private boolean isNotUpdateable(SingularAttribute singularAttribute) {
Annotation a = singularAttribute.getType().getJavaType().getAnnotation(Column.class);
if (a != null) {
Column column = (Column) a;
return !column.updatable();
} else {
return false;
}
}
private EmbeddedDescriptor buildEmbeddedDescriptor(Class type, Metamodel metamodel,
TynamoPropertyDescriptor descriptor, TynamoClassDescriptor parentClassDescriptor) {
//EmbeddableType componentMapping = (EmbeddableType) metamodel.getValue();
EmbeddableType componentMapping = metamodel.embeddable(descriptor.getPropertyType());
TynamoClassDescriptor embeddedClassDescriptor = descriptorFactory.buildClassDescriptor(descriptor.getPropertyType());
ArrayList<TynamoPropertyDescriptor> decoratedProperties = new ArrayList<TynamoPropertyDescriptor>();
// go thru each property and decorate it with JPA info
for (TynamoPropertyDescriptor propertyDescriptor : embeddedClassDescriptor.getPropertyDescriptors()) {
if (notAHibernateProperty(componentMapping, propertyDescriptor)) {
decoratedProperties.add(propertyDescriptor);
} else {
//Attribute property = componentMapping.getAttribute(propertyDescriptor.getName());
TynamoPropertyDescriptor TynamoPropertyDescriptor = decoratePropertyDescriptor(embeddedClassDescriptor.getBeanType(),
metamodel, propertyDescriptor, parentClassDescriptor);
decoratedProperties.add(TynamoPropertyDescriptor);
}
}
embeddedClassDescriptor.setPropertyDescriptors(decoratedProperties);
return new EmbeddedDescriptor(type, descriptor, embeddedClassDescriptor);
}
/**
* The default way to order our property descriptors is by the order they
* appear in the jpa config, with id first. Any non-mapped properties
* are tacked on at the end, til I think of a better way.
*
* @param propertyDescriptors
* @return
*/
// protected List sortPropertyDescriptors(Class type, List propertyDescriptors) {
// ArrayList sortedPropertyDescriptors = new ArrayList();
//
// try {
// sortedPropertyDescriptors.add(Ognl.getValue("#this.{? identifier == true}[0]", propertyDescriptors));
// for (Object obj : findMetadata(type).managedType(type).getAttributes()) {
// Attribute mapping = (Attribute) obj;
// sortedPropertyDescriptors.addAll((List) Ognl.getValue("#this.{ ? name == \"" + mapping.getName()
// + "\"}", propertyDescriptors));
// }
// } catch (Exception ex) {
// throw new TynamoRuntimeException(ex);
// }
// return sortedPropertyDescriptors;
// }
/**
* Find the Hibernate metadata for this type, traversing up the hierarchy to
* supertypes if necessary
*
* @param type
* @return
*/
protected <T> Metamodel findMetadata(Class<T> type) throws MetadataNotFoundException {
Metamodel managedType = entityManager.getMetamodel();
if (managedType != null) {
return managedType;
}
if (!type.equals(Object.class)) {
return findMetadata(type.getSuperclass());
} else {
throw new MetadataNotFoundException("Failed to find metadata.");
}
}
/**
* Checks to see if a property descriptor is in a component mapping
*
* @param componentMapping
* @param propertyDescriptor
* @return true if the propertyDescriptor property is in componentMapping
*/
protected boolean notAHibernateProperty(EmbeddableType componentMapping, TynamoPropertyDescriptor propertyDescriptor) {
for (Object obj : componentMapping.getAttributes()) {
Attribute property = (Attribute) obj;
if (property.getName().equals(propertyDescriptor.getName())) {
return false;
}
}
return true;
}
private boolean isLarge(SingularAttribute mappingProperty) {
// Hack to avoid setting large property if length
// is exactly equal to Hibernate default column length
return findColumnLength(mappingProperty) != 255
&& findColumnLength(mappingProperty) > largeColumnLength;
}
private Annotation getAnnotation(Member member, Class annotationType) {
return member instanceof Field ? ((Field) member).getAnnotation(annotationType)
: member instanceof Method ? ((Method) member).getAnnotation(annotationType) : null;
}
private int findColumnLength(SingularAttribute mappingProperty) {
Annotation a = getAnnotation(mappingProperty.getJavaMember(), Column.class);
if (a != null) {
Column column = (Column) a;
return column.length();
} else {
return 255; // default length
}
}
protected boolean notJpaProperty(Metamodel metamodel, Class classType, TynamoPropertyDescriptor descriptor) {
ManagedType type = metamodel.managedType(classType);
Set<Attribute> attrs = type.getAttributes();
boolean ret = true;
for (Attribute a : attrs) {
if (a.getName().equals(descriptor.getName())) {
ret = false;
}
}
return ret;
}
/**
* @param type
* @param descriptor
* @param parentClassDescriptor
* @return
*/
private IdentifierDescriptor createIdentifierDescriptor(Class type, TynamoPropertyDescriptor descriptor, TynamoClassDescriptor parentClassDescriptor) {
IdentifierDescriptor identifierDescriptor;
ManagedType mapping = getMapping(type);
if (mapping.getAttribute(descriptor.getName()).getPersistentAttributeType().equals(Attribute.PersistentAttributeType.EMBEDDED)) {
EmbeddedDescriptor embeddedDescriptor = buildEmbeddedDescriptor(type,
findMetadata(type), descriptor, parentClassDescriptor);
embeddedDescriptor.setIdentifier(true);
identifierDescriptor = embeddedDescriptor;
} else {
identifierDescriptor = new IdentifierDescriptorImpl(type, descriptor);
}
if ( getAnnotation(mapping.getSingularAttribute(descriptor.getName()).getJavaMember(), GeneratedValue.class) == null)
{
identifierDescriptor.setGenerated(false);
}
return identifierDescriptor;
}
public TynamoPropertyDescriptor decorateAssociationDescriptor(Attribute attribute, Metamodel metamodel,
TynamoPropertyDescriptor descriptor, TynamoClassDescriptor parentClassDescriptor) {
ObjectReferenceDescriptor descriptorReference = new ObjectReferenceDescriptor(attribute.getJavaType(), descriptor, attribute.getJavaType());
System.out.println("Association Type: " + attribute.getPersistentAttributeType());
switch (attribute.getPersistentAttributeType()) {
case ONE_TO_ONE: {
Annotation a = attribute.getJavaType().getAnnotation(OneToOne.class);
if (a != null) {
OneToOne aOneToOne = (OneToOne) a;
String inverseProperty = aOneToOne.mappedBy();
//descriptorReference.setChildRelationship(aOneToOne.orphanRemoval());
//TODO: understand what is going on here and optimize it
if ("".equals(inverseProperty)) {
// http://forums.jpa.org/viewtopic.php?t=974287&sid=12d018b08dffe07e263652190cfc4e60
// Caution... this does not support multiple
// class references across the OneToOne relationship
Class returnType = attribute.getJavaType();
for (int i = 0; i < returnType.getDeclaredMethods().length; i++) {
if (returnType.getDeclaredMethods()[i].getReturnType().equals(attribute.getDeclaringType().getJavaType())) {
Method theProperty = returnType.getDeclaredMethods()[i];
/* strips preceding 'get' */
inverseProperty = theProperty.getName().substring(3).toLowerCase();
break;
}
}
}
} else {
return descriptorReference;
}
}
break;
case MANY_TO_ONE: {
Annotation a = attribute.getJavaType().getAnnotation(OneToOne.class);
if (a != null) {
ManyToOne aManyToOne = (ManyToOne) a;
//TODO: do some stuff?
}
}
break;
}
return descriptorReference;
}
/**
* @param type
* @return
*/
protected ManagedType getMapping(Class type) {
//Configuration cfg = entityManagerSource.getConfiguration();
return entityManager.getMetamodel().managedType(type);
//return cfg.getClassMapping(type.getName());
}
private Set<String> getIdentifierProperties(Class type) {
try {
ManagedType mapping = getMapping(type);
if (!(mapping instanceof EntityType)) return null;
EntityType entityType = (EntityType)mapping;
// NOTE either eclipselink 2.4.0 has a bug or I don't understand how hasSingleIdAttribute is supposed to work, but
// it returns true even if a class is annotated with @IdClass and has multiple @Id attributes.
// Directly check for annotation as well as a fix
if (!entityType.hasSingleIdAttribute() || type.isAnnotationPresent(IdClass.class)) {
Set<SingularAttribute> idAttrs = entityType.getIdClassAttributes();
Set<String> result = new HashSet<String>(idAttrs.size());
for (SingularAttribute attr : idAttrs)
result.add(attr.getName());
return result;
}
Set<String> result = new HashSet<String>(1);
result.add(entityType.getId(entityType.getIdType().getJavaType()).getName());
return result;
} catch (PersistenceException e) {
throw new TynamoRuntimeException(e);
}
}
}