Massive update that reworks the entire metadata system.

This commit is contained in:
Jon Brisbin
2012-04-30 16:13:11 -05:00
parent 077529e830
commit d78126d018
26 changed files with 918 additions and 781 deletions

View File

@@ -14,8 +14,8 @@ allprojects {
}
repositories {
maven { url "http://repo.springframework.org/libs-milestone" }
maven { url "http://repo.springframework.org/libs-release" }
maven { url "http://repo.springframework.org/milestone" }
maven { url "http://repo.springframework.org/release" }
mavenCentral()
mavenLocal()
}

View File

@@ -200,3 +200,6 @@ To maintain a relationship between two entities, access the resource of the rela
You can also delete a relationship by issuing a DELETE request to the resource path that represents the relationship between parent and child entities. For example, to delete a relationship between a Profile entity with an id of 2 and a Person with an id of 1:
curl -v -X DELETE http://localhost:8080/data/person/1/profiles/2
### Handling events

View File

@@ -1,6 +1,6 @@
# Logging
slf4jVersion = 1.6.4
logbackVersion = 1.0.0
logbackVersion = 1.0.1
# Libraries
springVersion = 3.1.1.RELEASE
@@ -10,10 +10,10 @@ cglibVersion = 2.2
groovyVersion = 1.8.6
# Supporting libraries
sdCommonsVersion = 1.2.0.RELEASE
sdJpaVersion = 1.0.1.RELEASE
jacksonVersion = 1.9.2
hibernateVersion = 3.5.6-Final
sdCommonsVersion = 1.3.0.RC1
sdJpaVersion = 1.1.0.RC1
jacksonVersion = 1.9.5
hibernateVersion = 4.1.1.Final
# Testing
spockVersion = 0.5-groovy-1.8

View File

@@ -1,6 +1,6 @@
#Tue Mar 27 08:53:14 CDT 2012
#Fri Apr 27 15:44:36 CDT 2012
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=http\://services.gradle.org/distributions/gradle-1.0-milestone-9-bin.zip
distributionUrl=http\://services.gradle.org/distributions/gradle-1.0-rc-1-bin.zip

View File

@@ -17,6 +17,6 @@ dependencies {
// Testing
testCompile "org.hibernate:hibernate-entitymanager:$hibernateVersion"
testCompile "org.hsqldb:hsqldb:1.8.0.10"
testCompile "org.hsqldb:hsqldb:2.2.8"
}

View File

@@ -0,0 +1,34 @@
package org.springframework.data.rest.repository;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public interface AttributeMetadata {
String name();
Class<?> type();
Class<?> elementType();
boolean isCollectionLike();
Collection<?> asCollection(Object target);
boolean isSetLike();
Set<?> asSet(Object target);
boolean isMapLike();
Map asMap(Object target);
Object get(Object target);
AttributeMetadata set(Object value, Object target);
}

View File

@@ -0,0 +1,22 @@
package org.springframework.data.rest.repository;
import java.util.Map;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public interface EntityMetadata<A extends AttributeMetadata> {
Class<?> type();
Map<String, A> embeddedAttributes();
Map<String, A> linkedAttributes();
A idAttribute();
A versionAttribute();
A attribute(String name);
}

View File

