Tweaking query method support.

This commit is contained in:
Jon Brisbin
2012-05-02 11:09:27 -05:00
parent b2564ceeca
commit d549aaecee
67 changed files with 609 additions and 210 deletions

View File

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

View File

@@ -1,7 +1,7 @@
package org.springframework.data.rest.core;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public interface Handler<T,V> {
V handle(T t);

View File

@@ -3,7 +3,7 @@ package org.springframework.data.rest.core;
import java.net.URI;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public interface Link {

View File

@@ -3,7 +3,7 @@ package org.springframework.data.rest.core;
import java.net.URI;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class SimpleLink implements Link {

View File

@@ -21,7 +21,7 @@ import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public abstract class BeanUtils {

View File

@@ -13,7 +13,7 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.util.ClassUtils;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class FluentBeanDeserializer extends StdDeserializer {

View File

@@ -13,7 +13,7 @@ import org.codehaus.jackson.map.ser.std.SerializerBase;
import org.springframework.util.ClassUtils;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class FluentBeanSerializer extends SerializerBase {

View File

@@ -16,7 +16,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.util.ReflectionUtils;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public abstract class FluentBeanUtils {

View File

@@ -6,7 +6,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class RestHelper<T> {

View File

@@ -9,7 +9,7 @@ import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public abstract class UriUtils {

View File

@@ -4,7 +4,7 @@ import org.springframework.data.rest.core.util.UriUtils
import spock.lang.Specification
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
class UriUtilsSpec extends Specification {

View File

@@ -5,30 +5,93 @@ import java.util.Map;
import java.util.Set;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* Encapsulates necessary information about an attribute of a generic entity.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public interface AttributeMetadata {
/**
* Name of the attribute.
*
* @return
*/
String name();
/**
* The type of this attribute.
*
* @return
*/
Class<?> type();
/**
* The element type of this attribute, if this attribute is a "plural"-like attribute (a Collection, Map, etc...).
*
* @return
*/
Class<?> elementType();
/**
* Can this attribute look like a {@link Collection}?
*
* @return
*/
boolean isCollectionLike();
/**
* Get the path of this attribute as a {@link Collection}.
*
* @param target
* @return
*/
Collection<?> asCollection(Object target);
/**
* Can this attribute look like a {@link Set}?
*
* @return
*/
boolean isSetLike();
/**
* Get the path of this attribute as a {@link Set}.
*
* @param target
* @return
*/
Set<?> asSet(Object target);
/**
* Can this attribute look like a {@link Map}?
*
* @return
*/
boolean isMapLike();
/**
* Get the path of this attribute as a {@link Map}.
*
* @param target
* @return
*/
Map asMap(Object target);
/**
* Get the path of this attribute.
*
* @param target
* @return
*/
Object get(Object target);
/**
* Set the path of this attribute.
*
* @param value
* @param target
* @return
*/
AttributeMetadata set(Object value, Object target);
}

View File

@@ -3,20 +3,53 @@ package org.springframework.data.rest.repository;
import java.util.Map;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* Encapsulates necessary metadata about a generic entity.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public interface EntityMetadata<A extends AttributeMetadata> {
/**
* The class of this entity.
*
* @return
*/
Class<?> type();
/**
* A Map of attribute metadata keyed on the attribute's name.
*
* @return
*/
Map<String, A> embeddedAttributes();
/**
* A Map of linked attribute metadata keyed on the attribute's name.
*
* @return
*/
Map<String, A> linkedAttributes();
/**
* The {@link AttributeMetadata} representing the ID of the entity.
*
* @return
*/
A idAttribute();
/**
* The {@link AttributeMetadata} representing the version of the entity, if applicable.
*
* @return
*/
A versionAttribute();
/**
* Get {@link AttributeMetadata} by name.
*
* @param name
* @return
*/
A attribute(String name);
}

View File

@@ -4,7 +4,7 @@ import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.validation.Errors;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class RepositoryConstraintViolationException extends DataIntegrityViolationException {

View File

@@ -2,26 +2,29 @@ package org.springframework.data.rest.repository;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
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.data.repository.support.Repositories;
import org.springframework.data.rest.repository.annotation.RestResource;
import org.springframework.util.StringUtils;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* Abstract class that contains the basic functionality that any exporter will need
* to export a Repository implementation.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public abstract class RepositoryExporter<M extends RepositoryMetadata<R, E>,
R extends Repository<? extends Object, ? extends Serializable>,
@@ -30,12 +33,30 @@ public abstract class RepositoryExporter<M extends RepositoryMetadata<R, E>,
InitializingBean {
protected ApplicationContext applicationContext;
protected EntityManager entityManager;
protected Repositories repositories;
protected List<String> exportOnlyTheseClasses = Collections.emptyList();
protected Map<String, M> repositoryMetadata;
@PersistenceContext
public void setEntityManager(EntityManager entityManager) {
this.entityManager = entityManager;
/**
* Get the list of class names of Repositories to export.
*
* @return a List of class names to export
*/
public List<String> getExportOnlyTheseClasses() {
return exportOnlyTheseClasses;
}
/**
* Set the class names of only those Repositories you want exported.
* Default is to export all found Repositories.
*
* @param exportOnlyTheseClasses
* @return @this
*/
@SuppressWarnings({"unchecked"})
public M setExportOnlyTheseClasses(List<String> exportOnlyTheseClasses) {
this.exportOnlyTheseClasses = exportOnlyTheseClasses;
return (M) this;
}
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
@@ -44,15 +65,45 @@ public abstract class RepositoryExporter<M extends RepositoryMetadata<R, E>,
@SuppressWarnings({"unchecked"})
@Override public void afterPropertiesSet() throws Exception {
repositories = new Repositories(applicationContext);
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;
RestResource pathSeg = repoClass.getAnnotation(RestResource.class);
if (null != pathSeg) {
name = pathSeg.path();
} 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);
}
}
/**
* Get the list of Repository names being exported.
*
* @return
*/
public Set<String> repositoryNames() {
maybeCacheRepositoryFactoryInfo();
return repositoryMetadata.keySet();
}
/**
* Is a Repository being exporter that supports this domain type?
*
* @param domainType
* @return {@literal true} if a Repository is being exported, {@literal false} otherwise.
*/
public boolean hasRepositoryFor(Class<?> domainType) {
maybeCacheRepositoryFactoryInfo();
for (M repoMeta : repositoryMetadata.values()) {
if (repoMeta.domainType().isAssignableFrom(domainType)) {
return true;
@@ -61,8 +112,13 @@ public abstract class RepositoryExporter<M extends RepositoryMetadata<R, E>,
return false;
}
/**
* Get the RepositoryMetadata for the Repository responsible for this domain type.
*
* @param domainType
* @return {@link RepositoryMetadata} instance
*/
public M repositoryMetadataFor(Class<?> domainType) {
maybeCacheRepositoryFactoryInfo();
for (M repoMeta : repositoryMetadata.values()) {
if (repoMeta.domainType().isAssignableFrom(domainType)) {
return repoMeta;
@@ -71,8 +127,13 @@ public abstract class RepositoryExporter<M extends RepositoryMetadata<R, E>,
return null;
}
/**
* Get the {@link RepositoryMetadata} for the Repository exported under the given name.
*
* @param name
* @return {@link RepositoryMetadata} instance
*/
public M repositoryMetadataFor(String name) {
maybeCacheRepositoryFactoryInfo();
return repositoryMetadata.get(name);
}
@@ -83,30 +144,4 @@ public abstract class RepositoryExporter<M extends RepositoryMetadata<R, E>,
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

@@ -6,25 +6,47 @@ import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* Abstract class used as a helper for those classes that need access to the exported repositories.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public abstract class RepositoryExporterSupport<S extends RepositoryExporterSupport<? super S>> {
@Autowired
protected List<RepositoryExporter> repositoryExporters = Collections.emptyList();
/**
* Get a List of {@link RepositoryExporter}s.
*
* @return
*/
public List<RepositoryExporter> getRepositoryExporters() {
return repositoryExporters;
}
/**
* Set the List of {@link RepositoryExporter}s.
*
* @param repositoryExporters
*/
public void setRepositoryExporters(List<RepositoryExporter> repositoryExporters) {
this.repositoryExporters = repositoryExporters;
}
/**
* Get a List of {@link RepositoryExporter}s.
*
* @return
*/
public List<RepositoryExporter> repositoryExporters() {
return repositoryExporters;
}
/**
* Set the List of {@link RepositoryExporter}s.
*
* @param repositoryExporters
*/
@SuppressWarnings({"unchecked"})
public S repositoryExporters(List<RepositoryExporter> repositoryExporters) {
this.repositoryExporters = repositoryExporters;

View File

@@ -6,20 +6,67 @@ import java.util.Map;
import org.springframework.data.repository.Repository;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* Encapsulates necessary metadata about a {@link Repository}.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public interface RepositoryMetadata<R extends Repository<? extends Object, ? extends Serializable>, E extends EntityMetadata<? extends AttributeMetadata>> {
/**
* The name this {@link Repository} is exported under.
*
* @return
*/
String name();
/**
* Get the string value to be used as part of a link {@literal rel} attribute.
*
* @return
*/
String rel();
/**
* The type of domain object this {@link Repository} is repsonsible for.
*
* @return
*/
Class<? extends Object> domainType();
/**
* The Class of the {@link Repository} subinterface.
*
* @return
*/
Class<? extends Repository<? extends Object, ? extends Serializable>> repositoryClass();
/**
* The {@link Repository} instance.
*
* @return
*/
R repository();
/**
* The {@link EntityMetadata} associated with the domain type of this {@literal Repository}.
*
* @return
*/
E entityMetadata();
/**
* Get a {@link RepositoryQueryMethod} by key.
*
* @param key
* @return
*/
RepositoryQueryMethod queryMethod(String key);
/**
* Get a Map of all {@link RepositoryQueryMethod}s, keyed by name.
*
* @return
*/
Map<String, RepositoryQueryMethod> queryMethods();
}

View File

@@ -3,7 +3,7 @@ package org.springframework.data.rest.repository;
import org.springframework.dao.DataAccessResourceFailureException;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class RepositoryNotFoundException extends DataAccessResourceFailureException {

View File

@@ -7,16 +7,10 @@ import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.data.repository.query.Param;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class RepositoryQueryMethod {
private static final Class[] SIMPLE_TYPES = new Class[]{
String.class,
Integer.class,
Long.class,
Boolean.class
};
private static final LocalVariableTableParameterNameDiscoverer nameLookup = new LocalVariableTableParameterNameDiscoverer();
private Method method;

View File

@@ -9,7 +9,7 @@ import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class ValidationErrors extends AbstractErrors {

View File

@@ -7,7 +7,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)

View File

@@ -7,7 +7,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)

View File

@@ -7,7 +7,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)

View File

@@ -7,7 +7,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)

View File

@@ -7,7 +7,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)

View File

@@ -7,7 +7,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)

View File

@@ -7,13 +7,20 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* Advertises classes annotated with this that they are event handlers.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RepositoryEventHandler {
/**
* The list of {@link org.springframework.context.ApplicationEvent} classes this event handler cares about.
*
* @return
*/
Class<?>[] value() default {};
}

View File

@@ -7,7 +7,10 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* Annotate a {@link org.springframework.data.repository.Repository} with this to influence how it is exported and what
* the value of the {@literal rel} attribute will be in links.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@Target({
ElementType.METHOD,
@@ -15,8 +18,10 @@ import java.lang.annotation.Target;
})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RestPathSegment {
public @interface RestResource {
String value();
String path();
String rel() default "";
}

View File

@@ -11,7 +11,10 @@ import org.springframework.data.rest.repository.RepositoryExporter;
import org.springframework.data.rest.repository.RepositoryExporterSupport;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* Abstract class that listens for generic {@link RepositoryEvent}s and dispatches them to a specific
* method based on the event type.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public abstract class AbstractRepositoryEventListener<T extends AbstractRepositoryEventListener<? super T>>
extends RepositoryExporterSupport<T>
@@ -45,16 +48,48 @@ public abstract class AbstractRepositoryEventListener<T extends AbstractReposito
}
}
/**
* Override this method if you are interested in {@literal beforeSave} events.
*
* @param entity
*/
protected void onBeforeSave(Object entity) {}
/**
* Override this method if you are interested in {@literal afterSave} events.
*
* @param entity
*/
protected void onAfterSave(Object entity) {}
/**
* Override this method if you are interested in {@literal beforeLinkSave} events.
*
* @param parent
* @param linked
*/
protected void onBeforeLinkSave(Object parent, Object linked) {}
/**
* Override this method if you are interested in {@literal afterLinkSave} events.
*
* @param parent
* @param linked
*/
protected void onAfterLinkSave(Object parent, Object linked) {}
/**
* Override this method if you are interested in {@literal beforeDelete} events.
*
* @param entity
*/
protected void onBeforeDelete(Object entity) {}
/**
* Override this method if you are interested in {@literal afterDelete} events.
*
* @param entity
*/
protected void onAfterDelete(Object entity) {}
}

View File

@@ -1,7 +1,7 @@
package org.springframework.data.rest.repository.context;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class AfterDeleteEvent extends RepositoryEvent {
public AfterDeleteEvent(Object source) {

View File

@@ -1,7 +1,7 @@
package org.springframework.data.rest.repository.context;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class AfterLinkSaveEvent extends LinkSaveEvent {
public AfterLinkSaveEvent(Object source, Object child) {

View File

@@ -3,7 +3,7 @@ package org.springframework.data.rest.repository.context;
import org.springframework.context.ApplicationEvent;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class AfterSaveEvent extends RepositoryEvent {
public AfterSaveEvent(Object source) {

View File

@@ -15,20 +15,22 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.rest.repository.annotation.HandleAfterLinkSave;
import org.springframework.data.rest.repository.annotation.HandleAfterDelete;
import org.springframework.data.rest.repository.annotation.HandleAfterLinkSave;
import org.springframework.data.rest.repository.annotation.HandleAfterSave;
import org.springframework.data.rest.repository.annotation.HandleBeforeLinkSave;
import org.springframework.data.rest.repository.annotation.HandleBeforeDelete;
import org.springframework.data.rest.repository.annotation.HandleBeforeLinkSave;
import org.springframework.data.rest.repository.annotation.HandleBeforeSave;
import org.springframework.data.rest.repository.annotation.RepositoryEventHandler;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* {@link ApplicationListener} that will dispatch {@link RepositoryEvent}s to handlers annotated with {@link
* RepositoryEventHandler}.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class AnnotatedHandlerRepositoryEventListener
implements ApplicationListener<RepositoryEvent>,
@@ -43,19 +45,41 @@ public class AnnotatedHandlerRepositoryEventListener
this.applicationContext = applicationContext;
}
/**
* Get the base package in which to search for event handlers.
*
* @return
*/
public String getBasePackage() {
return basePackage;
}
/**
* Set the base package in which to search for event handlers.
*
* @param basePackage
* @return
*/
public AnnotatedHandlerRepositoryEventListener setBasePackage(String basePackage) {
this.basePackage = basePackage;
return this;
}
/**
* Get the base package in which to search for event handlers.
*
* @return
*/
public String basePackage() {
return basePackage;
}
/**
* Set the base package in which to search for event handlers.
*
* @param basePackage
* @return
*/
public AnnotatedHandlerRepositoryEventListener basePackage(String basePackage) {
this.basePackage = basePackage;
return this;
@@ -67,7 +91,7 @@ public class AnnotatedHandlerRepositoryEventListener
for (BeanDefinition beanDef : scanner.findCandidateComponents(basePackage)) {
String typeName = beanDef.getBeanClassName();
Class<?> handlerType = ClassUtils.forName(typeName, ClassUtils.getDefaultClassLoader());
RepositoryEventHandler typeAnno = AnnotationUtils.findAnnotation(handlerType, RepositoryEventHandler.class);
RepositoryEventHandler typeAnno = handlerType.getAnnotation(RepositoryEventHandler.class);
Class<?>[] targetTypes = typeAnno.value();
if (targetTypes.length == 0) {
targetTypes = new Class<?>[]{null};
@@ -128,7 +152,7 @@ public class AnnotatedHandlerRepositoryEventListener
Method method,
Class<T> annoType,
Class<? extends RepositoryEvent> eventType) {
T anno = AnnotationUtils.findAnnotation(method, annoType);
T anno = method.getAnnotation(annoType);
if (null != anno) {
try {
Class<?>[] targetTypes;

View File

@@ -3,7 +3,7 @@ package org.springframework.data.rest.repository.context;
import org.springframework.context.ApplicationEvent;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class BeforeDeleteEvent extends RepositoryEvent {
public BeforeDeleteEvent(Object source) {

View File

@@ -1,7 +1,7 @@
package org.springframework.data.rest.repository.context;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class BeforeLinkSaveEvent extends LinkSaveEvent {
public BeforeLinkSaveEvent(Object source, Object linked) {

View File

@@ -3,7 +3,7 @@ package org.springframework.data.rest.repository.context;
import org.springframework.context.ApplicationEvent;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class BeforeSaveEvent extends RepositoryEvent {
public BeforeSaveEvent(Object source) {

View File

@@ -1,7 +1,7 @@
package org.springframework.data.rest.repository.context;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class LinkSaveEvent extends RepositoryEvent {

View File

@@ -3,7 +3,7 @@ package org.springframework.data.rest.repository.context;
import org.springframework.context.ApplicationEvent;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public abstract class RepositoryEvent extends ApplicationEvent {
protected RepositoryEvent(Object source) {

View File

@@ -15,7 +15,10 @@ import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* {@link org.springframework.context.ApplicationListener} implementation that dispatches {@link RepositoryEvent}s to a
* specific {@link Validator}.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class ValidatingRepositoryEventListener
extends AbstractRepositoryEventListener<ValidatingRepositoryEventListener>
@@ -43,10 +46,20 @@ public class ValidatingRepositoryEventListener
}
}
/**
* Get a Map of {@link Validator}s that are assigned to the various {@link RepositoryEvent}s.
*
* @return
*/
public Map<String, Collection<Validator>> getValidators() {
return validators.asMap();
}
/**
* Assign a Map of {@link Validator}s that are assigned to the various {@link RepositoryEvent}s.
*
* @return
*/
public ValidatingRepositoryEventListener setValidators(Map<String, Collection<Validator>> validators) {
for (Map.Entry<String, Collection<Validator>> entry : validators.entrySet()) {
this.validators.replaceValues(entry.getKey(), entry.getValue());
@@ -54,6 +67,13 @@ public class ValidatingRepositoryEventListener
return this;
}
/**
* Add a {@link Validator} that will be triggered on the given event.
*
* @param event
* @param validator
* @return
*/
public ValidatingRepositoryEventListener addValidator(String event, Validator validator) {
validators.put(event, validator);
return this;

View File

@@ -15,7 +15,7 @@ import org.springframework.data.rest.repository.AttributeMetadata;
import org.springframework.util.ReflectionUtils;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class JpaAttributeMetadata implements AttributeMetadata {

View File

@@ -11,7 +11,7 @@ import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.repository.EntityMetadata;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class JpaEntityMetadata implements EntityMetadata<JpaAttributeMetadata> {

View File

@@ -1,20 +1,30 @@
package org.springframework.data.rest.repository.jpa;
import java.io.Serializable;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
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>
* Implementation of {@link RepositoryExporter} for exporting JPA {@link Repository} subinterfaces.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class JpaRepositoryExporter extends RepositoryExporter<
JpaRepositoryMetadata<Repository<Object, Serializable>>,
Repository<Object, Serializable>,
JpaEntityMetadata> {
protected EntityManager entityManager;
@PersistenceContext
public void setEntityManager(EntityManager entityManager) {
this.entityManager = entityManager;
}
@SuppressWarnings({"unchecked"})
@Override
protected JpaRepositoryMetadata<Repository<Object, Serializable>> createRepositoryMetadata(
@@ -22,7 +32,7 @@ public class JpaRepositoryExporter extends RepositoryExporter<
Repository<Object, Serializable> repo,
String name,
EntityInformation entityInfo) {
return new JpaRepositoryMetadata(new Repositories(applicationContext),
return new JpaRepositoryMetadata(repositories,
name,
repoClass,
repo,

View File

@@ -8,31 +8,33 @@ 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.RepositoryQueryMethod;
import org.springframework.data.rest.repository.annotation.RestPathSegment;
import org.springframework.data.rest.repository.annotation.RestResource;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class JpaRepositoryMetadata<R extends Repository<Object, Serializable>> implements RepositoryMetadata<R, JpaEntityMetadata> {
private final String name;
private final Class<?> repoClass;
private final Class<? extends Repository<? extends Object, ? extends Serializable>> repoClass;
private final R repository;
private final EntityInformation entityInfo;
private final Map<String, RepositoryQueryMethod> queryMethods = new HashMap<String, RepositoryQueryMethod>();
private String rel;
private JpaEntityMetadata entityMetadata;
@SuppressWarnings({"unchecked"})
public JpaRepositoryMetadata(Repositories repositories,
String name,
final Class<?> repoClass,
final Class<? extends Repository<? extends Object, ? extends Serializable>> repoClass,
R repository,
EntityInformation entityInfo,
EntityManager entityManager) {
@@ -41,11 +43,19 @@ public class JpaRepositoryMetadata<R extends Repository<Object, Serializable>> i
this.repository = repository;
this.entityInfo = entityInfo;
RestResource resourceAnno = repoClass.getAnnotation(RestResource.class);
if (null != resourceAnno && StringUtils.hasText(resourceAnno.rel())) {
rel = resourceAnno.rel();
} else {
rel = name;
}
ReflectionUtils.doWithMethods(
repoClass,
new ReflectionUtils.MethodCallback() {
@Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
String pathSeg = AnnotationUtils.findAnnotation(method, RestPathSegment.class).value();
RestResource resourceAnno = method.getAnnotation(RestResource.class);
String pathSeg = resourceAnno.path();
ReflectionUtils.makeAccessible(method);
queryMethods.put(pathSeg, new RepositoryQueryMethod(method));
}
@@ -56,7 +66,7 @@ public class JpaRepositoryMetadata<R extends Repository<Object, Serializable>> i
&& !method.isBridge()
&& method.getDeclaringClass() != Object.class
&& !method.getName().contains("$")
&& null != AnnotationUtils.findAnnotation(method, RestPathSegment.class));
&& null != method.getAnnotation(RestResource.class));
}
}
);
@@ -69,10 +79,18 @@ public class JpaRepositoryMetadata<R extends Repository<Object, Serializable>> i
return name;
}
@Override public String rel() {
return rel;
}
@Override public Class<? extends Object> domainType() {
return entityMetadata.type();
}
@Override public Class<? extends Repository<? extends Object, ? extends Serializable>> repositoryClass() {
return repoClass;
}
@Override public R repository() {
return repository;
}

View File

@@ -22,7 +22,7 @@ import org.springframework.data.rest.repository.annotation.HandleBeforeLinkSave
import org.springframework.data.rest.repository.annotation.HandleAfterLinkSave
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@ContextConfiguration(locations = ["/ExtensionsSpec-test.xml"])
class ExtensionsSpec extends Specification {

View File

@@ -15,7 +15,7 @@ import org.springframework.test.context.ContextConfiguration
import spock.lang.Specification
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@ContextConfiguration(locations = ["/JpaMetadataSpec-test.xml"])
class JpaMetadataSpec extends Specification {

View File

@@ -7,7 +7,7 @@ import javax.persistence.Id;
import javax.persistence.OneToMany;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@Entity
public class Family {

View File

@@ -3,7 +3,7 @@ package org.springframework.data.rest.repository.test;
import org.springframework.data.repository.CrudRepository;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public interface FamilyRepository extends CrudRepository<Family, Long> {
}

View File

@@ -5,7 +5,7 @@ import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@Entity
public class Person {

View File

@@ -3,14 +3,14 @@ package org.springframework.data.rest.repository.test;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.repository.annotation.RestPathSegment;
import org.springframework.data.rest.repository.annotation.RestResource;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public interface PersonRepository extends CrudRepository<Person, Long> {
@RestPathSegment("byName")
@RestResource(path = "byName")
public List<Person> findByName(String name);
}

View File

@@ -14,7 +14,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.view.AbstractView;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@SuppressWarnings({"unchecked"})
public class JsonView extends AbstractView {

View File

@@ -9,7 +9,7 @@ import org.springframework.data.rest.core.Link;
import org.springframework.data.rest.core.SimpleLink;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class Links {

View File

@@ -6,10 +6,11 @@ import java.util.List;
import javax.persistence.EntityManagerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener;
import org.springframework.data.rest.repository.jpa.JpaRepositoryExporter;
@@ -19,10 +20,11 @@ import org.springframework.http.converter.json.MappingJacksonHttpMessageConverte
import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* Base configuration for the Spring Data REST Exporter.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@Configuration
@ImportResource("classpath*:META-INF/spring-data-rest/**/*-export.xml")
public class RepositoryRestConfiguration {
@Autowired
@@ -31,12 +33,17 @@ public class RepositoryRestConfiguration {
JpaRepositoryExporter jpaRepositoryExporter;
@Autowired(required = false)
ConversionService customConversionService;
ConversionService defaultConversionService = new DefaultConversionService();
ConfigurableConversionService defaultConversionService = new DefaultConversionService();
@Autowired(required = false)
List<HttpMessageConverter<?>> httpMessageConverters = new ArrayList<HttpMessageConverter<?>>();
@Autowired(required = false)
ValidatingRepositoryEventListener validatingRepositoryEventListener;
/**
* Either the user's pre-configured {@link ConversionService} or the {@link DefaultConversionService}.
*
* @return
*/
@Bean ConversionService conversionService() {
if (null != customConversionService) {
return customConversionService;
@@ -45,6 +52,11 @@ public class RepositoryRestConfiguration {
}
}
/**
* A list of {@link HttpMessageConverter}s to be used to read incoming data and to write outgoing responses.
*
* @return
*/
@Bean List<HttpMessageConverter<?>> httpMessageConverters() {
if (httpMessageConverters.isEmpty()) {
MappingJacksonHttpMessageConverter json = new MappingJacksonHttpMessageConverter();
@@ -56,6 +68,11 @@ public class RepositoryRestConfiguration {
return httpMessageConverters;
}
/**
* Export any JPA {@link org.springframework.data.repository.Repository} implementations we find.
*
* @return
*/
@Bean JpaRepositoryExporter jpaRepositoryExporter() {
if (null == jpaRepositoryExporter) {
jpaRepositoryExporter = new JpaRepositoryExporter();

View File

@@ -26,6 +26,8 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.Repository;
import org.springframework.data.rest.core.Handler;
@@ -39,6 +41,7 @@ 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.RepositoryQueryMethod;
import org.springframework.data.rest.repository.annotation.RestResource;
import org.springframework.data.rest.repository.context.AfterDeleteEvent;
import org.springframework.data.rest.repository.context.AfterLinkSaveEvent;
import org.springframework.data.rest.repository.context.AfterSaveEvent;
@@ -55,10 +58,12 @@ import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
@@ -69,7 +74,7 @@ import org.springframework.web.context.request.WebRequest;
import org.springframework.web.util.UriComponentsBuilder;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@Controller
public class RepositoryRestController
@@ -90,6 +95,7 @@ public class RepositoryRestController
private MediaType jsonMediaType = MediaType.parseMediaType("application/x-spring-data+json");
private ConversionService conversionService = new DefaultConversionService();
private List<HttpMessageConverter<?>> httpMessageConverters = Collections.emptyList();
private Map<String, Handler<Object, Object>> resourceHandlers = Collections.emptyMap();
private ObjectMapper objectMapper = new ObjectMapper();
@Override public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
@@ -130,6 +136,24 @@ public class RepositoryRestController
return this;
}
public Map<String, Handler<Object, Object>> getResourceHandlers() {
return resourceHandlers;
}
public RepositoryRestController setResourceHandlers(Map<String, Handler<Object, Object>> resourceHandlers) {
this.resourceHandlers = resourceHandlers;
return this;
}
public Map<String, Handler<Object, Object>> resourceHandlers() {
return resourceHandlers;
}
public RepositoryRestController resourceHandlers(Map<String, Handler<Object, Object>> resourceHandlers) {
this.resourceHandlers = resourceHandlers;
return this;
}
public MediaType getUriListMediaType() {
return uriListMediaType;
}
@@ -199,9 +223,12 @@ public class RepositoryRestController
URI baseUri = uriBuilder.build().toUri();
Links links = new Links();
for (RepositoryExporter repoMeta : repositoryExporters) {
for (String name : (Set<String>) repoMeta.repositoryNames()) {
links.add(new SimpleLink(name, buildUri(baseUri, name)));
for (RepositoryExporter repoExporter : repositoryExporters) {
for (String name : (Set<String>) repoExporter.repositoryNames()) {
RepositoryMetadata repoMeta = repoExporter.repositoryMetadataFor(name);
String rel = repoMeta.rel();
URI path = buildUri(baseUri, name);
links.add(new SimpleLink(rel, path));
}
}
@@ -229,13 +256,44 @@ public class RepositoryRestController
while (iter.hasNext()) {
Object o = iter.next();
Serializable id = (Serializable) repoMeta.entityMetadata().idAttribute().get(o);
links.add(new SimpleLink(repository + "." + o.getClass().getSimpleName(),
links.add(new SimpleLink(repoMeta.rel() + "." + o.getClass().getSimpleName(),
buildUri(baseUri, repository, id.toString())));
}
links.add(new SimpleLink(repoMeta.rel() + ".search",
buildUri(baseUri, repository, "search")));
model.addAttribute(STATUS, HttpStatus.OK);
model.addAttribute(RESOURCE, links);
}
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/{repository}/search",
method = RequestMethod.GET,
produces = {
"application/json"
}
)
public void listQueryMethods(UriComponentsBuilder uriBuilder,
@PathVariable String repository,
Model model) {
URI baseUri = uriBuilder.build().toUri();
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
Links links = new Links();
for (Map.Entry<String, RepositoryQueryMethod> entry : ((Map<String, RepositoryQueryMethod>) repoMeta.queryMethods())
.entrySet()) {
links.add(new SimpleLink(repository + "." + entry.getKey(),
buildUri(baseUri, repository, "search", entry.getKey())));
String rel = repoMeta.rel() + "." + entry.getKey();
URI path = buildUri(baseUri, repository, "search", entry.getKey());
RestResource resourceAnno = entry.getValue().method().getAnnotation(RestResource.class);
if (null != resourceAnno) {
path = buildUri(baseUri, repository, "search", resourceAnno.path());
if (StringUtils.hasText(resourceAnno.rel())) {
rel = repoMeta.rel() + "." + resourceAnno.rel();
}
}
links.add(new SimpleLink(rel, path));
}
model.addAttribute(STATUS, HttpStatus.OK);
@@ -266,10 +324,23 @@ public class RepositoryRestController
Object[] paramVals = new Object[paramTypes.length];
for (int i = 0; i < paramVals.length; i++) {
String queryVal = request.getParameter(paramNames[i]);
if (paramTypes[i].isAssignableFrom(String.class)) {
if (String.class.isAssignableFrom(paramTypes[i])) {
// Param type is a String
paramVals[i] = queryVal;
} else {
} else if (Pageable.class.isAssignableFrom(paramTypes[i])) {
// Handle paging
} else if (Sort.class.isAssignableFrom(paramTypes[i])) {
// Handle sorting
} else if (conversionService.canConvert(String.class, paramTypes[i])) {
// There's a converter from String -> param type
paramVals[i] = conversionService.convert(queryVal, paramTypes[i]);
} else {
// Param type isn't a "simple" type or no converter exists, try JSON
try {
paramVals[i] = objectMapper.readValue(queryVal, paramTypes[i]);
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
}
@@ -280,8 +351,10 @@ public class RepositoryRestController
for (Object o : (Collection) result) {
RepositoryMetadata elemRepoMeta = repositoryMetadataFor(o.getClass());
if (null != elemRepoMeta) {
Map<String, Object> dto = extractPropertiesLinkAware(repository, o, elemRepoMeta.entityMetadata(), baseUri);
coll.add(dto);
String id = elemRepoMeta.entityMetadata().idAttribute().get(o).toString();
String rel = elemRepoMeta.rel() + "." + elemRepoMeta.entityMetadata().type().getSimpleName();
URI path = buildUri(baseUri, repository, id);
coll.add(new SimpleLink(rel, path));
} else {
coll.add(o);
}
@@ -292,11 +365,11 @@ public class RepositoryRestController
} else {
RepositoryMetadata elemRepoMeta = repositoryMetadataFor(result.getClass());
if (null != elemRepoMeta) {
Map<String, Object> dto = extractPropertiesLinkAware(repository,
result,
elemRepoMeta.entityMetadata(),
baseUri);
model.addAttribute(RESOURCE, dto);
String id = elemRepoMeta.entityMetadata().idAttribute().get(result).toString();
String rel = elemRepoMeta.rel() + "." + elemRepoMeta.entityMetadata().type().getSimpleName();
URI path = buildUri(baseUri, repository, id);
Link link = new SimpleLink(rel, path);
model.addAttribute(RESOURCE, link);
} else {
model.addAttribute(RESOURCE, result);
}
@@ -387,12 +460,10 @@ public class RepositoryRestController
headers.set("ETag", "\"" + version.toString() + "\"");
}
Map<String, Object> entityDto = extractPropertiesLinkAware(repository,
repoMeta.rel(),
entity,
repoMeta.entityMetadata(),
UriComponentsBuilder.fromUri(baseUri)
.pathSegment(repository, id)
.build()
.toUri());
buildUri(baseUri, repository, id));
addSelfLink(baseUri, entityDto, repository, id);
model.addAttribute(HEADERS, headers);
@@ -549,14 +620,14 @@ public class RepositoryRestController
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(repository + "." + entity.getClass()
.getSimpleName() + "." + attrType.getSimpleName(), uri));
String rel = repository + "." + entity.getClass().getSimpleName() + "." + attrType.getSimpleName();
URI path = buildUri(baseUri, repository, id, property, propValId);
links.add(new SimpleLink(rel, path));
}
} 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);
URI path = buildUri(baseUri, repository, id, property, propValId);
Object oKey = entry.getKey();
String sKey;
if (ClassUtils.isAssignable(oKey.getClass(), String.class)) {
@@ -564,14 +635,14 @@ public class RepositoryRestController
} else {
sKey = conversionService.convert(oKey, String.class);
}
links.add(new SimpleLink(repository + "." + entity.getClass()
.getSimpleName() + "." + sKey, uri));
String rel = repository + "." + entity.getClass().getSimpleName() + "." + sKey;
links.add(new SimpleLink(rel, path));
}
} else {
String propValId = idAttr.get(propVal).toString();
URI uri = buildUri(baseUri, repository, id, property, propValId);
links.add(new SimpleLink(repository + "." + entity.getClass()
.getSimpleName() + "." + property, uri));
String rel = repository + "." + entity.getClass().getSimpleName() + "." + property;
URI path = buildUri(baseUri, repository, id, property, propValId);
links.add(new SimpleLink(rel, path));
}
model.addAttribute(RESOURCE, links);
} else {
@@ -597,12 +668,12 @@ public class RepositoryRestController
"text/uri-list"
}
)
public void updateLinks(final ServerHttpRequest request,
UriComponentsBuilder uriBuilder,
@PathVariable String repository,
@PathVariable String id,
final @PathVariable String property,
final Model model) throws IOException {
public void updatePropertyOfEntity(final ServerHttpRequest request,
UriComponentsBuilder uriBuilder,
@PathVariable String repository,
@PathVariable String id,
final @PathVariable String property,
final Model model) throws IOException {
URI baseUri = uriBuilder.build().toUri();
final RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
@@ -625,7 +696,7 @@ public class RepositoryRestController
@Override public Void handle(Object linkedEntity) {
if (attrMeta.isCollectionLike()) {
Collection c = new ArrayList();
Collection current = (Collection) attrMeta.get(entity);
Collection current = attrMeta.asCollection(entity);
if (request.getMethod() == HttpMethod.POST && null != current) {
c.addAll(current);
}
@@ -633,7 +704,7 @@ public class RepositoryRestController
attrMeta.set(c, entity);
} else if (attrMeta.isSetLike()) {
Set s = new HashSet();
Set current = (Set) attrMeta.get(entity);
Set current = attrMeta.asSet(entity);
if (request.getMethod() == HttpMethod.POST && null != current) {
s.addAll(current);
}
@@ -641,7 +712,7 @@ public class RepositoryRestController
attrMeta.set(s, entity);
} else if (attrMeta.isMapLike()) {
Map m = new HashMap();
Map current = (Map) attrMeta.get(entity);
Map current = attrMeta.asMap(entity);
if (request.getMethod() == HttpMethod.POST && null != current) {
m.putAll(current);
}
@@ -671,7 +742,9 @@ public class RepositoryRestController
}
}
} else if (jsonMediaType.equals(incomingMediaType)) {
final Map<String, List<Map<String, String>>> incoming = readIncoming(request, incomingMediaType, Map.class);
final Map<String, List<Map<String, String>>> incoming = readIncoming(request,
incomingMediaType,
Map.class);
for (Map<String, String> link : incoming.get(LINKS)) {
String sLinkUri = link.get("href");
Object o = resolveTopLevelResource(baseUri, sLinkUri);
@@ -782,6 +855,7 @@ public class RepositoryRestController
Object linkedEntity = linkedRepo.findOne(sChildId);
if (null != linkedEntity) {
Map<String, Object> entityDto = extractPropertiesLinkAware(repository,
linkedRepoMeta.rel(),
linkedEntity,
linkedRepoMeta.entityMetadata(),
baseUri);
@@ -969,7 +1043,8 @@ public class RepositoryRestController
}
@SuppressWarnings({"unchecked"})
private Map<String, Object> extractPropertiesLinkAware(String repository,
private Map<String, Object> extractPropertiesLinkAware(String repoName,
String repoRel,
Object entity,
EntityMetadata<AttributeMetadata> entityMetadata,
URI baseUri) {
@@ -988,7 +1063,7 @@ public class RepositoryRestController
.pathSegment(attrName)
.build()
.toUri();
Link l = new SimpleLink(repository + "." + entity.getClass().getSimpleName() + "." + attrName, uri);
Link l = new SimpleLink(repoRel + "." + entity.getClass().getSimpleName() + "." + attrName, uri);
List<Link> links = (List<Link>) entityDto.get(LINKS);
if (null == links) {
links = new ArrayList<Link>();

View File

@@ -19,7 +19,7 @@ import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolv
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@Configuration
public class RepositoryRestMvcConfiguration {

View File

@@ -12,7 +12,7 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class ServerHttpRequestMethodArgumentResolver implements HandlerMethodArgumentResolver {

View File

@@ -13,7 +13,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.view.AbstractView;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class UriListView extends AbstractView {

View File

@@ -4,13 +4,11 @@
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>org.springframework.data.rest.webmvc.RepositoryRestConfiguration</param-value>
<param-value>
classpath*:META-INF/spring-data-rest/**/*-export.xml
</param-value>
</context-param>
<listener>
@@ -26,7 +24,10 @@
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration</param-value>
<param-value>
org.springframework.data.rest.webmvc.RepositoryRestConfiguration
org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration
</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

View File

@@ -2,12 +2,9 @@ package org.springframework.data.rest.webmvc.spec
import org.codehaus.jackson.map.ObjectMapper
import org.codehaus.jackson.map.ser.CustomSerializerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.support.ClassPathXmlApplicationContext
import org.springframework.data.rest.core.SimpleLink
import org.springframework.data.rest.core.util.FluentBeanSerializer
import org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener
import org.springframework.data.rest.test.webmvc.Address
import org.springframework.data.rest.webmvc.RepositoryRestConfiguration
import org.springframework.data.rest.webmvc.RepositoryRestController
@@ -15,26 +12,25 @@ import org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration
import org.springframework.http.HttpStatus
import org.springframework.http.server.ServletServerHttpRequest
import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.test.context.ContextConfiguration
import org.springframework.mock.web.MockServletConfig
import org.springframework.mock.web.MockServletContext
import org.springframework.transaction.annotation.Transactional
import org.springframework.ui.ExtendedModelMap
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext
import org.springframework.web.util.UriComponentsBuilder
import spock.lang.Shared
import spock.lang.Specification
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@ContextConfiguration(classes = [RepositoryRestConfiguration, RepositoryRestMvcConfiguration, RepositorySpecConfig])
class RepositoryRestControllerSpec extends Specification {
@Shared
UriComponentsBuilder uriBuilder
@Shared
ObjectMapper mapper = new ObjectMapper()
@Autowired
URI baseUri
@Autowired
@Shared
RepositoryRestController controller
MockHttpServletRequest createRequest(String method, String path) {
@@ -46,6 +42,14 @@ class RepositoryRestControllerSpec extends Specification {
}
def setupSpec() {
def appCtx = new ClassPathXmlApplicationContext("classpath*:META-INF/spring-data-rest/**/*-export.xml")
def webAppCtx = new AnnotationConfigWebApplicationContext()
webAppCtx.setServletConfig(new MockServletConfig())
webAppCtx.setServletContext(new MockServletContext())
webAppCtx.setConfigLocations([RepositoryRestConfiguration.name, RepositoryRestMvcConfiguration.name] as String[])
webAppCtx.setParent(appCtx)
webAppCtx.afterPropertiesSet()
controller = webAppCtx.getBean(RepositoryRestController)
uriBuilder = UriComponentsBuilder.fromUriString("http://localhost:8080/data")
def customSerializerFactory = new CustomSerializerFactory()
customSerializerFactory.addSpecificMapping(SimpleLink, new FluentBeanSerializer(SimpleLink))
@@ -68,40 +72,40 @@ class RepositoryRestControllerSpec extends Specification {
when: "adding an entity"
model.clear()
def req = createRequest("POST", "person")
def req = createRequest("POST", "people")
def data = mapper.writeValueAsBytes([name: "John Doe"])
req.content = data
controller.create(new ServletServerHttpRequest(req), uriBuilder, "person", model)
controller.create(new ServletServerHttpRequest(req), uriBuilder, "people", model)
then:
model.status == HttpStatus.CREATED
when: "getting specific entity"
model.clear()
req = createRequest("GET", "person/1")
controller.entity(new ServletServerHttpRequest(req), uriBuilder, "person", "1", model)
req = createRequest("GET", "people/1")
controller.entity(new ServletServerHttpRequest(req), uriBuilder, "people", "1", model)
then:
model.resource?.name == "John Doe"
when: "updating an entity"
model.clear()
req = createRequest("PUT", "person/1")
req = createRequest("PUT", "people/1")
data = mapper.writeValueAsBytes([name: "Johnnie Doe", version: 0])
req.content = data
controller.createOrUpdate(new ServletServerHttpRequest(req), uriBuilder, "person", "1", model)
controller.createOrUpdate(new ServletServerHttpRequest(req), uriBuilder, "people", "1", model)
then:
model.status == HttpStatus.NO_CONTENT
when: "listing available entities"
model.clear()
controller.listEntities(uriBuilder, "person", model)
def personsLinks = model.resource?.links
controller.listEntities(uriBuilder, "people", model)
def peopleLinks = model.resource?.links
then:
model.status == HttpStatus.OK
personsLinks[0].href().toString() == "http://localhost:8080/data/person/1"
peopleLinks[0].href().toString() == "http://localhost:8080/data/people/1"
when: "creating child entity"
model.clear()
@@ -115,18 +119,18 @@ class RepositoryRestControllerSpec extends Specification {
when: "linking child to parent entity"
model.clear()
req = createRequest("POST", "person/1/addresses")
req = createRequest("POST", "people/1/addresses")
req.contentType = "text/uri-list"
data = "http://localhost:8080/data/address/1".bytes
req.content = data
controller.updateLinks(new ServletServerHttpRequest(req), uriBuilder, "person", "1", "addresses", model)
controller.updatePropertyOfEntity(new ServletServerHttpRequest(req), uriBuilder, "people", "1", "addresses", model)
then:
model.status == HttpStatus.CREATED
when: "getting property of entity"
model.clear()
controller.propertyOfEntity(uriBuilder, "person", "1", "addresses", model)
controller.propertyOfEntity(uriBuilder, "people", "1", "addresses", model)
def addrLinks = model.resource?.links
then:
@@ -136,12 +140,3 @@ class RepositoryRestControllerSpec extends Specification {
}
}
@Configuration
class RepositorySpecConfig {
@Bean ValidatingRepositoryEventListener validator() {
new ValidatingRepositoryEventListener()
}
}

View File

@@ -26,7 +26,7 @@ import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class RestBuilder {

View File

@@ -5,7 +5,7 @@ import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@Entity
public class Address {

View File

@@ -3,7 +3,7 @@ package org.springframework.data.rest.test.webmvc;
import org.springframework.data.repository.CrudRepository;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public interface AddressRepository extends CrudRepository<Address, Long> {
}

View File

@@ -9,7 +9,7 @@ import javax.persistence.OneToMany;
import javax.persistence.Version;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@Entity
public class Person {

View File

@@ -7,7 +7,7 @@ import java.util.Map;
import org.springframework.beans.factory.InitializingBean;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class PersonLoader implements InitializingBean {

View File

@@ -4,15 +4,15 @@ import java.util.List;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.repository.annotation.RestPathSegment;
import org.springframework.data.rest.repository.annotation.RestResource;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@RestPathSegment("person")
@RestResource(path = "people", rel = "peeps")
public interface PersonRepository extends CrudRepository<Person, Long> {
@RestPathSegment("byName")
@RestResource(path = "name", rel = "names")
public List<Person> findByName(@Param("name") String name);
}

View File

@@ -8,7 +8,7 @@ import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class PersonValidator implements Validator {

View File

@@ -5,7 +5,7 @@ import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@Entity
public class Profile {

View File

@@ -3,7 +3,7 @@ package org.springframework.data.rest.test.webmvc;
import org.springframework.data.repository.CrudRepository;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public interface ProfileRepository extends CrudRepository<Profile, Long> {
}