package net.eusashead.bjugquerydsl.hateoas;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
public class JpaEntityResourceMetadataExtractor implements
ResourceMetadataExtractor {
private final ConcurrentMap<Class<?>, ResourceMetadata> cache = new ConcurrentHashMap<Class<?>, ResourceMetadata>();
/* (non-Javadoc)
* @see net.eusashead.bjugquerydsl.hateoas.ResourceMetadataExtractor#extract(java.lang.Class)
*/
@Override
public ResourceMetadata extract(Class<?> target) {
// Get an ancestor with @Entity annotation
Class<?> entity = getEntityClass(target);
// Check cache for previously extracted metadata.
ResourceMetadata metadata = cache.putIfAbsent(entity, buildResourceMetadata(entity));
if (metadata == null) {
metadata = cache.get(entity);
}
return metadata;
}
/**
* Because Hibernate and other JPA
* implementations tend to use proxies
* or modified bytecode, we need to check
* not just the returned object's class
* but also its ancestors (which the
* proxy will extend).
* @param target
* @return
*/
private Class<?> getEntityClass(Class<?> target) {
if (target == null) {
return null;
}
if (target.isAnnotationPresent(Entity.class)) {
return target;
} else {
return getEntityClass(target.getSuperclass());
}
}
private ResourceMetadata buildResourceMetadata(Class<?> target) {
// Check it's an @Entity (or one of its ancestors is)
Class<?> entity = getEntityClass(target);
if (entity == null) {
throw new IllegalArgumentException(String.format("Class %s or its ancestors doesn't have an @Entity annotation.", target));
}
// Create a builder
final ResourceMetadataBuilder builder = new ResourceMetadataBuilder();
// Process the properties
BeanInfo beanInfo;
try {
beanInfo = Introspector.getBeanInfo(entity);
for (PropertyDescriptor property : beanInfo.getPropertyDescriptors()) {
if (!property.getName().equals("class")) {
extractPropertyMetadata(property, builder);
}
}
} catch (IntrospectionException e) {
throw new RuntimeException(e);
} catch (SecurityException e) {
throw new RuntimeException(e);
}
// Build the metadata
return builder.build();
}
/**
* Get the property metadata
* for the supplied property
* @param property
* @param builder
*/
@SuppressWarnings("unchecked")
private void extractPropertyMetadata(PropertyDescriptor property, ResourceMetadataBuilder builder) {
// Get the field for the property
Method accessor = property.getReadMethod();
// Get the property name
String name = property.getName();
// Is it an identity field?
if (AnnotationHelper.fieldOrPropertyAnnotated(property, Id.class)) {
builder.identityProperty(name, accessor);
}
// Is it a relation field (and therefore embedded)?
else if (AnnotationHelper.fieldOrPropertyAnnotated(property, ManyToOne.class, OneToOne.class, OneToMany.class, ManyToMany.class)) {
builder.embeddedResource(name, accessor);
}
// None of the above, must be @Basic or unadorned.
else {
builder.simpleProperty(name, accessor);
}
}
/* (non-Javadoc)
* @see net.eusashead.bjugquerydsl.hateoas.ResourceMetadataExtractor#canExtract(java.lang.Class)
*/
@Override
public boolean canExtract(Class<?> target) {
// Check it's an @Entity (or an ancestor is)
return getEntityClass(target) != null ? true : false;
}
}