@@ -1,182 +0,0 @@
package org.springframework.data.rest.repository;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.PluralAttribute;
import javax.persistence.metamodel.SingularAttribute;
import org.springframework.data.rest.core.Handler;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class JpaEntityMetadata {
private Class<?> targetType;
private Map<String, Attribute> embeddedAttributes = new HashMap<String, Attribute>();
private Map<String, Field> fields = new HashMap<String, Field>();
private Map<String, Method> setters = new HashMap<String, Method>();
private Map<String, Method> getters = new HashMap<String, Method>();
private Map<String, Attribute> linkedAttributes = new HashMap<String, Attribute>();
private final Attribute idAttribute;
private final Attribute versionAttribute;
@SuppressWarnings({"unchecked"})
public JpaEntityMetadata(EntityType entityType, JpaRepositoryMetadata repositoryMetadata) {
targetType = entityType.getJavaType();
Attribute idAttribute = entityType.getId(entityType.getIdType().getJavaType());
Attribute versionAttribute = entityType.getVersion(Long.class);
for (Attribute attr : (Set<Attribute>) entityType.getAttributes()) {
String name = attr.getName();
Field f = ReflectionUtils.findField(targetType, attr.getName());
ReflectionUtils.makeAccessible(f);
fields.put(name, f);
Method setter = null;
try {
setter = targetType.getMethod("set" + StringUtils.capitalize(name), attr.getJavaType());
} catch (NoSuchMethodException e) {}
if (null != setter) {
setters.put(name, setter);
}
Method getter = null;
try {
getter = targetType.getMethod("get" + StringUtils.capitalize(name));
} catch (NoSuchMethodException e) {}
if (null != setter) {
getters.put(name, getter);
}
if (attr instanceof SingularAttribute) {
SingularAttribute sattr = (SingularAttribute) attr;
if (null != repositoryMetadata.repositoryFor(attr.getJavaType())) {
linkedAttributes.put(name, attr);
} else if (!sattr.isId() && !sattr.isVersion()) {
embeddedAttributes.put(name, attr);
}
} else if (attr instanceof PluralAttribute) {
PluralAttribute pattr = (PluralAttribute) attr;
if (pattr.getElementType() instanceof EntityType
&& null != repositoryMetadata.repositoryFor(pattr.getElementType().getJavaType())) {
linkedAttributes.put(name, attr);
} else {
embeddedAttributes.put(name, attr);
}
}
}
this.idAttribute = idAttribute;
this.versionAttribute = versionAttribute;
}
public Class<?> targetType() {
return targetType;
}
public Map<String, Attribute> embeddedAttributes() {
return Collections.unmodifiableMap(embeddedAttributes);
}
public Map<String, Attribute> linkedAttributes() {
return Collections.unmodifiableMap(linkedAttributes);
}
public Attribute idAttribute() {
return idAttribute;
}
public Attribute versionAttribute() {
return versionAttribute;
}
public void id(Serializable id, Object target) {
set(idAttribute.getName(), id, target);
}
public Object id(Object target) {
return get(idAttribute.getName(), target);
}
public Object version(Object target) {
return (null != versionAttribute ? get(versionAttribute.getName(), target) : null);
}
public <V> V doWithEmbedded(Handler<Attribute, V> handler) {
if (null == handler) {
return null;
}
V v = null;
for (Attribute attr : embeddedAttributes.values()) {
v = handler.handle(attr);
}
return v;
}
public <V> V doWithLinked(String name, Handler<Attribute, V> handler) {
if (null == handler) {
return null;
}
V v = null;
Attribute attr = linkedAttributes.get(name);
if (null != attr) {
v = handler.handle(attr);
}
return v;
}
public <V> V doWithLinked(Handler<Attribute, V> handler) {
if (null == handler) {
return null;
}
V v = null;
for (Attribute attr : linkedAttributes.values()) {
v = handler.handle(attr);
}
return v;
}
public Object get(String name, Object target) {
try {
Method getter = getters.get(name);
if (null != getter) {
return getter.invoke(target);
} else {
Field f = fields.get(name);
return (null != f ? f.get(target) : null);
}
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
} catch (InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
public void set(String name, Object arg, Object target) {
try {
Method setter = setters.get(name);
if (null != setter) {
setter.invoke(target, arg);
} else {
Field f = fields.get(name);
if (null != f) {
f.set(target, arg);
}
}
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
} catch (InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
}

View File

@@ -1,180 +0,0 @@
package org.springframework.data.rest.repository;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.Metamodel;
import org.springframework.aop.support.AopUtils;
import org.springframework.aop.target.SingletonTargetSource;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.util.ReflectionUtils;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class JpaRepositoryMetadata
implements InitializingBean,
ApplicationContextAware {
private ApplicationContext applicationContext;
private Map<Class<?>, RepositoryCacheEntry> repositories = new HashMap<Class<?>, RepositoryCacheEntry>();
private EntityManager entityManager;
private Metamodel metamodel;
@PersistenceContext
public void setEntityManager(EntityManager entityManager) {
this.entityManager = entityManager;
this.metamodel = entityManager.getMetamodel();
}
public EntityManager entityManager() {
return this.entityManager;
}
@SuppressWarnings({"unchecked"})
public <T> CrudRepository<T, ? extends Serializable> repositoryFor(String name) {
if (null != name) {
for (Map.Entry<Class<?>, RepositoryCacheEntry> entry : repositories.entrySet()) {
if (name.equals(repositoryNameFor(entry.getValue().repository))) {
return entry.getValue().repository;
}
}
}
return null;
}
@SuppressWarnings({"unchecked"})
public <T> CrudRepository<T, ? extends Serializable> repositoryFor(Class<T> domainClass) {
RepositoryCacheEntry entry = repositories.get(domainClass);
if (null != entry) {
return entry.repository;
}
return null;
}
@SuppressWarnings({"unchecked"})
public <T> EntityInformation<T, ? extends Serializable> entityInfoFor(Class<T> domainClass) {
RepositoryCacheEntry entry = repositories.get(domainClass);
if (null != entry) {
return entry.entityInfo;
}
return null;
}
public <T> EntityType<T> entityTypeFor(Class<T> domainClass) {
return metamodel.entity(domainClass);
}
@SuppressWarnings({"unchecked"})
public <T> EntityInformation<T, ? extends Serializable> entityInfoFor(CrudRepository<T, ? extends Serializable> repository) {
for (Map.Entry<Class<?>, RepositoryCacheEntry> entry : repositories.entrySet()) {
if (entry.getValue().repository == repository) {
return entry.getValue().entityInfo;
}
}
return null;
}
public <T> JpaEntityMetadata entityMetadataFor(Class<T> domainClass) {
RepositoryCacheEntry entry = repositories.get(domainClass);
if (null == entry.entityMetadata) {
entry.entityMetadata = new JpaEntityMetadata(metamodel.entity(domainClass), this);
}
return entry.entityMetadata;
}
public String repositoryNameFor(Class<?> domainClass) {
RepositoryCacheEntry entry = repositories.get(domainClass);
if (null != entry) {
return entry.name;
}
return null;
}
public String repositoryNameFor(CrudRepository repository) {
for (Map.Entry<Class<?>, RepositoryCacheEntry> entry : repositories.entrySet()) {
if (entry.getValue().repository == repository) {
return entry.getValue().name;
}
}
return null;
}
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public List<String> repositoryNames() {
List<String> names = new ArrayList<String>();
for (Map.Entry<Class<?>, RepositoryCacheEntry> entry : repositories.entrySet()) {
names.add(entry.getValue().name);
}
return names;
}
public void setRepositories(Map<String, CrudRepository> repositories) {
for (Map.Entry<String, CrudRepository> entry : repositories.entrySet()) {
String name = entry.getKey();
CrudRepository repository = entry.getValue();
Class<?> repoClass = AopUtils.getTargetClass(repository);
Field infoField = ReflectionUtils.findField(repoClass, "entityInformation");
ReflectionUtils.makeAccessible(infoField);
Method m = ReflectionUtils.findMethod(repository.getClass(), "getTargetSource");
ReflectionUtils.makeAccessible(m);
try {
SingletonTargetSource targetRepo = (SingletonTargetSource) m.invoke(repository);
EntityInformation entityInfo = (EntityInformation) infoField.get(targetRepo.getTarget());
Class<?>[] intfs = repository.getClass().getInterfaces();
//String name = StringUtils.uncapitalize(intfs[0].getSimpleName().replaceAll("Repository", ""));
if (name.contains("Repository")) {
name = name.replaceAll("Repository", "");
}
this.repositories.put(entityInfo.getJavaType(), new RepositoryCacheEntry(name, repository, entityInfo, null));
} catch (Throwable t) {
throw new IllegalStateException(t);
}
}
}
@Override public void afterPropertiesSet() throws Exception {
if (this.repositories.isEmpty()) {
ApplicationContext appCtx = applicationContext;
while (null != appCtx) {
Map<String, CrudRepository> beans = appCtx.getBeansOfType(CrudRepository.class);
setRepositories(beans);
appCtx = appCtx.getParent();
}
}
}
private class RepositoryCacheEntry {
String name;
CrudRepository repository;
EntityInformation entityInfo;
JpaEntityMetadata entityMetadata;
private RepositoryCacheEntry(String name,
CrudRepository repository,
EntityInformation entityInfo,
JpaEntityMetadata entityMetadata) {
this.name = name;
this.repository = repository;
this.entityInfo = entityInfo;
this.entityMetadata = entityMetadata;
}
}
}

View File

@@ -0,0 +1,112 @@
package org.springframework.data.rest.repository;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.support.RepositoryFactoryInformation;
import org.springframework.data.rest.repository.annotation.RestPathSegment;
import org.springframework.util.StringUtils;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public abstract class RepositoryExporter<M extends RepositoryMetadata<R, E>,
R extends Repository<? extends Object, ? extends Serializable>,
E extends EntityMetadata<? extends AttributeMetadata>>
implements ApplicationContextAware,
InitializingBean {
protected ApplicationContext applicationContext;
protected EntityManager entityManager;
protected Map<String, M> repositoryMetadata;
@PersistenceContext
public void setEntityManager(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@SuppressWarnings({"unchecked"})
@Override public void afterPropertiesSet() throws Exception {
}
public Set<String> repositoryNames() {
maybeCacheRepositoryFactoryInfo();
return repositoryMetadata.keySet();
}
public boolean hasRepositoryFor(Class<?> domainType) {
maybeCacheRepositoryFactoryInfo();
for (M repoMeta : repositoryMetadata.values()) {
if (repoMeta.domainType().isAssignableFrom(domainType)) {
return true;
}
}
return false;
}
public M repositoryMetadataFor(Class<?> domainType) {
maybeCacheRepositoryFactoryInfo();
for (M repoMeta : repositoryMetadata.values()) {
if (repoMeta.domainType().isAssignableFrom(domainType)) {
return repoMeta;
}
}
return null;
}
public M repositoryMetadataFor(String name) {
maybeCacheRepositoryFactoryInfo();
return repositoryMetadata.get(name);
}
protected abstract M createRepositoryMetadata(
Class repoClass,
R repo,
String name,
EntityInformation entityInfo
);
@SuppressWarnings({"unchecked"})
private void maybeCacheRepositoryFactoryInfo() {
if (null == repositoryMetadata) {
repositoryMetadata = new HashMap<String, M>();
Collection<RepositoryFactoryInformation> providers = BeanFactoryUtils.beansOfTypeIncludingAncestors(
applicationContext,
RepositoryFactoryInformation.class
).values();
for (RepositoryFactoryInformation entry : providers) {
EntityInformation entityInfo = entry.getEntityInformation();
Class repoClass = entry.getRepositoryInterface();
String name;
RestPathSegment pathSeg = AnnotationUtils.findAnnotation(repoClass, RestPathSegment.class);
if (null != pathSeg) {
name = pathSeg.value();
} else {
name = StringUtils.uncapitalize(repoClass.getSimpleName().replaceAll("Repository", ""));
}
R repo = (R) BeanFactoryUtils.beanOfTypeIncludingAncestors(applicationContext, repoClass);
M repoMeta = createRepositoryMetadata(repoClass, repo, name, entityInfo);
repositoryMetadata.put(name, repoMeta);
}
}
}
}

View File

@@ -0,0 +1,65 @@
package org.springframework.data.rest.repository;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public abstract class RepositoryExporterSupport<S extends RepositoryExporterSupport<? super S>> {
@Autowired
protected List<RepositoryExporter> repositoryExporters = Collections.emptyList();
public List<RepositoryExporter> getRepositoryExporters() {
return repositoryExporters;
}
public void setRepositoryExporters(List<RepositoryExporter> repositoryExporters) {
this.repositoryExporters = repositoryExporters;
}
public List<RepositoryExporter> repositoryExporters() {
return repositoryExporters;
}
@SuppressWarnings({"unchecked"})
public S repositoryExporters(List<RepositoryExporter> repositoryExporters) {
this.repositoryExporters = repositoryExporters;
return (S) this;
}
@SuppressWarnings({"unchecked"})
protected RepositoryMetadata repositoryMetadataFor(String name) {
for (RepositoryExporter exporter : repositoryExporters) {
RepositoryMetadata repoMeta = exporter.repositoryMetadataFor(name);
if (null != repoMeta) {
return repoMeta;
}
}
throw new RepositoryNotFoundException("No repository found for name " + name);
}
@SuppressWarnings({"unchecked"})
protected RepositoryMetadata repositoryMetadataFor(Class<?> domainType) {
for (RepositoryExporter exporter : repositoryExporters) {
RepositoryMetadata repoMeta = exporter.repositoryMetadataFor(domainType);
if (null != repoMeta) {
return repoMeta;
}
}
throw new RepositoryNotFoundException("No repository found for type " + domainType.getName());
}
@SuppressWarnings({"unchecked"})
protected RepositoryMetadata repositoryMetadataFor(AttributeMetadata attrMeta) {
if (attrMeta.isCollectionLike() || attrMeta.isMapLike()) {
return repositoryMetadataFor(attrMeta.elementType());
} else {
return repositoryMetadataFor(attrMeta.type());
}
}
}

View File

@@ -0,0 +1,26 @@
package org.springframework.data.rest.repository;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Map;
import org.springframework.data.repository.Repository;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public interface RepositoryMetadata<R extends Repository<? extends Object, ? extends Serializable>, E extends EntityMetadata<? extends AttributeMetadata>> {
String name();
Class<? extends Object> domainType();
R repository();
E entityMetadata();
Method queryMethod(String key);
Map<String, Method> queryMethods();
}

View File

@@ -0,0 +1,18 @@
package org.springframework.data.rest.repository;
import org.springframework.dao.DataAccessResourceFailureException;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class RepositoryNotFoundException extends DataAccessResourceFailureException {
public RepositoryNotFoundException(String msg) {
super(msg);
}
public RepositoryNotFoundException(String msg, Throwable cause) {
super(msg, cause);
}
}

View File

@@ -15,11 +15,11 @@ public class ValidationErrors extends AbstractErrors {
private String name;
private Object entity;
private JpaEntityMetadata entityMetadata;
private EntityMetadata entityMetadata;
private List<ObjectError> globalErrors = new ArrayList<ObjectError>();
private List<FieldError> fieldErrors = new ArrayList<FieldError>();
public ValidationErrors(String name, Object entity, JpaEntityMetadata entityMetadata) {
public ValidationErrors(String name, Object entity, EntityMetadata entityMetadata) {
this.name = name;
this.entity = entity;
this.entityMetadata = entityMetadata;
@@ -56,6 +56,6 @@ public class ValidationErrors extends AbstractErrors {
}
@Override public Object getFieldValue(String field) {
return entityMetadata.get(field, entity);
return entityMetadata.attribute(field).get(entity);
}
}

View File

@@ -0,0 +1,22 @@
package org.springframework.data.rest.repository.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
@Target({
ElementType.METHOD,
ElementType.TYPE
})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RestPathSegment {
String value();
}

View File

@@ -1,43 +1,32 @@
package org.springframework.data.rest.repository.context;
import java.util.List;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.data.rest.repository.JpaRepositoryMetadata;
import org.springframework.data.rest.repository.RepositoryExporter;
import org.springframework.data.rest.repository.RepositoryExporterSupport;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public abstract class AbstractRepositoryEventListener<T extends AbstractRepositoryEventListener<? super T>>
extends RepositoryExporterSupport<T>
implements ApplicationListener<RepositoryEvent>,
ApplicationContextAware {
@Autowired
protected JpaRepositoryMetadata repositoryMetadata;
protected ApplicationContext applicationContext;
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public JpaRepositoryMetadata getRepositoryMetadata() {
return repositoryMetadata;
}
public void setRepositoryMetadata(JpaRepositoryMetadata repositoryMetadata) {
this.repositoryMetadata = repositoryMetadata;
}
public JpaRepositoryMetadata repositoryMetadata() {
return repositoryMetadata;
}
@SuppressWarnings({"unchecked"})
public T repositoryMetadata(JpaRepositoryMetadata repositoryMetadata) {
this.repositoryMetadata = repositoryMetadata;
return (T) this;
@Autowired
public void setRepositoryExporters(List<RepositoryExporter> repositoryExporters) {
super.setRepositoryExporters(repositoryExporters);
}
@Override public final void onApplicationEvent(RepositoryEvent event) {

View File

@@ -86,9 +86,10 @@ public class ValidatingRepositoryEventListener
private Errors validate(String event, Object entity) {
Errors errors = null;
if (null != entity) {
errors = new ValidationErrors(entity.getClass().getSimpleName(),
Class<?> domainType = entity.getClass();
errors = new ValidationErrors(domainType.getSimpleName(),
entity,
repositoryMetadata.entityMetadataFor(entity.getClass()));
repositoryMetadataFor(domainType).entityMetadata());
Collection<Validator> validators = this.validators.get(event);
if (null != validators) {
for (Validator v : validators) {

View File

@@ -0,0 +1,142 @@
package org.springframework.data.rest.repository.jpa;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.PluralAttribute;
import org.springframework.beans.BeanUtils;
import org.springframework.data.rest.repository.AttributeMetadata;
import org.springframework.util.ReflectionUtils;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class JpaAttributeMetadata implements AttributeMetadata {
private String name;
private Attribute attribute;
private Class<?> type;
private Field field;
private Method getter;
private Method setter;
public JpaAttributeMetadata(EntityType<?> entityType, Attribute attribute) {
this.attribute = attribute;
name = attribute.getName();
type = attribute.getJavaType();
field = ReflectionUtils.findField(entityType.getJavaType(), name);
ReflectionUtils.makeAccessible(field);
PropertyDescriptor property = BeanUtils.getPropertyDescriptor(entityType.getJavaType(), name);
if (null != property) {
getter = property.getReadMethod();
if (null != getter)
ReflectionUtils.makeAccessible(getter);
setter = property.getWriteMethod();
if (null != setter)
ReflectionUtils.makeAccessible(setter);
}
}
@Override public String name() {
return name;
}
@Override public Class<?> type() {
return type;
}
@Override public Class<?> elementType() {
return (attribute instanceof PluralAttribute
? ((PluralAttribute) attribute).getElementType().getJavaType()
: null);
}
@Override public boolean isCollectionLike() {
if (attribute instanceof PluralAttribute) {
PluralAttribute plattr = (PluralAttribute) attribute;
switch (plattr.getCollectionType()) {
case COLLECTION:
case LIST:
return true;
default:
return false;
}
} else {
return false;
}
}
@Override public Collection<?> asCollection(Object target) {
return (Collection<?>) get(target);
}
@Override public boolean isSetLike() {
if (attribute instanceof PluralAttribute) {
PluralAttribute plattr = (PluralAttribute) attribute;
switch (plattr.getCollectionType()) {
case SET:
return true;
default:
return false;
}
} else {
return false;
}
}
@Override public Set<?> asSet(Object target) {
return (Set<?>) get(target);
}
@Override public boolean isMapLike() {
if (attribute instanceof PluralAttribute) {
PluralAttribute plattr = (PluralAttribute) attribute;
switch (plattr.getCollectionType()) {
case MAP:
return true;
default:
return false;
}
} else {
return false;
}
}
@Override public Map asMap(Object target) {
return (Map) get(target);
}
@Override public Object get(Object target) {
try {
if (null != getter) {
return getter.invoke(target);
} else {
return field.get(target);
}
} catch (Exception e) {
return null;
}
}
@Override public AttributeMetadata set(Object value, Object target) {
try {
if (null != setter) {
setter.invoke(target, value);
} else {
field.set(target, value);
}
} catch (Exception e) {
}
return this;
}
}

View File

@@ -0,0 +1,76 @@
package org.springframework.data.rest.repository.jpa;
import java.util.HashMap;
import java.util.Map;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.PluralAttribute;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.repository.EntityMetadata;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class JpaEntityMetadata implements EntityMetadata<JpaAttributeMetadata> {
private Class<?> type;
private JpaAttributeMetadata idAttribute;
private JpaAttributeMetadata versionAttribute;
private Map<String, JpaAttributeMetadata> embeddedAttributes = new HashMap<String, JpaAttributeMetadata>();
private Map<String, JpaAttributeMetadata> linkedAttributes = new HashMap<String, JpaAttributeMetadata>();
@SuppressWarnings({"unchecked"})
public JpaEntityMetadata(Repositories repositories, EntityType<?> entityType) {
type = entityType.getJavaType();
idAttribute = new JpaAttributeMetadata(entityType, entityType.getId(entityType.getIdType().getJavaType()));
if (null != entityType.getVersion(Long.class)) {
versionAttribute = new JpaAttributeMetadata(entityType, entityType.getVersion(Long.class));
}
for (Attribute attr : entityType.getAttributes()) {
Class<?> attrType = (attr instanceof PluralAttribute
? ((PluralAttribute) attr).getElementType().getJavaType()
: attr.getJavaType());
if (repositories.hasRepositoryFor(attrType)) {
linkedAttributes.put(attr.getName(), new JpaAttributeMetadata(entityType, attr));
} else {
embeddedAttributes.put(attr.getName(), new JpaAttributeMetadata(entityType, attr));
}
}
}
@Override public Class<?> type() {
return type;
}
@Override public Map<String, JpaAttributeMetadata> embeddedAttributes() {
return embeddedAttributes;
}
@Override public Map<String, JpaAttributeMetadata> linkedAttributes() {
return linkedAttributes;
}
@Override public JpaAttributeMetadata idAttribute() {
return idAttribute;
}
@Override public JpaAttributeMetadata versionAttribute() {
return versionAttribute;
}
@Override public JpaAttributeMetadata attribute(String name) {
if (idAttribute.name().equals(name)) {
return idAttribute;
} else if (null != versionAttribute && versionAttribute.name().equals(name)) {
return versionAttribute;
} else if (embeddedAttributes.containsKey(name)) {
return embeddedAttributes.get(name);
} else if (linkedAttributes.containsKey(name)) {
return linkedAttributes.get(name);
}
return null;
}
}

View File

@@ -0,0 +1,33 @@
package org.springframework.data.rest.repository.jpa;
import java.io.Serializable;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.repository.RepositoryExporter;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class JpaRepositoryExporter extends RepositoryExporter<
JpaRepositoryMetadata<Repository<Object, Serializable>>,
Repository<Object, Serializable>,
JpaEntityMetadata> {
@SuppressWarnings({"unchecked"})
@Override
protected JpaRepositoryMetadata<Repository<Object, Serializable>> createRepositoryMetadata(
Class repoClass,
Repository<Object, Serializable> repo,
String name,
EntityInformation entityInfo) {
return new JpaRepositoryMetadata(new Repositories(applicationContext),
name,
repoClass,
repo,
entityInfo,
entityManager);
}
}

View File

@@ -0,0 +1,91 @@
package org.springframework.data.rest.repository.jpa;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.persistence.EntityManager;
import javax.persistence.metamodel.Metamodel;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.data.rest.repository.annotation.RestPathSegment;
import org.springframework.util.ReflectionUtils;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class JpaRepositoryMetadata<R extends Repository<Object, Serializable>> implements RepositoryMetadata<R, JpaEntityMetadata> {
private final String name;
private final Class<?> repoClass;
private final R repository;
private final EntityInformation entityInfo;
private final Map<String, Method> queryMethods = new HashMap<String, Method>();
private JpaEntityMetadata entityMetadata;
@SuppressWarnings({"unchecked"})
public JpaRepositoryMetadata(Repositories repositories,
String name,
final Class<?> repoClass,
R repository,
EntityInformation entityInfo,
EntityManager entityManager) {
this.name = name;
this.repoClass = repoClass;
this.repository = repository;
this.entityInfo = entityInfo;
ReflectionUtils.doWithMethods(
repoClass,
new ReflectionUtils.MethodCallback() {
@Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
String pathSeg = AnnotationUtils.findAnnotation(method, RestPathSegment.class).value();
ReflectionUtils.makeAccessible(method);
queryMethods.put(pathSeg, method);
}
},
new ReflectionUtils.MethodFilter() {
@Override public boolean matches(Method method) {
return (!method.isSynthetic()
&& !method.isBridge()
&& method.getDeclaringClass() != Object.class
&& !method.getName().contains("$")
&& null != AnnotationUtils.findAnnotation(method, RestPathSegment.class));
}
}
);
Metamodel metamodel = entityManager.getMetamodel();
entityMetadata = new JpaEntityMetadata(repositories, metamodel.entity(entityInfo.getJavaType()));
}
@Override public String name() {
return name;
}
@Override public Class<? extends Object> domainType() {
return entityMetadata.type();
}
@Override public R repository() {
return repository;
}
@Override public JpaEntityMetadata entityMetadata() {
return entityMetadata;
}
@Override public Method queryMethod(String key) {
return queryMethods.get(key);
}
@Override public Map<String, Method> queryMethods() {
return Collections.unmodifiableMap(queryMethods);
}
}

View File

@@ -11,8 +11,8 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.rest.repository.JpaRepositoryMetadata;
import org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener;
import org.springframework.data.rest.repository.jpa.JpaRepositoryExporter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
@@ -28,7 +28,7 @@ public class RepositoryRestConfiguration {
@Autowired
EntityManagerFactory entityManagerFactory;
@Autowired(required = false)
JpaRepositoryMetadata jpaRepositoryMetadata;
JpaRepositoryExporter jpaRepositoryExporter;
@Autowired(required = false)
ConversionService customConversionService;
ConversionService defaultConversionService = new DefaultConversionService();
@@ -56,18 +56,11 @@ public class RepositoryRestConfiguration {
return httpMessageConverters;
}
@Bean JpaRepositoryMetadata jpaRepositoryMetadata() throws Exception {
if (null == jpaRepositoryMetadata) {
jpaRepositoryMetadata = new JpaRepositoryMetadata();
@Bean JpaRepositoryExporter jpaRepositoryExporter() {
if (null == jpaRepositoryExporter) {
jpaRepositoryExporter = new JpaRepositoryExporter();
}
return jpaRepositoryMetadata;
}
@Bean ValidatingRepositoryEventListener validatingRepositoryEventListener() {
if (null == validatingRepositoryEventListener) {
validatingRepositoryEventListener = new ValidatingRepositoryEventListener();
}
return validatingRepositoryEventListener;
return jpaRepositoryExporter;
}
@Bean PersistenceAnnotationBeanPostProcessor persistenceAnnotationBeanPostProcessor() {

View File

@@ -7,6 +7,7 @@ import java.io.Serializable;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -14,12 +15,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.PluralAttribute;
import javax.persistence.metamodel.SingularAttribute;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.beans.factory.InitializingBean;
@@ -29,19 +25,21 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.rest.core.Handler;
import org.springframework.data.rest.core.Link;
import org.springframework.data.rest.core.SimpleLink;
import org.springframework.data.rest.core.util.UriUtils;
import org.springframework.data.rest.repository.JpaEntityMetadata;
import org.springframework.data.rest.repository.JpaRepositoryMetadata;
import org.springframework.data.rest.repository.AttributeMetadata;
import org.springframework.data.rest.repository.EntityMetadata;
import org.springframework.data.rest.repository.RepositoryConstraintViolationException;
import org.springframework.data.rest.repository.context.AfterLinkSaveEvent;
import org.springframework.data.rest.repository.RepositoryExporter;
import org.springframework.data.rest.repository.RepositoryExporterSupport;
import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.data.rest.repository.context.AfterDeleteEvent;
import org.springframework.data.rest.repository.context.AfterLinkSaveEvent;
import org.springframework.data.rest.repository.context.AfterSaveEvent;
import org.springframework.data.rest.repository.context.BeforeLinkSaveEvent;
import org.springframework.data.rest.repository.context.BeforeDeleteEvent;
import org.springframework.data.rest.repository.context.BeforeLinkSaveEvent;
import org.springframework.data.rest.repository.context.BeforeSaveEvent;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
@@ -63,7 +61,6 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
import org.springframework.web.util.UriComponentsBuilder;
/**
@@ -71,6 +68,7 @@ import org.springframework.web.util.UriComponentsBuilder;
*/
@Controller
public class RepositoryRestController
extends RepositoryExporterSupport<RepositoryRestController>
implements ApplicationEventPublisherAware,
InitializingBean {
@@ -85,34 +83,14 @@ public class RepositoryRestController
private MediaType uriListMediaType = MediaType.parseMediaType("text/uri-list");
private MediaType jsonMediaType = MediaType.parseMediaType("application/x-spring-data+json");
private JpaRepositoryMetadata repositoryMetadata;
private Map<CrudRepository, TypeMetaCacheEntry> typeMetaCache = new ConcurrentHashMap<CrudRepository, TypeMetaCacheEntry>();
private ConversionService conversionService = new DefaultConversionService();
private List<HttpMessageConverter<?>> httpMessageConverters;
private ContentNegotiatingViewResolver viewResolver;
private List<HttpMessageConverter<?>> httpMessageConverters = Collections.emptyList();
private ObjectMapper objectMapper = new ObjectMapper();
@Override public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public JpaRepositoryMetadata getRepositoryMetadata() {
return repositoryMetadata;
}
public void setRepositoryMetadata(JpaRepositoryMetadata repositoryMetadata) {
this.repositoryMetadata = repositoryMetadata;
}
public JpaRepositoryMetadata repositoryMetadata() {
return repositoryMetadata;
}
public RepositoryRestController repositoryMetadata(JpaRepositoryMetadata repositoryMetadata) {
this.repositoryMetadata = repositoryMetadata;
return this;
}
public ConversionService getConversionService() {
return conversionService;
}
@@ -147,23 +125,6 @@ public class RepositoryRestController
return this;
}
public ContentNegotiatingViewResolver getViewResolver() {
return viewResolver;
}
public void setViewResolver(ContentNegotiatingViewResolver viewResolver) {
this.viewResolver = viewResolver;
}
public ContentNegotiatingViewResolver viewResolver() {
return viewResolver;
}
public RepositoryRestController viewResolver(ContentNegotiatingViewResolver viewResolver) {
this.viewResolver = viewResolver;
return this;
}
public MediaType getUriListMediaType() {
return uriListMediaType;
}
@@ -220,6 +181,7 @@ public class RepositoryRestController
Assert.notNull(httpMessageConverters, "HttpMessageConverters cannot be null");
}
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/",
method = RequestMethod.GET,
@@ -232,8 +194,10 @@ public class RepositoryRestController
URI baseUri = uriBuilder.build().toUri();
Links links = new Links();
for (String name : repositoryMetadata.repositoryNames()) {
links.add(new SimpleLink(name, buildUri(baseUri, name)));
for (RepositoryExporter repoMeta : repositoryExporters) {
for (String name : (Set<String>) repoMeta.repositoryNames()) {
links.add(new SimpleLink(name, buildUri(baseUri, name)));
}
}
model.addAttribute(STATUS, HttpStatus.OK);
@@ -253,20 +217,13 @@ public class RepositoryRestController
Model model) {
URI baseUri = uriBuilder.build().toUri();
final CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
return;
}
final TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
Links links = new Links();
Iterator iter = repo.findAll().iterator();
Iterator iter = ((CrudRepository) repoMeta.repository()).findAll().iterator();
while (iter.hasNext()) {
Object o = iter.next();
Serializable id = typeMeta.entityInfo.getId(o);
Serializable id = (Serializable) repoMeta.entityMetadata().idAttribute().get(o);
links.add(new SimpleLink(o.getClass().getSimpleName(), buildUri(baseUri, repository, id.toString())));
}
@@ -288,16 +245,10 @@ public class RepositoryRestController
Model model) throws IOException {
URI baseUri = uriBuilder.build().toUri();
CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_IMPLEMENTED);
return;
}
final TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
CrudRepository repo = (CrudRepository) repoMeta.repository();
MediaType incomingMediaType = request.getHeaders().getContentType();
final Object incoming = readIncoming(request, incomingMediaType, typeMeta.domainClass);
final Object incoming = readIncoming(request, incomingMediaType, repoMeta.entityMetadata().type());
if (null == incoming) {
model.addAttribute(STATUS, HttpStatus.NOT_ACCEPTABLE);
} else {
@@ -308,7 +259,7 @@ public class RepositoryRestController
if (null != eventPublisher) {
eventPublisher.publishEvent(new AfterSaveEvent(savedEntity));
}
String sId = typeMeta.entityInfo.getId(savedEntity).toString();
String sId = repoMeta.entityMetadata().idAttribute().get(savedEntity).toString();
URI selfUri = buildUri(baseUri, repository, sId);
@@ -335,21 +286,18 @@ public class RepositoryRestController
Model model) {
URI baseUri = uriBuilder.build().toUri();
CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
return;
}
TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
Serializable serId = stringToSerializable(id, typeMeta.idType);
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
Serializable serId = stringToSerializable(id,
(Class<? extends Serializable>) repoMeta.entityMetadata()
.idAttribute()
.type());
CrudRepository repo = (CrudRepository) repoMeta.repository();
Object entity = repo.findOne(serId);
if (null == entity) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
} else {
HttpHeaders headers = new HttpHeaders();
Object version = typeMeta.entityMetadata.version(entity);
Object version = repoMeta.entityMetadata().versionAttribute().get(entity);
if (null != version) {
List<String> etags = request.getHeaders().getIfNoneMatch();
for (String etag : etags) {
@@ -361,7 +309,7 @@ public class RepositoryRestController
headers.set("ETag", "\"" + version.toString() + "\"");
}
Map<String, Object> entityDto = extractPropertiesLinkAware(entity,
typeMeta.entityMetadata,
repoMeta.entityMetadata(),
UriComponentsBuilder.fromUri(baseUri)
.pathSegment(repository, id)
.build()
@@ -398,19 +346,17 @@ public class RepositoryRestController
InstantiationException {
URI baseUri = uriBuilder.build().toUri();
CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_IMPLEMENTED);
return;
}
final TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
Serializable serId = stringToSerializable(id, typeMeta.idType);
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
Serializable serId = stringToSerializable(id,
(Class<? extends Serializable>) repoMeta.entityMetadata()
.idAttribute()
.type());
CrudRepository repo = (CrudRepository) repoMeta.repository();
Object entity = null;
Class<?> domainType = repoMeta.entityMetadata().type();
switch (request.getMethod()) {
case POST:
entity = typeMeta.domainClass.newInstance();
entity = domainType.newInstance();
break;
case PUT:
entity = repo.findOne(serId);
@@ -421,12 +367,11 @@ public class RepositoryRestController
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
} else {
final MediaType incomingMediaType = request.getHeaders().getContentType();
final Object incoming = readIncoming(request, incomingMediaType, typeMeta.domainClass);
final Object incoming = readIncoming(request, incomingMediaType, domainType);
if (null == incoming) {
throw new HttpMessageNotReadableException("Could not create an instance of " + typeMeta.domainClass
.getSimpleName() + " from input.");
throw new HttpMessageNotReadableException("Could not create an instance of " + domainType.getSimpleName() + " from input.");
} else {
typeMeta.entityMetadata.id(serId, incoming);
repoMeta.entityMetadata().idAttribute().set(serId, incoming);
if (request.getMethod() == HttpMethod.POST) {
if (null != eventPublisher) {
eventPublisher.publishEvent(new BeforeSaveEvent(incoming));
@@ -462,14 +407,12 @@ public class RepositoryRestController
public void deleteEntity(@PathVariable String repository,
@PathVariable String id,
Model model) {
CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
return;
}
TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
Serializable serId = stringToSerializable(id, typeMeta.idType);
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
Serializable serId = stringToSerializable(id,
(Class<? extends Serializable>) repoMeta.entityMetadata()
.idAttribute()
.type());
CrudRepository repo = (CrudRepository) repoMeta.repository();
if (null != eventPublisher) {
eventPublisher.publishEvent(new BeforeDeleteEvent(serId));
@@ -499,53 +442,41 @@ public class RepositoryRestController
Model model) {
URI baseUri = uriBuilder.build().toUri();
CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
return;
}
TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
Serializable serId = stringToSerializable(id, typeMeta.idType);
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
Serializable serId = stringToSerializable(id,
(Class<? extends Serializable>) repoMeta.entityMetadata()
.idAttribute()
.type());
CrudRepository repo = (CrudRepository) repoMeta.repository();
Object entity = repo.findOne(serId);
if (null == entity) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
} else {
Attribute attr = typeMeta.entityType.getAttribute(property);
if (null == attr) {
AttributeMetadata attrMeta = repoMeta.entityMetadata().attribute(property);
if (null == attrMeta) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
} else {
Class<?> childType;
if (attr instanceof PluralAttribute) {
childType = ((PluralAttribute) attr).getElementType().getJavaType();
} else {
childType = attr.getJavaType();
}
CrudRepository childRepo = repositoryMetadata.repositoryFor(childType);
if (null == childRepo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
return;
Class<?> attrType = attrMeta.elementType();
if (null == attrType) {
attrType = attrMeta.type();
}
RepositoryMetadata propRepoMeta = repositoryMetadataFor(attrType);
model.addAttribute(STATUS, HttpStatus.OK);
TypeMetaCacheEntry childTypeMeta = typeMetaEntry(childRepo);
Object child = typeMeta.entityMetadata.get(property, entity);
if (null != child) {
Object propVal = attrMeta.get(entity);
AttributeMetadata idAttr = propRepoMeta.entityMetadata().idAttribute();
if (null != propVal) {
Links links = new Links();
if (child instanceof Collection) {
for (Object o : (Collection) child) {
String childId = childTypeMeta.entityInfo.getId(o).toString();
URI uri = buildUri(baseUri, repository, id, property, childId);
links.add(new SimpleLink(childType.getSimpleName(), uri));
if (propVal instanceof Collection) {
for (Object o : (Collection) propVal) {
String propValId = idAttr.get(o).toString();
URI uri = buildUri(baseUri, repository, id, property, propValId);
links.add(new SimpleLink(attrType.getSimpleName(), uri));
}
} else if (child instanceof Map) {
for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) child).entrySet()) {
String childId = childTypeMeta.entityInfo.getId(entry.getValue()).toString();
URI uri = buildUri(baseUri, repository, id, property, childId);
} else if (propVal instanceof Map) {
for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) propVal).entrySet()) {
String propValId = idAttr.get(entry.getValue()).toString();
URI uri = buildUri(baseUri, repository, id, property, propValId);
Object oKey = entry.getKey();
String sKey;
if (ClassUtils.isAssignable(oKey.getClass(), String.class)) {
@@ -556,8 +487,8 @@ public class RepositoryRestController
links.add(new SimpleLink(sKey, uri));
}
} else {
String childId = childTypeMeta.entityInfo.getId(child).toString();
URI uri = buildUri(baseUri, repository, id, property, childId);
String propValId = idAttr.get(propVal).toString();
URI uri = buildUri(baseUri, repository, id, property, propValId);
links.add(new SimpleLink(property, uri));
}
model.addAttribute(RESOURCE, links);
@@ -592,72 +523,57 @@ public class RepositoryRestController
final Model model) throws IOException {
URI baseUri = uriBuilder.build().toUri();
CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
return;
}
final TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
Serializable serId = stringToSerializable(id, typeMeta.idType);
final RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
Serializable serId = stringToSerializable(id,
(Class<? extends Serializable>) repoMeta.entityMetadata()
.idAttribute()
.type());
CrudRepository repo = (CrudRepository) repoMeta.repository();
final Object entity = repo.findOne(serId);
if (null == entity) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
} else {
final Attribute attr = typeMeta.entityMetadata.linkedAttributes().get(property);
if (null == attr) {
final AttributeMetadata attrMeta = repoMeta.entityMetadata().attribute(property);
if (null == attrMeta) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
} else {
Object child = typeMeta.entityMetadata.get(attr.getName(), entity);
Object linked = attrMeta.get(entity);
final AtomicReference<String> rel = new AtomicReference<String>();
Handler<Object, Void> entityHandler = new Handler<Object, Void>() {
@Override public Void handle(Object childEntity) {
if (attr instanceof PluralAttribute) {
PluralAttribute plAttr = (PluralAttribute) attr;
switch (plAttr.getCollectionType()) {
case COLLECTION:
case LIST: {
Collection c = new ArrayList();
Collection current = (Collection) typeMeta.entityMetadata.get(property, entity);
if (request.getMethod() == HttpMethod.POST && null != current) {
c.addAll(current);
}
c.add(childEntity);
typeMeta.entityMetadata.set(property, c, entity);
}
break;
case SET: {
Set s = new HashSet();
Set current = (Set) typeMeta.entityMetadata.get(property, entity);
if (request.getMethod() == HttpMethod.POST && null != current) {
s.addAll(current);
}
s.add(childEntity);
typeMeta.entityMetadata.set(property, s, entity);
}
break;
case MAP: {
Map m = new HashMap();
Map current = (Map) typeMeta.entityMetadata.get(property, entity);
if (request.getMethod() == HttpMethod.POST && null != current) {
m.putAll(current);
}
String key = rel.get();
if (null == key) {
model.addAttribute(STATUS, HttpStatus.NOT_ACCEPTABLE);
return null;
} else {
m.put(rel.get(), childEntity);
typeMeta.entityMetadata.set(property, m, entity);
}
}
break;
@Override public Void handle(Object linkedEntity) {
if (attrMeta.isCollectionLike()) {
Collection c = new ArrayList();
Collection current = (Collection) attrMeta.get(entity);
if (request.getMethod() == HttpMethod.POST && null != current) {
c.addAll(current);
}
} else if (attr instanceof SingularAttribute) {
typeMeta.entityMetadata.set(property, childEntity, entity);
c.add(linkedEntity);
attrMeta.set(c, entity);
} else if (attrMeta.isSetLike()) {
Set s = new HashSet();
Set current = (Set) attrMeta.get(entity);
if (request.getMethod() == HttpMethod.POST && null != current) {
s.addAll(current);
}
s.add(linkedEntity);
attrMeta.set(s, entity);
} else if (attrMeta.isMapLike()) {
Map m = new HashMap();
Map current = (Map) attrMeta.get(entity);
if (request.getMethod() == HttpMethod.POST && null != current) {
m.putAll(current);
}
String key = rel.get();
if (null == key) {
model.addAttribute(STATUS, HttpStatus.NOT_ACCEPTABLE);
return null;
} else {
m.put(rel.get(), linkedEntity);
attrMeta.set(m, entity);
}
} else {
attrMeta.set(linkedEntity, entity);
}
return null;
}
};
@@ -686,12 +602,12 @@ public class RepositoryRestController
if (null != eventPublisher) {
eventPublisher.publishEvent(new BeforeSaveEvent(entity));
eventPublisher.publishEvent(new BeforeLinkSaveEvent(entity, child));
eventPublisher.publishEvent(new BeforeLinkSaveEvent(entity, linked));
}
Object savedEntity = repo.save(entity);
if (null != eventPublisher) {
child = typeMeta.entityMetadata.get(attr.getName(), savedEntity);
eventPublisher.publishEvent(new AfterLinkSaveEvent(savedEntity, child));
linked = attrMeta.get(savedEntity);
eventPublisher.publishEvent(new AfterLinkSaveEvent(savedEntity, linked));
eventPublisher.publishEvent(new AfterSaveEvent(savedEntity));
}
@@ -715,26 +631,23 @@ public class RepositoryRestController
@PathVariable String id,
@PathVariable String property,
Model model) {
CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
return;
}
final TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
Serializable serId = stringToSerializable(id, typeMeta.idType);
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
Serializable serId = stringToSerializable(id,
(Class<? extends Serializable>) repoMeta.entityMetadata()
.idAttribute()
.type());
CrudRepository repo = (CrudRepository) repoMeta.repository();
final Object entity = repo.findOne(serId);
if (null == entity) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
} else {
final Attribute attr = typeMeta.entityMetadata.linkedAttributes().get(property);
if (null != attr) {
Object child = typeMeta.entityMetadata.get(property, entity);
typeMeta.entityMetadata.set(property, null, entity);
AttributeMetadata attrMeta = repoMeta.entityMetadata().attribute(property);
if (null != attrMeta) {
Object linked = attrMeta.get(entity);
attrMeta.set(null, entity);
if (null != eventPublisher) {
eventPublisher.publishEvent(new BeforeLinkSaveEvent(entity, child));
eventPublisher.publishEvent(new BeforeLinkSaveEvent(entity, linked));
}
Object savedEntity = repo.save(entity);
if (null != eventPublisher) {
@@ -750,7 +663,7 @@ public class RepositoryRestController
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/{repository}/{id}/{property}/{childId}",
value = "/{repository}/{id}/{property}/{linkedId}",
method = {
RequestMethod.GET
},
@@ -758,36 +671,36 @@ public class RepositoryRestController
"application/json"
}
)
public void childEntity(UriComponentsBuilder uriBuilder,
@PathVariable String repository,
@PathVariable String id,
@PathVariable String property,
@PathVariable String childId,
Model model) {
public void linkedEntity(UriComponentsBuilder uriBuilder,
@PathVariable String repository,
@PathVariable String id,
@PathVariable String property,
@PathVariable String linkedId,
Model model) {
URI baseUri = uriBuilder.build().toUri();
CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
return;
}
final TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
Serializable serId = stringToSerializable(id, typeMeta.idType);
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
Serializable serId = stringToSerializable(id,
(Class<? extends Serializable>) repoMeta.entityMetadata()
.idAttribute()
.type());
CrudRepository repo = (CrudRepository) repoMeta.repository();
final Object entity = repo.findOne(serId);
if (null != entity) {
final Attribute attr = typeMeta.entityMetadata.linkedAttributes().get(property);
if (null != attr) {
// Find child entity
CrudRepository childRepo = repositoryFromAttribute(attr);
if (null != childRepo) {
TypeMetaCacheEntry childTypeMeta = typeMetaEntry(childRepo);
Serializable sChildId = stringToSerializable(childId, childTypeMeta.idType);
Object childEntity = childRepo.findOne(sChildId);
if (null != childEntity) {
Map<String, Object> entityDto = extractPropertiesLinkAware(childEntity,
childTypeMeta.entityMetadata,
AttributeMetadata attrMeta = repoMeta.entityMetadata().attribute(property);
if (null != attrMeta) {
// Find linked entity
RepositoryMetadata linkedRepoMeta = repositoryMetadataFor(attrMeta);
if (null != linkedRepoMeta) {
CrudRepository linkedRepo = (CrudRepository) linkedRepoMeta.repository();
Serializable sChildId = stringToSerializable(linkedId,
(Class<? extends Serializable>) linkedRepoMeta.entityMetadata()
.idAttribute()
.type());
Object linkedEntity = linkedRepo.findOne(sChildId);
if (null != linkedEntity) {
Map<String, Object> entityDto = extractPropertiesLinkAware(linkedEntity,
linkedRepoMeta.entityMetadata(),
baseUri);
URI selfUri = addSelfLink(baseUri, entityDto, repository, id);
@@ -807,7 +720,7 @@ public class RepositoryRestController
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/{repository}/{id}/{property}/{childId}",
value = "/{repository}/{id}/{property}/{linkedId}",
method = {
RequestMethod.DELETE
}
@@ -815,77 +728,68 @@ public class RepositoryRestController
public void deleteLink(@PathVariable String repository,
@PathVariable String id,
@PathVariable String property,
@PathVariable String childId,
@PathVariable String linkedId,
Model model) {
CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
return;
}
final TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
Serializable serId = stringToSerializable(id, typeMeta.idType);
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
Serializable serId = stringToSerializable(id,
(Class<? extends Serializable>) repoMeta.entityMetadata()
.idAttribute()
.type());
CrudRepository repo = (CrudRepository) repoMeta.repository();
final Object entity = repo.findOne(serId);
if (null == entity) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
} else {
final Attribute attr = typeMeta.entityMetadata.linkedAttributes().get(property);
if (null == attr) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
} else {
// Find child entity
CrudRepository childRepo = repositoryFromAttribute(attr);
if (null == childRepo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
} else {
TypeMetaCacheEntry childTypeMeta = typeMetaEntry(childRepo);
Serializable sChildId = stringToSerializable(childId, childTypeMeta.idType);
Object childEntity = childRepo.findOne(sChildId);
if (null != childEntity) {
// Remove child entity from relationship based on property type
if (attr instanceof PluralAttribute) {
PluralAttribute plAttr = (PluralAttribute) attr;
switch (plAttr.getCollectionType()) {
case COLLECTION:
case LIST:
Collection c = (Collection) typeMeta.entityMetadata.get(property, entity);
if (null != c) {
c.remove(childEntity);
}
break;
case SET:
Set s = (Set) typeMeta.entityMetadata.get(property, entity);
if (null != s) {
s.remove(childEntity);
}
break;
case MAP:
Object keyToRemove = null;
Map<Object, Object> m = (Map) typeMeta.entityMetadata.get(property, entity);
if (null != m) {
for (Map.Entry<Object, Object> entry : m.entrySet()) {
Object val = entry.getValue();
if (null != val && val.equals(childEntity)) {
keyToRemove = entry.getKey();
break;
}
}
if (null != keyToRemove) {
m.remove(keyToRemove);
}
}
break;
AttributeMetadata attrMeta = repoMeta.entityMetadata().attribute(property);
if (null != attrMeta) {
// Find linked entity
RepositoryMetadata linkedRepoMeta = repositoryMetadataFor(attrMeta);
if (null != linkedRepoMeta) {
CrudRepository linkedRepo = (CrudRepository) linkedRepoMeta.repository();
Serializable sChildId = stringToSerializable(linkedId,
(Class<? extends Serializable>) linkedRepoMeta.entityMetadata()
.idAttribute()
.type());
Object linkedEntity = linkedRepo.findOne(sChildId);
if (null != linkedEntity) {
// Remove linked entity from relationship based on property type
if (attrMeta.isCollectionLike()) {
Collection c = (Collection) attrMeta.get(entity);
if (null != c) {
c.remove(linkedEntity);
}
} else if (attr instanceof SingularAttribute) {
typeMeta.entityMetadata.set(property, childEntity, entity);
} else if (attrMeta.isSetLike()) {
Set s = (Set) attrMeta.get(entity);
if (null != s) {
s.remove(linkedEntity);
}
} else if (attrMeta.isMapLike()) {
Object keyToRemove = null;
Map<Object, Object> m = (Map) attrMeta.get(entity);
if (null != m) {
for (Map.Entry<Object, Object> entry : m.entrySet()) {
Object val = entry.getValue();
if (null != val && val.equals(linkedEntity)) {
keyToRemove = entry.getKey();
break;
}
}
if (null != keyToRemove) {
m.remove(keyToRemove);
}
}
} else {
attrMeta.set(linkedEntity, entity);
}
model.addAttribute(STATUS, HttpStatus.NO_CONTENT);
return;
}
}
}
}
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
}
@SuppressWarnings({"unchecked"})
@@ -920,26 +824,6 @@ public class RepositoryRestController
return UriComponentsBuilder.fromUri(baseUri).pathSegment(pathSegments).build().toUri();
}
private TypeMetaCacheEntry typeMetaEntry(CrudRepository repo) {
TypeMetaCacheEntry entry = typeMetaCache.get(repo);
if (null == entry) {
entry = new TypeMetaCacheEntry(repo);
typeMetaCache.put(repo, entry);
}
return entry;
}
@SuppressWarnings({"unchecked"})
private CrudRepository repositoryFromAttribute(Attribute attr) {
CrudRepository repo;
if (attr instanceof PluralAttribute) {
repo = repositoryMetadata.repositoryFor(((PluralAttribute) attr).getElementType().getJavaType());
} else {
repo = repositoryMetadata.repositoryFor(attr.getJavaType());
}
return repo;
}
@SuppressWarnings({"unchecked"})
private URI addSelfLink(URI baseUri, Map<String, Object> model, String... pathComponents) {
List<Link> links = (List<Link>) model.get(LINKS);
@@ -972,15 +856,16 @@ public class RepositoryRestController
String repoName = UriUtils.path(uris.get(0));
String sId = UriUtils.path(uris.get(1));
CrudRepository repo = repositoryMetadata.repositoryFor(repoName);
RepositoryMetadata repoMeta = repositoryMetadataFor(repoName);
CrudRepository repo = (CrudRepository) repoMeta.repository();
if (null == repo) {
return null;
}
EntityInformation entityInfo = repositoryMetadata.entityInfoFor(repo);
if (null == entityInfo) {
EntityMetadata entityMeta = repoMeta.entityMetadata();
if (null == entityMeta) {
return null;
}
Class<? extends Serializable> idType = entityInfo.getIdType();
Class<? extends Serializable> idType = (Class<? extends Serializable>) entityMeta.idAttribute().type();
Serializable serId = stringToSerializable(sId, idType);
@@ -1001,58 +886,34 @@ public class RepositoryRestController
}
@SuppressWarnings({"unchecked"})
private Map<String, Object> extractPropertiesLinkAware(final Object entity,
final JpaEntityMetadata entityMetadata,
final URI baseUri) {
private Map<String, Object> extractPropertiesLinkAware(Object entity,
EntityMetadata<AttributeMetadata> entityMetadata,
URI baseUri) {
final Map<String, Object> entityDto = new HashMap<String, Object>();
entityMetadata.doWithEmbedded(new Handler<Attribute, Void>() {
@Override public Void handle(Attribute attr) {
String name = attr.getName();
Object val = entityMetadata.get(name, entity);
if (null != val) {
entityDto.put(name, val);
}
return null;
for (Map.Entry<String, AttributeMetadata> attrMeta : entityMetadata.embeddedAttributes().entrySet()) {
String name = attrMeta.getKey();
Object val = attrMeta.getValue().get(entity);
if (null != val) {
entityDto.put(name, val);
}
});
}
entityMetadata.doWithLinked(new Handler<Attribute, Void>() {
@Override public Void handle(Attribute attr) {
String name = attr.getName();
URI uri = UriComponentsBuilder.fromUri(baseUri)
.pathSegment(name)
.build()
.toUri();
Link l = new SimpleLink(name, uri);
List<Link> links = (List<Link>) entityDto.get(LINKS);
if (null == links) {
links = new ArrayList<Link>();
entityDto.put(LINKS, links);
}
links.add(l);
return null;
for (String attrName : entityMetadata.linkedAttributes().keySet()) {
URI uri = UriComponentsBuilder.fromUri(baseUri)
.pathSegment(attrName)
.build()
.toUri();
Link l = new SimpleLink(attrName, uri);
List<Link> links = (List<Link>) entityDto.get(LINKS);
if (null == links) {
links = new ArrayList<Link>();
entityDto.put(LINKS, links);
}
});
links.add(l);
}
return entityDto;
}
private class TypeMetaCacheEntry {
EntityInformation entityInfo;
Class<?> domainClass;
Class<? extends Serializable> idType;
EntityType entityType;
JpaEntityMetadata entityMetadata;
@SuppressWarnings({"unchecked"})
private TypeMetaCacheEntry(CrudRepository repo) {
entityInfo = repositoryMetadata.entityInfoFor(repo);
domainClass = entityInfo.getJavaType();
idType = entityInfo.getIdType();
entityType = repositoryMetadata.entityTypeFor(domainClass);
entityMetadata = repositoryMetadata.entityMetadataFor(domainClass);
}
}
}

View File

@@ -7,6 +7,8 @@ import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener;
import org.springframework.data.rest.repository.jpa.JpaRepositoryExporter;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver;
@@ -48,15 +50,21 @@ public class RepositoryRestMvcConfiguration {
@Bean RepositoryRestController repositoryRestController() throws Exception {
if (null == repositoryRestController) {
this.repositoryRestController = new RepositoryRestController()
.repositoryMetadata(parentConfig.jpaRepositoryMetadata())
.conversionService(parentConfig.conversionService())
.httpMessageConverters(parentConfig.httpMessageConverters())
.viewResolver(contentNegotiatingViewResolver())
.jsonMediaType("application/json");
}
return repositoryRestController;
}
@Bean ValidatingRepositoryEventListener validatingRepositoryEventListener() {
if (null == parentConfig.validatingRepositoryEventListener) {
return new ValidatingRepositoryEventListener();
}
return parentConfig.validatingRepositoryEventListener;
}
@Bean RequestMappingHandlerMapping handlerMapping() {
return new RequestMappingHandlerMapping();
}

View File

@@ -130,6 +130,7 @@ class RepositoryRestControllerSpec extends Specification {
def addrLinks = model.resource?.links
then:
null != addrLinks
addrLinks.size() == 1
}

View File

@@ -1,9 +1,11 @@
package org.springframework.data.rest.test.webmvc;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.repository.annotation.RestPathSegment;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
@RestPathSegment("person")
public interface PersonRepository extends CrudRepository<Person, Long> {
}