Incorporated recent changes to Spring Data Commons and dependent projects that obsoleted the need to manage domain object metadata within Spring Data REST. Required updating to the latest snapshots available for spring-data-commons and spring-data-jpa.

Additional changes include:

* Re-wrote the monolithic Controller into separate controller classes that have a more narrow focus.
* Implemented common functionality as a `HandlerMethodArgumentResolver` rather than as a helper method in a controller class.
* Re-implemented JSONP functionality as an HttpMessageConverter rather than inline within a controller class.
* Updated to Jackson 2 for all JSON handling.
* By relying on spring-data-commons, spring-data-rest now handles all supported Repository types: JPA, MongoDB, and GemFire.

Added support for MongoDB and GemFire repositories by relying on spring-data-commons to provide the metadata rather than maintaining internal metadata information that is store-specific.

Replaced Spock spec tests with JMock unit and integration tests. Started integrating Jetty 8 into the testing so MVC testing can be done against a live server.
This commit is contained in:
Jon Brisbin
2013-01-04 11:13:04 -06:00
parent 269d6f5983
commit 6e4e7da142
242 changed files with 8466 additions and 9335 deletions

View File

@@ -0,0 +1,381 @@
package org.springframework.data.rest.config;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.Assert;
/**
* @author Jon Brisbin
*/
public class RepositoryRestConfiguration {
private URI baseUri = null;
private int defaultPageSize = 20;
private String pageParamName = "page";
private String limitParamName = "limit";
private String sortParamName = "sort";
private String jsonpParamName = "callback";
private String jsonpOnErrParamName = null;
private List<HttpMessageConverter<?>> customConverters = Collections.emptyList();
private Map<Class<?>, Class<?>> typeMappings = Collections.emptyMap();
private MediaType defaultMediaType = MediaType.APPLICATION_JSON;
private boolean dumpErrors = true;
private List<Class<?>> exposeIdsFor = new ArrayList<Class<?>>();
private ResourceMappingConfiguration domainMappings = new ResourceMappingConfiguration();
private ResourceMappingConfiguration repoMappings = new ResourceMappingConfiguration();
/**
* The base URI against which the exporter should calculate its links.
*
* @return The base URI.
*/
public URI getBaseUri() {
return baseUri;
}
/**
* The base URI against which the exporter should calculate its links.
*
* @param baseUri
* The base URI.
*/
public RepositoryRestConfiguration setBaseUri(URI baseUri) {
Assert.notNull(baseUri, "The baseUri cannot be null.");
this.baseUri = baseUri;
return this;
}
/**
* Get the default size of {@link org.springframework.data.domain.Pageable}s. Default is 20.
*
* @return The default page size.
*/
public int getDefaultPageSize() {
return defaultPageSize;
}
/**
* Set the default size of {@link org.springframework.data.domain.Pageable}s.
*
* @param defaultPageSize
* The default page size.
*
* @return {@literal this}
*/
public RepositoryRestConfiguration setDefaultPageSize(int defaultPageSize) {
Assert.isTrue((defaultPageSize > 0), "Page size must be greater than 0.");
this.defaultPageSize = defaultPageSize;
return this;
}
/**
* Get the name of the URL query string parameter that indicates what page to return. Default is 'page'.
*
* @return Name of the query parameter used to indicate the page number to return.
*/
public String getPageParamName() {
return pageParamName;
}
/**
* Set the name of the URL query string parameter that indicates what page to return.
*
* @param pageParamName
* Name of the query parameter used to indicate the page number to return.
*
* @return {@literal this}
*/
public RepositoryRestConfiguration setPageParamName(String pageParamName) {
Assert.notNull(pageParamName, "Page param name cannot be null.");
this.pageParamName = pageParamName;
return this;
}
/**
* Get the name of the URL query string parameter that indicates how many results to return at once. Default is
* 'limit'.
*
* @return Name of the query parameter used to indicate the maximum number of entries to return at a time.
*/
public String getLimitParamName() {
return limitParamName;
}
/**
* Set the name of the URL query string parameter that indicates how many results to return at once.
*
* @param limitParamName
* Name of the query parameter used to indicate the maximum number of entries to return at a time.
*
* @return {@literal this}
*/
public RepositoryRestConfiguration setLimitParamName(String limitParamName) {
Assert.notNull(limitParamName, "Limit param name cannot be null.");
this.limitParamName = limitParamName;
return this;
}
/**
* Get the name of the URL query string parameter that indicates what direction to sort results. Default is 'sort'.
*
* @return Name of the query string parameter used to indicate what field to sort on.
*/
public String getSortParamName() {
return sortParamName;
}
/**
* Set the name of the URL query string parameter that indicates what direction to sort results.
*
* @param sortParamName
* Name of the query string parameter used to indicate what field to sort on.
*
* @return {@literal this}
*/
public RepositoryRestConfiguration setSortParamName(String sortParamName) {
Assert.notNull(sortParamName, "Sort param name cannot be null.");
this.sortParamName = sortParamName;
return this;
}
/**
* Get the list of custom {@link HttpMessageConverter}s to be used to convert user input to objects and visa versa.
*
* @return List of custom {@literal HttpMessageConverter}s.
*/
public List<HttpMessageConverter<?>> getCustomConverters() {
return customConverters;
}
/**
* Set the list of custom {@link HttpMessageConverter}s to be used to convert user input to objects and visa versa.
*
* @param customConverters
* List of custom {@literal HttpMessageConverter}s.
*
* @return {@literal this}
*/
public RepositoryRestConfiguration setCustomConverters(List<HttpMessageConverter<?>> customConverters) {
Assert.notNull(customConverters, "Custom converters list cannot be null.");
this.customConverters = customConverters;
return this;
}
/**
* Get the list of domain type to repository implementation mappings that will help the exporters narrow down the
* correct {@link org.springframework.data.repository.Repository} to return for a given domain type.
*
* @return A {@link Map} of domain type to repository mappings.
*/
public Map<Class<?>, Class<?>> getDomainTypeToRepositoryMappings() {
return typeMappings;
}
/**
* Set the list of domain type to repository implementation mappings that will help the exporters narrow down the
* correct {@link org.springframework.data.repository.Repository} to return for a given domain type.
*
* @param typeMappings
* A {@link Map} of domain type to repository mappings.
*
* @return {@literal this}
*/
public RepositoryRestConfiguration setDomainTypeToRepositoryMappings(Map<Class<?>, Class<?>> typeMappings) {
this.typeMappings = typeMappings;
return this;
}
/**
* Get the name of the URL query string parameter that indicates the name of the javascript function to use as the
* JSONP wrapper for results.
*
* @return Name of the query string parameter used to indicate the JSONP callback function.
*/
public String getJsonpParamName() {
return jsonpParamName;
}
/**
* Set the name of the URL query string parameter that indicates the name of the javascript function to use as the
* JSONP wrapper for results.
*
* @param jsonpParamName
* Name of the query string parameter used to indicate the JSONP callback function.
*
* @return {@literal this}
*/
public RepositoryRestConfiguration setJsonpParamName(String jsonpParamName) {
this.jsonpParamName = jsonpParamName;
return this;
}
/**
* Get the name of the URL query string parameter that indicates the name of the javascript function to use as the
* error handler JSONP wrapper for errors.
*
* @return Name of the query string parameter used to indicate what javascript function to use as the JSONP error
* response.
*/
public String getJsonpOnErrParamName() {
return jsonpOnErrParamName;
}
/**
* Set the name of the URL query string parameter that indicates the name of the javascript function to use as the
* error handler JSONP wrapper for errors.
*
* @param jsonpOnErrParamName
* Name of the query string parameter used to indicate what javascript function to use as the JSONP error
* response.
*
* @return {@literal this}
*/
public RepositoryRestConfiguration setJsonpOnErrParamName(String jsonpOnErrParamName) {
this.jsonpOnErrParamName = jsonpOnErrParamName;
return this;
}
/**
* Get the {@link MediaType} to use as a default when none is specified.
*
* @return Default content type if none has been specified.
*/
public MediaType getDefaultMediaType() {
return defaultMediaType;
}
/**
* Set the {@link MediaType} to use as a default when none is specified.
*
* @param defaultMediaType
* Default content type if none has been specified.
*
* @return {@literal this}
*/
public RepositoryRestConfiguration setDefaultMediaType(MediaType defaultMediaType) {
this.defaultMediaType = defaultMediaType;
return this;
}
/**
* Should exception messages be logged to the body of the response in a JSON object?
*
* @return Flag indicating whether exception messages are logged to the body of the response.
*/
public boolean isDumpErrors() {
return dumpErrors;
}
/**
* Set whether exception messages should be logged to the body of the response as a JSON object.
*
* @param dumpErrors
* Flag indicating whether exception messages are logged to the body of the response.
*
* @return {@literal this}
*/
public RepositoryRestConfiguration setDumpErrors(boolean dumpErrors) {
this.dumpErrors = dumpErrors;
return this;
}
/**
* Start configuration a {@link ResourceMapping} for a specific domain type.
*
* @param domainType
* The {@link Class} of the domain type to configure a mapping for.
*
* @return A new {@link ResourceMapping} for configuring how a domain type is mapped.
*/
public ResourceMapping addResourceMappingForDomainType(Class<?> domainType) {
return domainMappings.addResourceMappingFor(domainType);
}
/**
* Get the {@link ResourceMapping} for a specific domain type.
*
* @param domainType
* The {@link Class} of the domain type.
*
* @return A {@link ResourceMapping} for that domain type or {@literal null} if none exists.
*/
public ResourceMapping getResourceMappingForDomainType(Class<?> domainType) {
return domainMappings.getResourceMappingFor(domainType);
}
public boolean hasResourceMappingForDomainType(Class<?> domainType) {
return domainMappings.hasResourceMappingFor(domainType);
}
public ResourceMappingConfiguration getDomainTypesResourceMappingConfiguration() {
return domainMappings;
}
/**
* Start configuration a {@link ResourceMapping} for a specific repository interface.
*
* @param repositoryInterface
* The {@link Class} of the repository interface to configure a mapping for.
*
* @return A new {@link ResourceMapping} for configuring how a repository interface is mapped.
*/
public ResourceMapping setResourceMappingForRepository(Class<?> repositoryInterface) {
return repoMappings.addResourceMappingFor(repositoryInterface);
}
/**
* Get the {@link ResourceMapping} for a specific repository interface.
*
* @param repositoryInterface
* The {@link Class} of the repository interface.
*
* @return A {@link ResourceMapping} for that repository interface or {@literal null} if none exists.
*/
public ResourceMapping getResourceMappingForRepository(Class<?> repositoryInterface) {
return repoMappings.getResourceMappingFor(repositoryInterface);
}
public boolean hasResourceMappingForRepository(Class<?> repositoryInterface) {
return repoMappings.hasResourceMappingFor(repositoryInterface);
}
public ResourceMapping findRepositoryMappingForPath(String path) {
Class<?> type = repoMappings.findTypeForPath(path);
if(null == type) {
return null;
}
return repoMappings.getResourceMappingFor(type);
}
/**
* Should we expose the ID property for this domain type?
*
* @param domainType
* The domain type we may need to expose the ID for.
*
* @return {@literal true} is the ID is to be exposed, {@literal false} otherwise.
*/
public boolean isIdExposedFor(Class<?> domainType) {
return exposeIdsFor.contains(domainType);
}
/**
* Set the list of domain types for which we will expose the ID value as a normal property.
*
* @param domainTypes
* Array of types to expose IDs for.
*
* @return {@literal this}
*/
public RepositoryRestConfiguration exposeIdsFor(Class<?>... domainTypes) {
Collections.addAll(exposeIdsFor, domainTypes);
return this;
}
}

View File

@@ -0,0 +1,110 @@
package org.springframework.data.rest.config;
import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*;
import java.util.HashMap;
import java.util.Map;
/**
* @author Jon Brisbin
*/
public class ResourceMapping {
private String rel;
private String path;
private boolean exported = true;
private final Map<String, ResourceMapping> resourceMappings = new HashMap<String, ResourceMapping>();
public ResourceMapping() {
}
public ResourceMapping(Class<?> type) {
rel = findRel(type);
path = findPath(type);
exported = findExported(type);
}
public ResourceMapping(String rel, String path) {
this.rel = rel;
this.path = path;
}
public ResourceMapping(String rel, String path, boolean exported) {
this.rel = rel;
this.path = path;
this.exported = exported;
}
public String getRel() {
return rel;
}
public ResourceMapping setRel(String rel) {
this.rel = rel;
return this;
}
public String getPath() {
return path;
}
public ResourceMapping setPath(String path) {
this.path = path;
return this;
}
public boolean isExported() {
return exported;
}
public ResourceMapping setExported(boolean exported) {
this.exported = exported;
return this;
}
public ResourceMapping addResourceMappings(Map<String, ResourceMapping> mappings) {
if(null == mappings) {
return this;
}
resourceMappings.putAll(mappings);
return this;
}
public ResourceMapping addResourceMappingFor(String name) {
ResourceMapping rm = new ResourceMapping();
resourceMappings.put(name, rm);
return rm;
}
public ResourceMapping getResourceMappingFor(String name) {
return resourceMappings.get(name);
}
public boolean hasResourceMappingFor(String name) {
return resourceMappings.containsKey(name);
}
public Map<String, ResourceMapping> getResourceMappings() {
return resourceMappings;
}
public String getNameForPath(String path) {
for(Map.Entry<String, ResourceMapping> mapping : resourceMappings.entrySet()) {
if(mapping.getValue().getPath().equals(path)) {
return mapping.getKey();
}
}
return path;
}
@Override public String toString() {
return "ResourceMapping{" +
"rel='" + rel + '\'' +
", path='" + path + '\'' +
", exported=" + exported +
", resourceMappings=" + resourceMappings +
'}';
}
}

View File

@@ -0,0 +1,45 @@
package org.springframework.data.rest.config;
import java.util.HashMap;
import java.util.Map;
/**
* Manages the {@link ResourceMapping} configurations for any resources being exported. This includes domain entities
* and repositories.
*
* @author Jon Brisbin
*/
public class ResourceMappingConfiguration {
private final Map<Class<?>, ResourceMapping> resourceMappings = new HashMap<Class<?>, ResourceMapping>();
public ResourceMapping addResourceMappingFor(Class<?> type) {
ResourceMapping rm = resourceMappings.get(type);
if(null == rm) {
rm = new ResourceMapping(type);
resourceMappings.put(type, rm);
}
return rm;
}
public ResourceMapping getResourceMappingFor(Class<?> type) {
return resourceMappings.get(type);
}
public boolean hasResourceMappingFor(Class<?> type) {
return resourceMappings.containsKey(type);
}
public Class<?> findTypeForPath(String path) {
if(null == path) {
return null;
}
for(Map.Entry<Class<?>, ResourceMapping> entry : resourceMappings.entrySet()) {
if(path.equals(entry.getValue().getPath())) {
return entry.getKey();
}
}
return null;
}
}

View File

@@ -1,144 +0,0 @@
package org.springframework.data.rest.repository;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
/**
* Encapsulates necessary information about an attribute of a generic entity.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public interface AttributeMetadata {
/**
* Name of the attribute.
*
* @return name of the attribute.
*/
String name();
/**
* The type of this attribute.
*
* @return type of this attribute.
*/
Class<?> type();
/**
* The type of this map's key, if it's map-like.
*
* @return
*/
Class<?> keyType();
/**
* The element type of this attribute, if this attribute is a "plural"-like attribute (a Collection, Map, etc...).
*
* @return Class of element type or {@literal null} if not a plural attribute.
*/
Class<?> elementType();
/**
* Whether this attribute can be nulled or not.
*
* @return
*/
boolean isNullable();
/**
* Can this attribute look like a {@link Collection}?
*
* @return {@literal true} if attribute is a Collection, {@literal false} otherwise.
*/
boolean isCollectionLike();
/**
* Get the path of this attribute as a {@link Collection}.
*
* @param target
* The entity to inspect for this attribute.
*
* @return attribute value as a {@link Collection}
*/
Collection<?> asCollection(Object target);
/**
* Can this attribute look like a {@link Set}?
*
* @return {@literal true} if attribute is a Set, {@literal false} otherwise.
*/
boolean isSetLike();
/**
* Get the path of this attribute as a {@link Set}.
*
* @param target
* The entity to inspect for this attribute.
*
* @return attribute value as a {@link Set}
*/
Set<?> asSet(Object target);
/**
* Can this attribute look like a {@link Map}?
*
* @return {@literal true} if attribute is a Map, {@literal false} otherwise.
*/
boolean isMapLike();
/**
* Get the path of this attribute as a {@link Map}.
*
* @param target
* The entity to inspect for this attribute.
*
* @return attribute value as a {@link Map}
*/
Map asMap(Object target);
/**
* Does this attribute have the given annotation on it?
*
* @param annoType
* The type of annotation to search for.
*
* @return {@literal true} if this annotation exists on this attribute, {@literal false} otherwise.
*/
boolean hasAnnotation(Class<? extends Annotation> annoType);
/**
* Get the given annotation.
*
* @param annoType
* The type of annotation to get.
* @param <A>
*
* @return The annotation, or {@literal null} if it doesn't exist.
*/
<A extends Annotation> A annotation(Class<A> annoType);
/**
* Get the path of this attribute.
*
* @param target
* The entity to inspect for this attribute.
*
* @return attribute value
*/
Object get(Object target);
/**
* Set the path of this attribute.
*
* @param value
* Value to set on this attribute.
* @param target
* The entity to set this attribute's value on.
*
* @return @this
*/
AttributeMetadata set(Object value, Object target);
}

View File

@@ -0,0 +1,66 @@
package org.springframework.data.rest.repository;
import static org.springframework.data.rest.core.util.UriUtils.*;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
/**
* @author Jon Brisbin
*/
public class BaseUriAwareResource<T> extends Resource<T> {
@JsonIgnore
private URI baseUri;
public BaseUriAwareResource() {
}
public BaseUriAwareResource(T content, Link... links) {
super(content, links);
}
public BaseUriAwareResource(T content, Iterable<Link> links) {
super(content, links);
}
public URI getBaseUri() {
return baseUri;
}
public BaseUriAwareResource<T> setBaseUri(URI baseUri) {
this.baseUri = baseUri;
return this;
}
@Override public List<Link> getLinks() {
String baseUriStr = baseUri.toString();
List<Link> links = new ArrayList<Link>();
for(Link l : super.getLinks()) {
if(!l.getHref().startsWith(baseUriStr)) {
links.add(new Link(buildUri(baseUri, l.getHref()).toString(), l.getRel()));
} else {
links.add(l);
}
}
return links;
}
@Override public Link getLink(String rel) {
Link l = super.getLink(rel);
if(null == l) {
return null;
}
if(!l.getHref().startsWith(baseUri.toString())) {
return new Link(buildUri(baseUri, l.getHref()).toString(), l.getRel());
} else {
return l;
}
}
}

View File

@@ -0,0 +1,73 @@
package org.springframework.data.rest.repository;
import static org.springframework.data.rest.core.util.UriUtils.*;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
/**
* @author Jon Brisbin
*/
public class BaseUriAwareResources extends Resources<Resource<?>> {
@JsonIgnore
private URI baseUri;
public BaseUriAwareResources() {
}
public BaseUriAwareResources(Iterable<Resource<?>> content, Link... links) {
super(content, links);
}
public BaseUriAwareResources(Iterable<Resource<?>> content, Iterable<Link> links) {
super(content, links);
}
public URI getBaseUri() {
return baseUri;
}
public BaseUriAwareResources setBaseUri(URI baseUri) {
this.baseUri = baseUri;
return this;
}
@Override public Collection<Resource<?>> getContent() {
List<Resource<?>> resources = new ArrayList<Resource<?>>();
for(Resource<?> resource : super.getContent()) {
if(resource instanceof BaseUriAwareResource) {
resources.add(((BaseUriAwareResource)resource).setBaseUri(baseUri));
} else {
resources.add(new BaseUriAwareResource<Object>(resource.getContent(), resource.getLinks()).setBaseUri(baseUri));
}
}
return resources;
}
@Override public Iterator<Resource<?>> iterator() {
return getContent().iterator();
}
@Override public List<Link> getLinks() {
List<Link> links = new ArrayList<Link>();
for(Link l : super.getLinks()) {
links.add(new Link(buildUri(baseUri, l.getHref()).toString(), l.getRel()));
}
return links;
}
@Override public Link getLink(String rel) {
Link l = super.getLink(rel);
return new Link(buildUri(baseUri, l.getHref()).toString(), l.getRel());
}
}

View File

@@ -1,57 +0,0 @@
package org.springframework.data.rest.repository;
import java.util.Map;
/**
* 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 Type of this domain class.
*/
Class<?> type();
/**
* A Map of attribute metadata keyed on the attribute's name.
*
* @return Attributes that do not involve relationships.
*/
Map<String, A> embeddedAttributes();
/**
* A Map of linked attribute metadata keyed on the attribute's name.
*
* @return Attributes that involve relationships.
*/
Map<String, A> linkedAttributes();
/**
* The {@link AttributeMetadata} representing the ID of the entity.
*
* @return {@link AttributeMetadata} for the ID.
*/
A idAttribute();
/**
* The {@link AttributeMetadata} representing the version of the entity, if applicable.
*
* @return {@link AttributeMetadata} or {@literal null} if no version attributes exists.
*/
A versionAttribute();
/**
* Get {@link AttributeMetadata} by name.
*
* @param name
* The name of the attribute.
*
* @return {@link AttributeMetadata} or {@literal null} if that attribute doesn't exist.
*/
A attribute(String name);
}

View File

@@ -1,62 +0,0 @@
package org.springframework.data.rest.repository;
/**
* @author Jon Brisbin
*/
public class PagingMetadata {
private int number = 0;
private int size = 0;
private int totalPages = 0;
private long totalElements = 0;
public PagingMetadata() {
}
public PagingMetadata(int number,
int size,
int totalPages,
long totalElements) {
this.number = number;
this.size = size;
this.totalPages = totalPages;
this.totalElements = totalElements;
}
public int getNumber() {
return number;
}
public PagingMetadata setNumber(int number) {
this.number = number;
return this;
}
public int getSize() {
return size;
}
public PagingMetadata setSize(int size) {
this.size = size;
return this;
}
public int getTotalPages() {
return totalPages;
}
public PagingMetadata setTotalPages(int totalPages) {
this.totalPages = totalPages;
return this;
}
public long getTotalElements() {
return totalElements;
}
public PagingMetadata setTotalElements(long totalElements) {
this.totalElements = totalElements;
return this;
}
}

View File

@@ -0,0 +1,51 @@
package org.springframework.data.rest.repository;
import java.net.URI;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
/**
* A Spring HATEOAS {@link Resource} subclass that holds a reference to the entity's {@link PersistentEntity} metadata.
*
* @author Jon Brisbin
*/
public class PersistentEntityResource<T> extends BaseUriAwareResource<T> {
@JsonIgnore
private final PersistentEntity<T, ?> persistentEntity;
@SuppressWarnings({"unchecked"})
public static <T> PersistentEntityResource<T> wrap(PersistentEntity persistentEntity,
T obj,
URI baseUri) {
PersistentEntityResource<T> resource = new PersistentEntityResource<T>(persistentEntity, obj);
resource.setBaseUri(baseUri);
return resource;
}
public PersistentEntityResource(PersistentEntity<T, ?> persistentEntity) {
this.persistentEntity = persistentEntity;
}
public PersistentEntityResource(PersistentEntity<T, ?> persistentEntity,
T content,
Link... links) {
super(content, links);
this.persistentEntity = persistentEntity;
}
public PersistentEntityResource(PersistentEntity<T, ?> persistentEntity,
T content,
Iterable<Link> links) {
super(content, links);
this.persistentEntity = persistentEntity;
}
public PersistentEntity<T, ?> getPersistentEntity() {
return persistentEntity;
}
}

View File

@@ -4,10 +4,11 @@ import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.validation.Errors;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
* Exception that is thrown when a Spring {@link org.springframework.validation.Validator} throws an error.
*
* @author Jon Brisbin
*/
public class RepositoryConstraintViolationException
extends DataIntegrityViolationException {
public class RepositoryConstraintViolationException extends DataIntegrityViolationException {
private Errors errors;

View File

@@ -1,181 +0,0 @@
package org.springframework.data.rest.repository;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.support.Repositories;
import org.springframework.data.rest.repository.annotation.RestResource;
import org.springframework.util.StringUtils;
/**
* 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<R extends RepositoryExporter<? super R, M, E>, M extends RepositoryMetadata<E>, E extends EntityMetadata<? extends AttributeMetadata>>
implements ApplicationContextAware,
InitializingBean {
protected ApplicationContext applicationContext;
protected Repositories repositories;
protected Map<String, M> repositoryMetadata;
protected List<String> exportOnlyTheseClasses = Collections.emptyList();
protected Map<Class<?>, Class<?>> domainTypeMappings = new HashMap<Class<?>, Class<?>>();
/**
* 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
* {@link List} of class names to export.
*
* @return @this
*/
@SuppressWarnings({"unchecked"})
public R setExportOnlyTheseClasses(List<String> exportOnlyTheseClasses) {
this.exportOnlyTheseClasses = exportOnlyTheseClasses;
return (R)this;
}
public Map<Class<?>, Class<?>> getDomainTypeMappings() {
return domainTypeMappings;
}
@SuppressWarnings({"unchecked"})
public R setDomainTypeMappings(Map<Class<?>, Class<?>> domainTypeMappings) {
this.domainTypeMappings = domainTypeMappings;
return (R)this;
}
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@SuppressWarnings({"unchecked"})
@Override public void afterPropertiesSet() throws Exception {
}
/**
* Get the list of Repository names being exported.
*
* @return {@link List} of class names to export.
*/
public Set<String> repositoryNames() {
refresh();
return repositoryMetadata.keySet();
}
/**
* Is a Repository being exporter that supports this domain type?
*
* @param domainType
* Type of the domain class.
*
* @return {@literal true} if a Repository is being exported, {@literal false} otherwise.
*/
public boolean hasRepositoryFor(Class<?> domainType) {
refresh();
for(M repoMeta : repositoryMetadata.values()) {
if(repoMeta.domainType().isAssignableFrom(domainType)) {
return true;
}
}
return false;
}
/**
* Get the RepositoryMetadata for the Repository responsible for this domain type.
*
* @param domainType
* Type of the domain class.
*
* @return {@link RepositoryMetadata} instance
*/
public M repositoryMetadataFor(Class<?> domainType) {
refresh();
// Look for an exact match
for(M repoMeta : repositoryMetadata.values()) {
if(repoMeta.domainType() == domainType) {
return repoMeta;
}
}
// Didn't find an exact match, look for domain type mapping
Class<?> repoClass = domainTypeMappings.get(domainType);
if(null != repoClass) {
for(M repoMeta : repositoryMetadata.values()) {
if(repoMeta.repositoryClass() == repoClass) {
return repoMeta;
}
}
}
// Didn't find a mapping, look for a superclass
for(M repoMeta : repositoryMetadata.values()) {
if(repoMeta.domainType().isAssignableFrom(domainType)) {
return repoMeta;
}
}
return null;
}
/**
* Get the {@link RepositoryMetadata} for the Repository exported under the given name.
*
* @param name
* Name a Repository would be exported under.
*
* @return {@link RepositoryMetadata} instance
*/
public M repositoryMetadataFor(String name) {
refresh();
return repositoryMetadata.get(name);
}
protected abstract M createRepositoryMetadata(String name,
Class<?> domainType,
Class<?> repoClass,
Repositories repositories);
@SuppressWarnings({"unchecked"})
public void refresh() {
if(null != repositories) {
return;
}
repositories = new Repositories(applicationContext);
repositoryMetadata = new HashMap<String, M>();
for(Class<?> domainType : repositories) {
if(exportOnlyTheseClasses.isEmpty() || exportOnlyTheseClasses.contains(domainType.getName())) {
Class<?> repoClass = repositories.getRepositoryInformationFor(domainType).getRepositoryInterface();
String name = StringUtils.uncapitalize(repoClass.getSimpleName().replaceAll("Repository", ""));
RestResource resourceAnno = repoClass.getAnnotation(RestResource.class);
boolean exported = true;
if(null != resourceAnno) {
if(StringUtils.hasText(resourceAnno.path())) {
name = resourceAnno.path();
}
exported = resourceAnno.exported();
}
if(exported) {
repositoryMetadata.put(name, createRepositoryMetadata(name, domainType, repoClass, repositories));
}
}
}
}
}

View File

@@ -1,162 +0,0 @@
package org.springframework.data.rest.repository;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
/**
* 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>> {
protected List<RepositoryExporter> repositoryExporters = Collections.emptyList();
/**
* Get a List of {@link RepositoryExporter}s.
*
* @return Exported {@link RepositoryExporter}s.
*/
public List<RepositoryExporter> getRepositoryExporters() {
return repositoryExporters;
}
/**
* Set the List of {@link RepositoryExporter}s.
*
* @param repositoryExporters
* Export this {@link List} of {@link RepositoryExporter}s.
*/
@Autowired(required = false)
public void setRepositoryExporters(List<RepositoryExporter> repositoryExporters) {
this.repositoryExporters = repositoryExporters;
}
/**
* Get a List of {@link RepositoryExporter}s.
*
* @return Exported {@link RepositoryExporter}s.
*/
public List<RepositoryExporter> repositoryExporters() {
return repositoryExporters;
}
/**
* Set the List of {@link RepositoryExporter}s.
*
* @param repositoryExporters
* Export this {@link List} of {@link RepositoryExporter}s.
*
* @return @this
*/
@SuppressWarnings({"unchecked"})
public S repositoryExporters(List<RepositoryExporter> repositoryExporters) {
setRepositoryExporters(repositoryExporters);
return (S)this;
}
/**
* Set the {@link RepositoryExporter}s to use.
*
* @param repositoryExporter
*
* @return
*/
@SuppressWarnings({"unchecked"})
public S repositoryExporters(RepositoryExporter... repositoryExporter) {
setRepositoryExporters(Arrays.asList(repositoryExporter));
return (S)this;
}
/**
* Does a Repository exist for this name?
*
* @param name
*
* @return true
*/
public boolean hasRepositoryMetadataFor(String name) {
try {
return (null != repositoryMetadataFor(name));
} catch(RepositoryNotFoundException ignored) {
return false;
}
}
/**
* Is there a Repository responsible for this domain type?
*
* @param domainType
*
* @return
*/
public boolean hasRepositoryMetadataFor(Class<?> domainType) {
try {
return (null != repositoryMetadataFor(domainType));
} catch(RepositoryNotFoundException ignored) {
return false;
}
}
/**
* Find {@link RepositoryMetadata} for the {@link org.springframework.data.repository.Repository} exported under this
* name.
*
* @param name
* URL segment name.
*
* @return {@link RepositoryMetadata} or {@literal null} if none found.
*/
@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);
}
/**
* Find the {@link RepositoryMetadata} for the {@link org.springframework.data.repository.Repository} responsible for
* the given domain type.
*
* @param domainType
* Type of the domain class.
*
* @return {@link RepositoryMetadata} or {@literal null} if none found.
*/
@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());
}
/**
* Find the {@link RepositoryMetadata} for an attribute of an entity which is possibly managed by a {@link
* org.springframework.data.repository.Repository}.
*
* @param attrMeta
* {@link AttributeMetadata} of a possibly-managed entity.
*
* @return {@link RepositoryMetadata} or {@literal null} if none found.
*/
@SuppressWarnings({"unchecked"})
protected RepositoryMetadata repositoryMetadataFor(AttributeMetadata attrMeta) {
if(null != attrMeta.elementType()) {
return repositoryMetadataFor(attrMeta.elementType());
} else {
return repositoryMetadataFor(attrMeta.type());
}
}
}

View File

@@ -1,88 +0,0 @@
package org.springframework.data.rest.repository;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Map;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.Repository;
import org.springframework.data.rest.repository.invoke.CrudMethod;
import org.springframework.data.rest.repository.invoke.RepositoryQueryMethod;
/**
* Encapsulates necessary metadata about a {@link Repository}.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public interface RepositoryMetadata<E extends EntityMetadata<? extends AttributeMetadata>> {
/**
* The name this {@link Repository} is exported under.
*
* @return Name used in the URL for this Repository.
*/
String name();
/**
* Get the string value to be used as part of a link {@literal rel} attribute.
*
* @return Rel value used in links.
*/
String rel();
/**
* The type of domain object this {@link Repository} is repsonsible for.
*
* @return Type of the domain class.
*/
Class<?> domainType();
/**
* The Class of the {@link Repository} subinterface.
*
* @return Type of the Repository being proxied.
*/
Class<?> repositoryClass();
/**
* The {@link Repository} instance.
*
* @return The actual {@link Repository} instance.
*/
CrudRepository<Object, Serializable> repository();
/**
* The {@link EntityMetadata} associated with the domain type of this {@literal Repository}.
*
* @return EntityMetadata associated with this Repository's domain type.
*/
E entityMetadata();
/**
* Get a {@link org.springframework.data.rest.repository.invoke.RepositoryQueryMethod} by key.
*
* @param key
* Segment of the URL to find a query method for.
*
* @return Found {@link org.springframework.data.rest.repository.invoke.RepositoryQueryMethod} or {@literal null} if
* none found.
*/
RepositoryQueryMethod queryMethod(String key);
/**
* Get a Map of all {@link RepositoryQueryMethod}s, keyed by name.
*
* @return All query methods for this Repository.
*/
Map<String, RepositoryQueryMethod> queryMethods();
/**
* Does this Repository all this method to be exported?
*
* @param method
*
* @return
*/
Boolean exportsMethod(CrudMethod method);
}

View File

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

View File

@@ -0,0 +1,72 @@
package org.springframework.data.rest.repository;
import java.net.URI;
import java.util.HashSet;
import java.util.Set;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.repository.support.DomainClassConverter;
import org.springframework.data.rest.repository.support.RepositoryInformationSupport;
/**
* A {@link ConditionalGenericConverter} that can convert a {@link URI} domain entity.
*
* @author Jon Brisbin
*/
public class UriDomainClassConverter
extends RepositoryInformationSupport
implements ConditionalGenericConverter,
InitializingBean {
private static TypeDescriptor STRING_TYPE = TypeDescriptor.valueOf(String.class);
@Autowired
private DomainClassConverter domainClassConverter;
private Set<ConvertiblePair> convertiblePairs = new HashSet<ConvertiblePair>();
@Override public void afterPropertiesSet() throws Exception {
for(Class<?> domainType : repositories) {
convertiblePairs.add(new ConvertiblePair(URI.class, domainType));
}
}
@Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return URI.class.isAssignableFrom(sourceType.getType())
&& (null != repositories.getPersistentEntity(targetType.getType()));
}
@Override public Set<ConvertiblePair> getConvertibleTypes() {
return convertiblePairs;
}
@Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
PersistentEntity entity = repositories.getPersistentEntity(targetType.getType());
if(null == entity || !domainClassConverter.matches(STRING_TYPE, targetType)) {
throw new ConversionFailedException(
sourceType,
targetType,
source,
new IllegalArgumentException("No PersistentEntity information available for " + targetType.getType())
);
}
URI uri = (URI)source;
String[] parts = uri.getPath().split("/");
if(parts.length < 2) {
throw new ConversionFailedException(
sourceType,
targetType,
source,
new IllegalArgumentException("Cannot resolve URI " + uri + ". Is it local or remote? Only local URIs are resolvable.")
);
}
return domainClassConverter.convert(parts[parts.length - 1], STRING_TYPE, targetType);
}
}

View File

@@ -1,76 +0,0 @@
package org.springframework.data.rest.repository;
import java.io.Serializable;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.core.UriResolver;
import org.springframework.data.rest.core.util.UriUtils;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.util.ClassUtils;
/**
* @author Jon Brisbin
*/
public class UriToDomainObjectUriResolver
extends RepositoryExporterSupport<UriToDomainObjectUriResolver>
implements UriResolver<Object> {
@Autowired(required = false)
private List<ConversionService> conversionServices = Arrays.<ConversionService>asList(new DefaultFormattingConversionService());
public List<ConversionService> getConversionServices() {
return conversionServices;
}
public UriToDomainObjectUriResolver setConversionServices(List<ConversionService> conversionServices) {
this.conversionServices = conversionServices;
return this;
}
@SuppressWarnings({"unchecked"})
@Override public Object resolve(URI baseUri, URI uri) {
URI relativeUri = baseUri.relativize(uri);
Stack<URI> uris = UriUtils.explode(baseUri, relativeUri);
if(uris.size() < 1) {
return null;
}
String repoName = UriUtils.path(uris.get(0));
String sId = UriUtils.path(uris.get(1));
RepositoryMetadata repoMeta = repositoryMetadataFor(repoName);
CrudRepository repo;
if(null == (repo = repoMeta.repository())) {
return null;
}
EntityMetadata entityMeta;
if(null == (entityMeta = repoMeta.entityMetadata())) {
return null;
}
Class<? extends Serializable> idType = (Class<? extends Serializable>)entityMeta.idAttribute().type();
Serializable serId = null;
if(ClassUtils.isAssignable(idType, String.class)) {
serId = sId;
} else {
for(ConversionService cs : conversionServices) {
if(cs.canConvert(String.class, idType)) {
serId = cs.convert(sId, idType);
break;
}
}
}
return repo.findOne(serId);
}
}

View File

@@ -1,8 +1,14 @@
package org.springframework.data.rest.repository;
import static org.springframework.util.ReflectionUtils.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.validation.AbstractErrors;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
@@ -15,16 +21,16 @@ import org.springframework.validation.ObjectError;
*/
public class ValidationErrors extends AbstractErrors {
private String name;
private Object entity;
private EntityMetadata entityMetadata;
private String name;
private Object entity;
private PersistentEntity persistentEntity;
private List<ObjectError> globalErrors = new ArrayList<ObjectError>();
private List<FieldError> fieldErrors = new ArrayList<FieldError>();
public ValidationErrors(String name, Object entity, EntityMetadata entityMetadata) {
public ValidationErrors(String name, Object entity, PersistentEntity persistentEntity) {
this.name = name;
this.entity = entity;
this.entityMetadata = entityMetadata;
this.persistentEntity = persistentEntity;
}
@Override public String getObjectName() {
@@ -58,6 +64,21 @@ public class ValidationErrors extends AbstractErrors {
}
@Override public Object getFieldValue(String field) {
return entityMetadata.attribute(field).get(entity);
PersistentProperty prop = (null != persistentEntity ? persistentEntity.getPersistentProperty(field) : null);
if(null == prop) {
return null;
}
Method getter = prop.getGetter();
if(null != getter) {
return invokeMethod(getter, entity);
}
Field fld = prop.getField();
if(null != fld) {
return getField(fld, entity);
}
return null;
}
}

View File

@@ -1,7 +1,6 @@
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;
@@ -9,11 +8,12 @@ import java.lang.annotation.Target;
/**
* @author Jon Brisbin
*/
@Target({ElementType.METHOD})
@Target({
ElementType.TYPE,
ElementType.FIELD,
ElementType.METHOD
})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleBeforeRenderResource {
Class<?>[] value() default {};
public @interface Description {
String value();
}

View File

@@ -7,9 +7,14 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
* Denotes a component that should handle the {@literal afterDelete} event.
*
* @author Jon Brisbin
*/
@Target({ElementType.METHOD})
@Target({
ElementType.TYPE,
ElementType.METHOD
})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleAfterDelete {

View File

@@ -7,9 +7,14 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Denotes a component that should handle the {@literal afterLinkDelete} event.
*
* @author Jon Brisbin
*/
@Target({ElementType.METHOD})
@Target({
ElementType.TYPE,
ElementType.METHOD
})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleAfterLinkDelete {

View File

@@ -7,9 +7,14 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
* Denotes a component that should handle the {@literal afterLinkSave} event.
*
* @author Jon Brisbin
*/
@Target({ElementType.METHOD})
@Target({
ElementType.TYPE,
ElementType.METHOD
})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleAfterLinkSave {

View File

@@ -7,9 +7,14 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
* Denotes a component that should handle the {@literal afterSave} event.
*
* @author Jon Brisbin
*/
@Target({ElementType.METHOD})
@Target({
ElementType.TYPE,
ElementType.METHOD
})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleAfterSave {

View File

@@ -7,9 +7,14 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
* Denotes a component that should handle the {@literal beforeDelete} event.
*
* @author Jon Brisbin
*/
@Target({ElementType.METHOD})
@Target({
ElementType.TYPE,
ElementType.METHOD
})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleBeforeDelete {

View File

@@ -7,9 +7,14 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Denotes a component that should handle the {@literal beforeLinkDelete} event.
*
* @author Jon Brisbin
*/
@Target({ElementType.METHOD})
@Target({
ElementType.TYPE,
ElementType.METHOD
})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleBeforeLinkDelete {

View File

@@ -7,9 +7,14 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
* Denotes a component that should handle the {@literal beforeLinkSave} event.
*
* @author Jon Brisbin
*/
@Target({ElementType.METHOD})
@Target({
ElementType.TYPE,
ElementType.METHOD
})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleBeforeLinkSave {

View File

@@ -1,19 +0,0 @@
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
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleBeforeRenderResources {
Class<?>[] value() default {};
}

View File

@@ -7,9 +7,14 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
* Denotes a component that should handle the {@literal beforeSave} event.
*
* @author Jon Brisbin
*/
@Target({ElementType.METHOD})
@Target({
ElementType.TYPE,
ElementType.METHOD
})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleBeforeSave {

View File

@@ -10,7 +10,7 @@ import java.lang.annotation.Target;
* 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>
* @author Jon Brisbin
*/
@Target({
ElementType.FIELD,
@@ -21,10 +21,25 @@ import java.lang.annotation.Target;
@Inherited
public @interface RestResource {
/**
* Flag indicating whether this resource is exported at all.
*
* @return {@literal true} if the resource is to be exported, {@literal false} otherwise.
*/
boolean exported() default true;
/**
* The path segment under which this resource is to be exported.
*
* @return A valid path segment.
*/
String path() default "";
/**
* The rel value to use when generating links to this resource.
*
* @return A valid rel value.
*/
String rel() default "";
}

View File

@@ -1,55 +1,51 @@
package org.springframework.data.rest.repository.context;
import java.util.List;
import static org.springframework.core.GenericTypeResolver.*;
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.RepositoryExporter;
import org.springframework.data.rest.repository.RepositoryExporterSupport;
/**
* 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>
* @author Jon Brisbin
*/
public abstract class AbstractRepositoryEventListener<T extends AbstractRepositoryEventListener<? super T>>
extends RepositoryExporterSupport<T>
implements ApplicationListener<RepositoryEvent>,
ApplicationContextAware {
public abstract class AbstractRepositoryEventListener<T> implements ApplicationListener<RepositoryEvent>,
ApplicationContextAware {
private final Class<?> INTERESTED_TYPE = resolveTypeArgument(getClass(), AbstractRepositoryEventListener.class);
protected ApplicationContext applicationContext;
@Override public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Autowired
public void setRepositoryExporters(List<RepositoryExporter> repositoryExporters) {
super.setRepositoryExporters(repositoryExporters);
}
@SuppressWarnings({"unchecked"})
@Override public final void onApplicationEvent(RepositoryEvent event) {
Class<?> srcType = event.getSource().getClass();
if(null != INTERESTED_TYPE && !INTERESTED_TYPE.isAssignableFrom(srcType)) {
return;
}
if(event instanceof BeforeSaveEvent) {
onBeforeSave(event.getSource());
onBeforeSave((T)event.getSource());
} else if(event instanceof AfterSaveEvent) {
onAfterSave(event.getSource());
onAfterSave((T)event.getSource());
} else if(event instanceof BeforeLinkSaveEvent) {
onBeforeLinkSave(event.getSource(), ((BeforeLinkSaveEvent)event).getLinked());
onBeforeLinkSave((T)event.getSource(), ((BeforeLinkSaveEvent)event).getLinked());
} else if(event instanceof AfterLinkSaveEvent) {
onAfterLinkSave(event.getSource(), ((AfterLinkSaveEvent)event).getLinked());
onAfterLinkSave((T)event.getSource(), ((AfterLinkSaveEvent)event).getLinked());
} else if(event instanceof BeforeLinkDeleteEvent) {
onBeforeLinkDelete(event.getSource(), ((BeforeLinkDeleteEvent)event).getLinked());
onBeforeLinkDelete((T)event.getSource(), ((BeforeLinkDeleteEvent)event).getLinked());
} else if(event instanceof AfterLinkDeleteEvent) {
onAfterLinkDelete(event.getSource(), ((AfterLinkDeleteEvent)event).getLinked());
onAfterLinkDelete((T)event.getSource(), ((AfterLinkDeleteEvent)event).getLinked());
} else if(event instanceof BeforeDeleteEvent) {
onBeforeDelete(event.getSource());
onBeforeDelete((T)event.getSource());
} else if(event instanceof AfterDeleteEvent) {
onAfterDelete(event.getSource());
onAfterDelete((T)event.getSource());
}
}
@@ -57,68 +53,80 @@ public abstract class AbstractRepositoryEventListener<T extends AbstractReposito
* Override this method if you are interested in {@literal beforeSave} events.
*
* @param entity
* The entity being saved.
*/
protected void onBeforeSave(Object entity) {
protected void onBeforeSave(T entity) {
}
/**
* Override this method if you are interested in {@literal afterSave} events.
*
* @param entity
* The entity that was just saved.
*/
protected void onAfterSave(Object entity) {
protected void onAfterSave(T entity) {
}
/**
* Override this method if you are interested in {@literal beforeLinkSave} events.
*
* @param parent
* The parent entity to which the child object is linked.
* @param linked
* The linked, child entity.
*/
protected void onBeforeLinkSave(Object parent, Object linked) {
protected void onBeforeLinkSave(T parent, Object linked) {
}
/**
* Override this method if you are interested in {@literal afterLinkSave} events.
*
* @param parent
* The parent entity to which the child object is linked.
* @param linked
* The linked, child entity.
*/
protected void onAfterLinkSave(Object parent, Object linked) {
protected void onAfterLinkSave(T parent, Object linked) {
}
/**
* Override this method if you are interested in {@literal beforeLinkDelete} events.
*
* @param parent
* The parent entity to which the child object is linked.
* @param linked
* The linked, child entity.
*/
protected void onBeforeLinkDelete(Object parent, Object linked) {
protected void onBeforeLinkDelete(T parent, Object linked) {
}
/**
* Override this method if you are interested in {@literal afterLinkDelete} events.
*
* @param parent
* The parent entity to which the child object is linked.
* @param linked
* The linked, child entity.
*/
protected void onAfterLinkDelete(Object parent, Object linked) {
protected void onAfterLinkDelete(T parent, Object linked) {
}
/**
* Override this method if you are interested in {@literal beforeDelete} events.
*
* @param entity
* The entity that is being deleted.
*/
protected void onBeforeDelete(Object entity) {
protected void onBeforeDelete(T entity) {
}
/**
* Override this method if you are interested in {@literal afterDelete} events.
*
* @param entity
* The entity that was just deleted.
*/
protected void onAfterDelete(Object entity) {
protected void onAfterDelete(T entity) {
}
}

View File

@@ -5,8 +5,7 @@ package org.springframework.data.rest.repository.context;
*
* @author Jon Brisbin
*/
public class AfterSaveEvent
extends RepositoryEvent {
public class AfterSaveEvent extends RepositoryEvent {
public AfterSaveEvent(Object source) {
super(source);
}

View File

@@ -15,9 +15,11 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationListener;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.rest.repository.annotation.HandleAfterDelete;
import org.springframework.data.rest.repository.annotation.HandleAfterLinkDelete;
import org.springframework.data.rest.repository.annotation.HandleAfterLinkSave;
import org.springframework.data.rest.repository.annotation.HandleAfterSave;
import org.springframework.data.rest.repository.annotation.HandleBeforeDelete;
import org.springframework.data.rest.repository.annotation.HandleBeforeLinkDelete;
import org.springframework.data.rest.repository.annotation.HandleBeforeLinkSave;
import org.springframework.data.rest.repository.annotation.HandleBeforeSave;
import org.springframework.data.rest.repository.annotation.RepositoryEventHandler;
@@ -96,6 +98,8 @@ public class AnnotatedHandlerBeanPostProcessor implements ApplicationListener<Re
inspect(targetType, bean, method, HandleAfterLinkSave.class, AfterLinkSaveEvent.class);
inspect(targetType, bean, method, HandleBeforeDelete.class, BeforeDeleteEvent.class);
inspect(targetType, bean, method, HandleAfterDelete.class, AfterDeleteEvent.class);
inspect(targetType, bean, method, HandleBeforeLinkDelete.class, BeforeLinkDeleteEvent.class);
inspect(targetType, bean, method, HandleAfterLinkDelete.class, AfterLinkDeleteEvent.class);
}
},
new ReflectionUtils.MethodFilter() {
@@ -129,8 +133,8 @@ public class AnnotatedHandlerBeanPostProcessor implements ApplicationListener<Re
}
for(Class<?> type : targetTypes) {
EventHandlerMethod m = new EventHandlerMethod(type, handler, method);
if(LOG.isInfoEnabled()) {
LOG.info("Annotated handler method found: " + m);
if(LOG.isDebugEnabled()) {
LOG.debug("Annotated handler method found: " + m);
}
handlerMethods.put(eventType, m);
}

View File

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

View File

@@ -0,0 +1,21 @@
package org.springframework.data.rest.repository.context;
/**
* An event to encapsulate an exception occurring anywhere within the REST exporter.
*
* @author Jon Brisbin
*/
public class ExceptionEvent extends RepositoryEvent {
public ExceptionEvent(Throwable t) {
super(t);
}
/**
* Get the source of this exception event.
*
* @return The {@link Throwable} that is the source of this exception event.
*/
public Throwable getException() {
return (Throwable)getSource();
}
}

View File

@@ -0,0 +1,33 @@
package org.springframework.data.rest.repository.context;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.repository.support.Repositories;
/**
* @author Jon Brisbin
*/
public class RepositoriesFactoryBean implements FactoryBean<Repositories>,
ApplicationContextAware {
private ApplicationContext applicationContext;
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override public Repositories getObject() throws Exception {
return new Repositories(applicationContext);
}
@Override public Class<?> getObjectType() {
return Repositories.class;
}
@Override public boolean isSingleton() {
return false;
}
}

View File

@@ -1,16 +1,32 @@
package org.springframework.data.rest.repository.context;
import static org.springframework.beans.factory.BeanFactoryUtils.*;
import static org.springframework.core.annotation.AnnotationUtils.*;
import static org.springframework.util.StringUtils.*;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.repository.RepositoryConstraintViolationException;
import org.springframework.data.rest.repository.ValidationErrors;
import org.springframework.data.rest.repository.annotation.HandleAfterDelete;
import org.springframework.data.rest.repository.annotation.HandleAfterLinkDelete;
import org.springframework.data.rest.repository.annotation.HandleAfterLinkSave;
import org.springframework.data.rest.repository.annotation.HandleAfterSave;
import org.springframework.data.rest.repository.annotation.HandleBeforeDelete;
import org.springframework.data.rest.repository.annotation.HandleBeforeLinkDelete;
import org.springframework.data.rest.repository.annotation.HandleBeforeLinkSave;
import org.springframework.data.rest.repository.annotation.HandleBeforeSave;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
@@ -22,19 +38,31 @@ import org.springframework.validation.Validator;
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class ValidatingRepositoryEventListener
extends AbstractRepositoryEventListener<ValidatingRepositoryEventListener>
extends AbstractRepositoryEventListener<Object>
implements InitializingBean {
private static final Logger LOG = LoggerFactory.getLogger(ValidatingRepositoryEventListener.class);
@SuppressWarnings({"unchecked"})
private static final List<Class<? extends Annotation>> ANNOTATIONS_TO_FIND = Arrays.asList(
HandleBeforeSave.class,
HandleAfterSave.class,
HandleBeforeDelete.class,
HandleAfterDelete.class,
HandleBeforeLinkSave.class,
HandleAfterLinkSave.class,
HandleBeforeLinkDelete.class,
HandleAfterLinkDelete.class
);
@Autowired
private Repositories repositories;
private Multimap<String, Validator> validators = ArrayListMultimap.create();
@Override public void afterPropertiesSet()
throws Exception {
@Override public void afterPropertiesSet() throws Exception {
if(validators.size() == 0) {
for(Map.Entry<String, Validator> entry : BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext,
Validator.class)
.entrySet()) {
for(Map.Entry<String, Validator> entry : beansOfTypeIncludingAncestors(applicationContext,
Validator.class).entrySet()) {
String name = null;
Validator v = entry.getValue();
@@ -42,7 +70,15 @@ public class ValidatingRepositoryEventListener
name = entry.getKey().substring(0, entry.getKey().indexOf("Save") + 4);
} else if(entry.getKey().contains("Delete")) {
name = entry.getKey().substring(0, entry.getKey().indexOf("Delete") + 6);
} else {
Annotation anno;
for(Class<? extends Annotation> annoType : ANNOTATIONS_TO_FIND) {
if(null != (anno = findAnnotation(v.getClass(), annoType))) {
name = uncapitalize(annoType.getSimpleName().substring(6));
}
}
}
if(null != name) {
this.validators.put(name, v);
}
@@ -119,7 +155,7 @@ public class ValidatingRepositoryEventListener
Class<?> domainType = o.getClass();
errors = new ValidationErrors(domainType.getSimpleName(),
o,
repositoryMetadataFor(domainType).entityMetadata());
repositories.getPersistentEntity(domainType));
Collection<Validator> validators = this.validators.get(event);
if(null != validators) {

View File

@@ -21,12 +21,13 @@ public enum CrudMethod {
SAVE_SOME;
/**
* Get an enum from a {@link Method}. Narrow down overriden methods by looking for {@link Iterable} in the first
* Get an enum from a {@link Method}. Narrow down overridden methods by looking for {@link Iterable} in the first
* parameter, which tells us it is a '_SOME' type.
*
* @param m
* The CRUD method from the repository interface.
*
* @return
* @return An enum representing which CRUD operation this method represents.
*/
public static CrudMethod fromMethod(Method m) {
String s = m.getName();
@@ -52,7 +53,7 @@ public enum CrudMethod {
/**
* Turn this enum into a method name.
*
* @return
* @return The method name as a string.
*/
public String toMethodName() {
switch(this) {

View File

@@ -2,38 +2,38 @@ package org.springframework.data.rest.repository.invoke;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.core.MethodParameter;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.repository.support.Methods;
/**
* An abstraction to encapsulate metadata about a repository method.
*
* @author Jon Brisbin
*/
public class RepositoryMethod {
private Method method;
private Class<?>[] paramTypes;
private String[] paramNames;
private boolean pageable = false;
private boolean sortable = false;
private Method method;
private List<MethodParameter> methodParameters = new ArrayList<MethodParameter>();
private List<String> paramNames = new ArrayList<String>();
private boolean pageable = false;
private boolean sortable = false;
public RepositoryMethod(Method method) {
this.method = method;
paramTypes = method.getParameterTypes();
for(Class<?> type : paramTypes) {
if(Pageable.class.isAssignableFrom(type)) {
pageable = true;
}
if(Sort.class.isAssignableFrom(type)) {
sortable = true;
}
}
paramNames = Methods.NAME_DISCOVERER.getParameterNames(method);
Class<?>[] paramTypes = method.getParameterTypes();
String[] paramNames = Methods.NAME_DISCOVERER.getParameterNames(method);
if(null == paramNames) {
paramNames = new String[paramTypes.length];
}
Annotation[][] paramAnnos = method.getParameterAnnotations();
for(int i = 0; i < paramAnnos.length; i++) {
if(paramAnnos[i].length > 0) {
@@ -49,25 +49,65 @@ public class RepositoryMethod {
paramNames[i] = "arg" + i;
}
}
int idx = 0;
for(Class<?> type : paramTypes) {
if(Pageable.class.isAssignableFrom(type)) {
pageable = true;
}
if(Sort.class.isAssignableFrom(type)) {
sortable = true;
}
methodParameters.add(new MethodParameter(method, idx));
idx++;
}
Collections.addAll(this.paramNames, paramNames);
}
public Class<?>[] paramTypes() {
return paramTypes;
/**
* Get the method parameter types.
*
* @return Array of parameter types.
*/
public List<MethodParameter> getParameters() {
return methodParameters;
}
public String[] paramNames() {
/**
* Get the method parameter names.
*
* @return Array of parameter names.
*/
public List<String> getParameterNames() {
return paramNames;
}
public Method method() {
/**
* Get the reflected {@link Method} to invoke.
*
* @return The {@link Method} to invoke.
*/
public Method getMethod() {
return method;
}
public boolean pageable() {
/**
* Flag denoting whether this repository method returns a {@link org.springframework.data.domain.Page} result or not.
*
* @return {@literal true} if this method returns a {@link org.springframework.data.domain.Page}, {@literal false}
* otherwise.
*/
public boolean isPageable() {
return pageable;
}
public boolean sortable() {
/**
* Flag denoting whether this repository method accepts sorting information.
*
* @return {@literal true} if this method accepts a {@link Sort}, {@literal false} otherwise.
*/
public boolean isSortable() {
return sortable;
}

View File

@@ -0,0 +1,219 @@
package org.springframework.data.rest.repository.invoke;
import static org.springframework.util.ReflectionUtils.*;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.core.RepositoryInformation;
/**
* @author Jon Brisbin
*/
public class RepositoryMethodInvoker implements PagingAndSortingRepository<Object, Serializable> {
private final Object repository;
private final Map<String, RepositoryMethod> queryMethods = new HashMap<String, RepositoryMethod>();
private RepositoryMethod saveOne;
private RepositoryMethod saveSome;
private RepositoryMethod findOne;
private RepositoryMethod exists;
private RepositoryMethod findAll;
private RepositoryMethod findAllSorted;
private RepositoryMethod findAllPaged;
private RepositoryMethod findSome;
private RepositoryMethod count;
private RepositoryMethod deleteOne;
private RepositoryMethod deleteOneById;
private RepositoryMethod deleteSome;
private RepositoryMethod deleteAll;
@SuppressWarnings({"unchecked"})
public RepositoryMethodInvoker(Object repository,
RepositoryInformation repoInfo,
final PersistentEntity persistentEntity) {
this.repository = repository;
Class<?> repoType = repoInfo.getRepositoryInterface();
doWithMethods(repoType, new MethodCallback() {
@Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
String name = method.getName();
int cardinality = method.getParameterTypes().length;
Class<?> paramType = (cardinality == 1 ? method.getParameterTypes()[0] : null);
boolean someMethod = (null != paramType && Iterable.class.isAssignableFrom(paramType));
boolean byIdMethod = (null != paramType && paramType == Serializable.class);
boolean sortable = (null != paramType && Sort.class.isAssignableFrom(paramType));
boolean pageable = (null != paramType && Pageable.class.isAssignableFrom(paramType));
RepositoryMethod repoMethod = new RepositoryMethod(method);
if("save".equals(name) && someMethod) {
saveSome = repoMethod;
} else if("save".equals(name)) {
saveOne = repoMethod;
} else if("findOne".equals(name)) {
findOne = repoMethod;
} else if("exists".equals(name)) {
exists = repoMethod;
} else if("findAll".equals(name) && someMethod) {
findSome = repoMethod;
} else if("findAll".equals(name) && sortable) {
findAllSorted = repoMethod;
} else if("findAll".equals(name) && pageable) {
findAllPaged = repoMethod;
} else if("findAll".equals(name)) {
findAll = repoMethod;
} else if("count".equals(name)) {
count = repoMethod;
} else if("delete".equals(name) && byIdMethod) {
deleteOneById = repoMethod;
} else if("delete".equals(name) && someMethod) {
deleteSome = repoMethod;
} else if("delete".equals(name)) {
deleteOne = repoMethod;
} else if("deleteAll".equals(name)) {
deleteAll = repoMethod;
} else {
queryMethods.put(name, repoMethod);
}
}
});
}
@SuppressWarnings({"unchecked"})
@Override public <S extends Object> S save(S entity) {
return (S)invokeMethod(saveOne.getMethod(), repository, entity);
}
public boolean hasSaveOne() {
return null != saveOne;
}
@SuppressWarnings({"unchecked"})
@Override public <S extends Object> Iterable<S> save(Iterable<S> entities) {
return (Iterable<S>)invokeMethod(saveSome.getMethod(), repository, entities);
}
public boolean hasSaveSome() {
return null != saveSome;
}
@Override public Object findOne(Serializable serializable) {
return invokeMethod(findOne.getMethod(), repository, serializable);
}
public boolean hasFindOne() {
return null != findOne;
}
@Override public boolean exists(Serializable serializable) {
return (Boolean)invokeMethod(exists.getMethod(), repository, serializable);
}
public boolean hasExists() {
return null != exists;
}
@SuppressWarnings({"unchecked"})
@Override public Iterable<Object> findAll() {
return (Iterable<Object>)invokeMethod(findAll.getMethod(), repository);
}
public boolean hasFindAll() {
return null != findAll;
}
@SuppressWarnings({"unchecked"})
@Override public Iterable<Object> findAll(Iterable<Serializable> serializables) {
return (Iterable<Object>)invokeMethod(findSome.getMethod(), repository, serializables);
}
public boolean hasFindSome() {
return null != findSome;
}
@SuppressWarnings({"unchecked"})
@Override public Iterable<Object> findAll(Sort sort) {
return (Iterable<Object>)invokeMethod(findAllSorted.getMethod(), repository, sort);
}
public boolean hasFindAllSorted() {
return null != findAllSorted;
}
@SuppressWarnings({"unchecked"})
@Override public Page<Object> findAll(Pageable pageable) {
return (Page<Object>)invokeMethod(findAllPaged.getMethod(), repository, pageable);
}
public boolean hasFindAllPageable() {
return null != findAllPaged;
}
@Override public void delete(Serializable serializable) {
invokeMethod(deleteOneById.getMethod(), repository, serializable);
}
public boolean hasDeleteOneById() {
return null != deleteOneById;
}
@Override public long count() {
return (Long)invokeMethod(count.getMethod(), repository);
}
public boolean hasCount() {
return null != count;
}
@Override public void delete(Object entity) {
invokeMethod(deleteOne.getMethod(), repository, entity);
}
public boolean hasDeleteOne() {
return null != deleteOne;
}
@Override public void delete(Iterable<?> entities) {
invokeMethod(deleteSome.getMethod(), repository, entities);
}
public boolean hasDeleteSome() {
return null != deleteSome;
}
@Override public void deleteAll() {
invokeMethod(deleteAll.getMethod(), repository);
}
public boolean hasDeleteAll() {
return null != deleteAll;
}
public Map<String, RepositoryMethod> getQueryMethods() {
return queryMethods;
}
public RepositoryMethod getRepositoryMethod(String name) {
return queryMethods.get(name);
}
public Object invokeQueryMethod(String name, Object... params) {
RepositoryMethod repoMethod = queryMethods.get(name);
if(null == repoMethod) {
throw new NoSuchMethodError(name);
}
return invokeMethod(repoMethod.getMethod(), repository, params);
}
public Object invokeQueryMethod(RepositoryMethod method, Object... params) {
return invokeMethod(method.getMethod(), repository, params);
}
}

View File

@@ -5,7 +5,7 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.codehaus.jackson.annotate.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.hateoas.Link;
/**

View File

@@ -1,180 +0,0 @@
package org.springframework.data.rest.repository.jpa;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
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.ManyToOne;
import javax.persistence.OneToOne;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.MapAttribute;
import javax.persistence.metamodel.PluralAttribute;
import org.springframework.beans.BeanUtils;
import org.springframework.data.rest.repository.AttributeMetadata;
import org.springframework.util.ReflectionUtils;
/**
* Implementation of {@link AttributeMetadata} for JPA.
*
* @author Jon Brisbin
*/
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<?> keyType() {
return (attribute instanceof MapAttribute
? ((MapAttribute)attribute).getKeyJavaType()
: null);
}
@Override public Class<?> elementType() {
return (attribute instanceof PluralAttribute
? ((PluralAttribute)attribute).getElementType().getJavaType()
: null);
}
@Override public boolean isNullable() {
if(hasAnnotation(ManyToOne.class)) {
return annotation(ManyToOne.class).optional();
}
if(hasAnnotation(OneToOne.class)) {
return annotation(OneToOne.class).optional();
}
return true;
}
@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 boolean hasAnnotation(Class<? extends Annotation> annoType) {
return field.isAnnotationPresent(annoType);
}
@Override public <A extends Annotation> A annotation(Class<A> annoType) {
return field.getAnnotation(annoType);
}
@Override public Object get(Object target) {
if(null != getter) {
return ReflectionUtils.invokeMethod(getter, target);
} else {
return ReflectionUtils.getField(field, target);
}
}
@Override public AttributeMetadata set(Object value, Object target) {
if(null != setter) {
ReflectionUtils.invokeMethod(setter, target, value);
} else {
ReflectionUtils.setField(field, target, value);
}
return this;
}
@Override public String toString() {
return "JpaAttributeMetadata{" +
"name='" + name + '\'' +
", attribute=" + attribute +
", type=" + type +
", field=" + field +
", getter=" + getter +
", setter=" + setter +
'}';
}
}

View File

@@ -1,121 +0,0 @@
package org.springframework.data.rest.repository.jpa;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.PluralAttribute;
import javax.persistence.metamodel.SingularAttribute;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.repository.EntityMetadata;
import org.springframework.data.rest.repository.annotation.RestResource;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* Implementation of {@link EntityMetadata} for JPA.
*
* @author Jon Brisbin
*/
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()));
try {
if(null != entityType.getVersion(Long.class)) {
versionAttribute = new JpaAttributeMetadata(entityType, entityType.getVersion(Long.class));
}
} catch(IllegalArgumentException ignored) {
// No version exists, just ignore it
}
for(Attribute attr : entityType.getAttributes()) {
boolean exported = true;
Field field = ReflectionUtils.findField(type, attr.getJavaMember().getName());
if(null == field) {
continue;
}
RestResource fieldResourceAnno = field.getAnnotation(RestResource.class);
if(null != fieldResourceAnno) {
exported = fieldResourceAnno.exported();
}
if(exported) {
String name = attr.getName();
if(null != fieldResourceAnno && StringUtils.hasText(fieldResourceAnno.path())) {
name = fieldResourceAnno.path();
}
Class<?> attrType = (attr instanceof PluralAttribute
? ((PluralAttribute)attr).getElementType().getJavaType()
: attr.getJavaType());
if(repositories.hasRepositoryFor(attrType)) {
linkedAttributes.put(name, new JpaAttributeMetadata(entityType, attr));
} else {
if((attr instanceof SingularAttribute && ((SingularAttribute)attr).isId())) {
// Don't export the id attribute
continue;
} else if(((attr instanceof SingularAttribute) && ((SingularAttribute)attr).isVersion())
&& (null == fieldResourceAnno || !StringUtils.hasText(fieldResourceAnno.path()))) {
// Don't export the version attribute
continue;
}
embeddedAttributes.put(name, 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;
}
@Override public String toString() {
return "JpaEntityMetadata{" +
"type=" + type +
", idAttribute=" + idAttribute +
", versionAttribute=" + versionAttribute +
", embeddedAttributes=" + embeddedAttributes +
", linkedAttributes=" + linkedAttributes +
'}';
}
}

View File

@@ -1,31 +0,0 @@
package org.springframework.data.rest.repository.jpa;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.repository.RepositoryExporter;
/**
* Implementation of {@link RepositoryExporter} for exporting JPA {@link Repository} subinterfaces.
*
* @author Jon Brisbin
*/
public class JpaRepositoryExporter
extends RepositoryExporter<JpaRepositoryExporter, JpaRepositoryMetadata, JpaEntityMetadata> {
protected EntityManager entityManager;
@PersistenceContext
public void setEntityManager(EntityManager entityManager) {
this.entityManager = entityManager;
}
@SuppressWarnings({"unchecked"})
@Override
protected JpaRepositoryMetadata createRepositoryMetadata(String name, Class<?> domainType, Class<?> repoClass, Repositories repositories) {
return new JpaRepositoryMetadata(name, domainType, repoClass, repositories, entityManager);
}
}

View File

@@ -1,147 +0,0 @@
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.data.repository.CrudRepository;
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.RestResource;
import org.springframework.data.rest.repository.invoke.CrudMethod;
import org.springframework.data.rest.repository.invoke.RepositoryQueryMethod;
import org.springframework.data.rest.repository.support.Methods;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* Implementation of {@link RepositoryMetadata} for JPA.
*
* @author Jon Brisbin
*/
public class JpaRepositoryMetadata implements RepositoryMetadata<JpaEntityMetadata> {
private final String name;
private final Class<?> repoClass;
private final CrudRepository<Object, Serializable> repository;
private final EntityInformation entityInfo;
private final Map<CrudMethod, Boolean> crudMethodExposed = new HashMap<CrudMethod, Boolean>();
private final Map<String, RepositoryQueryMethod> queryMethods = new HashMap<String, RepositoryQueryMethod>();
private String rel;
private JpaEntityMetadata entityMetadata;
@SuppressWarnings({"unchecked"})
public JpaRepositoryMetadata(String name,
Class<?> domainType,
final Class<?> repoClass,
Repositories repositories,
EntityManager entityManager) {
this.name = name;
this.repoClass = repoClass;
this.repository = repositories.getRepositoryFor(domainType);
this.entityInfo = repositories.getEntityInformationFor(domainType);
RestResource resourceAnno = repoClass.getAnnotation(RestResource.class);
if(null != resourceAnno && StringUtils.hasText(resourceAnno.rel())) {
rel = resourceAnno.rel();
} else {
rel = name;
}
for(Method method : repositories.getRepositoryInformationFor(domainType).getQueryMethods()) {
String pathSeg = method.getName();
RestResource methodResourceAnno = method.getAnnotation(RestResource.class);
boolean methodExported = true;
if(null != methodResourceAnno) {
if(StringUtils.hasText(methodResourceAnno.path())) {
pathSeg = methodResourceAnno.path();
}
methodExported = methodResourceAnno.exported();
}
if(methodExported) {
ReflectionUtils.makeAccessible(method);
queryMethods.put(pathSeg, new RepositoryQueryMethod(method));
}
}
ReflectionUtils.doWithMethods(
repoClass,
new ReflectionUtils.MethodCallback() {
@Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
CrudMethod cr = CrudMethod.fromMethod(method);
RestResource rr = method.getAnnotation(RestResource.class);
if(null != rr) {
crudMethodExposed.put(cr, rr.exported());
}
}
},
new ReflectionUtils.MethodFilter() {
@Override public boolean matches(Method method) {
return (null != CrudMethod.fromMethod(method) && Methods.USER_METHODS.matches(method));
}
}
);
Metamodel metamodel = entityManager.getMetamodel();
entityMetadata = new JpaEntityMetadata(repositories, metamodel.entity(entityInfo.getJavaType()));
}
@Override public String name() {
return name;
}
@Override public String rel() {
return rel;
}
@Override public Class<?> domainType() {
return entityMetadata.type();
}
@Override public Class<?> repositoryClass() {
return repoClass;
}
@Override public CrudRepository<Object, Serializable> repository() {
return repository;
}
@Override public JpaEntityMetadata entityMetadata() {
return entityMetadata;
}
@Override public RepositoryQueryMethod queryMethod(String key) {
return queryMethods.get(key);
}
@Override public Map<String, RepositoryQueryMethod> queryMethods() {
return Collections.unmodifiableMap(queryMethods);
}
@Override public Boolean exportsMethod(CrudMethod method) {
Boolean b = crudMethodExposed.get(method);
if(null == b) {
return true;
} else {
return b;
}
}
@Override public String toString() {
return "JpaRepositoryMetadata{" +
"name='" + name + '\'' +
", repoClass=" + repoClass +
", repository=" + repository +
", entityInfo=" + entityInfo +
", queryMethods=" + queryMethods +
", entityMetadata=" + entityMetadata +
'}';
}
}

View File

@@ -0,0 +1,95 @@
package org.springframework.data.rest.repository.json;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.hateoas.Resource;
/**
* @author Jon Brisbin
*/
public class JsonSchema extends Resource<Map<String, JsonSchema.Property>> {
private final String name;
private final String description;
public JsonSchema(String name, String description) {
super(new HashMap<String, Property>());
this.name = name;
this.description = description;
}
public String getName() {
return name;
}
@JsonProperty("properties")
@Override public Map<String, JsonSchema.Property> getContent() {
return super.getContent();
}
public JsonSchema addProperty(String name, Property property) {
getContent().put(name, property);
return this;
}
public boolean isArrayProperty(String name) {
return (getContent().containsKey(name) && getContent().get(name) instanceof ArrayProperty);
}
public ArrayProperty getArrayProperty(String name) {
return (ArrayProperty)getContent().get(name);
}
public static class Property {
private final String type;
private final String description;
private final boolean required;
public Property(String type, String description, boolean required) {
this.type = type;
this.description = description;
this.required = required;
}
public String getType() {
return type;
}
public String getDescription() {
return description;
}
public boolean isRequired() {
return required;
}
}
public static class ArrayProperty extends Property {
private List<Property> items = new ArrayList<Property>();
public ArrayProperty(String type,
String description,
boolean required) {
super(type, description, required);
}
public List<? extends Property> getItems() {
return items;
}
public ArrayProperty setItems(List<Property> items) {
this.items = items;
return this;
}
public <P extends Property> ArrayProperty addItem(P item) {
this.items.add(item);
return this;
}
}
}

View File

@@ -0,0 +1,379 @@
package org.springframework.data.rest.repository.json;
import static org.springframework.beans.BeanUtils.*;
import static org.springframework.data.rest.core.util.UriUtils.*;
import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*;
import java.io.IOException;
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.List;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.BeanWrapper;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.config.RepositoryRestConfiguration;
import org.springframework.data.rest.config.ResourceMapping;
import org.springframework.data.rest.repository.PersistentEntityResource;
import org.springframework.data.rest.repository.UriDomainClassConverter;
import org.springframework.hateoas.Link;
import org.springframework.http.converter.HttpMessageNotReadableException;
/**
* @author Jon Brisbin
*/
public class PersistentEntityJackson2Module extends SimpleModule implements InitializingBean {
private static final Logger LOG = LoggerFactory.getLogger(PersistentEntityJackson2Module.class);
private static final TypeDescriptor URI_TYPE = TypeDescriptor.valueOf(URI.class);
private final ConversionService conversionService;
@Autowired
private Repositories repositories;
@Autowired
private RepositoryRestConfiguration config;
@Autowired
private UriDomainClassConverter uriDomainClassConverter;
public PersistentEntityJackson2Module(ConversionService conversionService) {
super(new Version(1, 1, 0, "BUILD-SNAPSHOT", "org.springframework.data.rest", "jackson-module"));
this.conversionService = conversionService;
addSerializer(new ResourceSerializer());
}
public static boolean maybeAddAssociationLink(Repositories repositories,
RepositoryRestConfiguration config,
URI baseEntityUri,
ResourceMapping propertyMapping,
PersistentProperty persistentProperty,
List<Link> links) {
Class<?> propertyType = persistentProperty.getType();
if(persistentProperty.isCollectionLike() || persistentProperty.isArray()) {
propertyType = persistentProperty.getComponentType();
}
String propertyPath = (null != propertyMapping
? propertyMapping.getPath()
: persistentProperty.getName());
// In case a property mapping is specified but no path is set
if(null == propertyPath) {
propertyPath = persistentProperty.getName();
}
// entityRel + "." +
String propertyRel = (null != propertyMapping
? propertyMapping.getRel()
: propertyPath);
if(repositories.hasRepositoryFor(propertyType)) {
// This is a managed type, generate a Link
RepositoryInformation linkedRepoInfo = repositories.getRepositoryInformationFor(propertyType);
ResourceMapping linkedRepoMapping = getResourceMapping(config, linkedRepoInfo);
if(linkedRepoMapping.isExported()) {
URI uri = buildUri(baseEntityUri, propertyPath);
Link l = new Link(uri.toString(), propertyRel);
links.add(l);
// This is an association. We added a Link.
return true;
}
}
// This is not an association. No Link was added.
return false;
}
@SuppressWarnings({"unchecked"})
@Override public void afterPropertiesSet() throws Exception {
for(Class<?> domainType : repositories) {
addDeserializer(domainType, new ResourceDeserializer(repositories.getPersistentEntity(domainType)));
}
}
private class ResourceDeserializer<T extends Object> extends StdDeserializer<T> {
private final PersistentEntity persistentEntity;
private final Object defaultObject;
private final Map<String, Object> defaultValues = new HashMap<String, Object>();
@SuppressWarnings({"unchecked"})
private ResourceDeserializer(PersistentEntity persistentEntity) {
super(persistentEntity.getType());
this.persistentEntity = persistentEntity;
this.defaultObject = instantiateClass(getValueClass());
final BeanWrapper wrapper = BeanWrapper.create(defaultObject, conversionService);
persistentEntity.doWithProperties(new PropertyHandler() {
@Override public void doWithPersistentProperty(PersistentProperty prop) {
Object defaultValue = wrapper.getProperty(prop);
if(null != defaultValue) {
defaultValues.put(prop.getName(), defaultValue);
}
}
});
}
@SuppressWarnings({"unchecked"})
@Override public T deserialize(JsonParser jp,
DeserializationContext ctxt) throws IOException,
JsonProcessingException {
Object entity = instantiateClass(getValueClass());
BeanWrapper wrapper = BeanWrapper.create(entity, conversionService);
ResourceMapping domainMapping = config.getResourceMappingForDomainType(getValueClass());
for(JsonToken tok = jp.nextToken(); tok != JsonToken.END_OBJECT; tok = jp.nextToken()) {
String name = jp.getCurrentName();
switch(tok) {
case FIELD_NAME: {
if("href".equals(name)) {
URI uri = URI.create(jp.nextTextValue());
TypeDescriptor entityType = TypeDescriptor.forObject(entity);
if(uriDomainClassConverter.matches(URI_TYPE, entityType)) {
entity = uriDomainClassConverter.convert(uri, URI_TYPE, entityType);
}
continue;
}
if("rel".equals(name)) {
// rel is currently ignored
continue;
}
PersistentProperty persistentProperty = persistentEntity.getPersistentProperty(name);
if(null == persistentProperty) {
String errMsg = "Property '" + name + "' not found for entity " + getValueClass().getName();
if(null == domainMapping) {
throw new HttpMessageNotReadableException(errMsg);
}
String propertyName = domainMapping.getNameForPath(name);
if(null == propertyName) {
throw new HttpMessageNotReadableException(errMsg);
}
persistentProperty = persistentEntity.getPersistentProperty(propertyName);
if(null == persistentProperty) {
throw new HttpMessageNotReadableException(errMsg);
}
}
Object val = null;
if("links".equals(name)) {
if((tok = jp.nextToken()) == JsonToken.START_ARRAY) {
while((tok = jp.nextToken()) != JsonToken.END_ARRAY) {
// Advance past the links
}
} else if(tok == JsonToken.VALUE_NULL) {
// skip null value
} else {
throw new HttpMessageNotReadableException(
"Property 'links' is not of array type. Either eliminate this property from the document or make it an array.");
}
continue;
}
if(null == persistentProperty) {
// do nothing
continue;
}
// Try and read the value of this attribute.
// The method of doing that varies based on the type of the property.
if(persistentProperty.isCollectionLike()) {
Class<? extends Collection> ctype = (Class<? extends Collection>)persistentProperty.getType();
Collection c = (Collection)wrapper.getProperty(persistentProperty, ctype, false);
if(null == c || c == Collections.EMPTY_LIST || c == Collections.EMPTY_SET) {
if(Collection.class.isAssignableFrom(ctype)) {
c = new ArrayList();
} else if(Set.class.isAssignableFrom(ctype)) {
c = new HashSet();
}
}
if((tok = jp.nextToken()) == JsonToken.START_ARRAY) {
while((tok = jp.nextToken()) != JsonToken.END_ARRAY) {
Object cval = jp.readValueAs(persistentProperty.getComponentType());
c.add(cval);
}
val = c;
} else if(tok == JsonToken.VALUE_NULL) {
val = null;
} else {
throw new HttpMessageNotReadableException("Cannot read a JSON " + tok + " as a Collection.");
}
} else if(persistentProperty.isMap()) {
Class<? extends Map> mtype = (Class<? extends Map>)persistentProperty.getType();
Map m = (Map)wrapper.getProperty(persistentProperty, mtype, false);
if(null == m || m == Collections.EMPTY_MAP) {
m = new HashMap();
}
if((tok = jp.nextToken()) == JsonToken.START_OBJECT) {
do {
name = jp.getCurrentName();
// TODO resolve domain object from URI
tok = jp.nextToken();
Object mval = jp.readValueAs(persistentProperty.getMapValueType());
m.put(name, mval);
} while((tok = jp.nextToken()) != JsonToken.END_OBJECT);
val = m;
} else if(tok == JsonToken.VALUE_NULL) {
val = null;
} else {
throw new HttpMessageNotReadableException("Cannot read a JSON " + tok + " as a Map.");
}
} else {
if((tok = jp.nextToken()) != JsonToken.VALUE_NULL) {
val = jp.readValueAs(persistentProperty.getType());
}
}
if(null != val) {
Object defaultValue = defaultValues.get(persistentProperty.getName());
if(null == defaultValue || defaultValue != val) {
wrapper.setProperty(persistentProperty, val, false);
}
}
break;
}
}
}
return (T)entity;
}
}
private class ResourceSerializer extends StdSerializer<PersistentEntityResource> {
private ResourceSerializer() {
super(PersistentEntityResource.class);
}
@SuppressWarnings({"unchecked"})
@Override public void serialize(final PersistentEntityResource resource,
final JsonGenerator jgen,
final SerializerProvider provider) throws IOException,
JsonGenerationException {
if(LOG.isDebugEnabled()) {
LOG.debug("Serializing PersistentEntity " + resource.getPersistentEntity());
}
Object obj = resource.getContent();
final PersistentEntity persistentEntity = resource.getPersistentEntity();
final ResourceMapping entityMapping = getResourceMapping(config, persistentEntity);
final RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(persistentEntity.getType());
final ResourceMapping repoMapping = getResourceMapping(config, repoInfo);
final BeanWrapper wrapper = BeanWrapper.create(obj, conversionService);
final Object entityId = wrapper.getProperty(persistentEntity.getIdProperty());
final URI baseEntityUri = buildUri(resource.getBaseUri(),
repoMapping.getPath(),
entityId.toString());
final List<Link> links = new ArrayList<Link>();
// Start with ResourceProcessor-added links
links.addAll(resource.getLinks());
jgen.writeStartObject();
try {
persistentEntity.doWithProperties(new PropertyHandler() {
@Override public void doWithPersistentProperty(PersistentProperty persistentProperty) {
if(persistentProperty.isIdProperty() && !config.isIdExposedFor(persistentEntity.getType())) {
return;
}
ResourceMapping propertyMapping = entityMapping.getResourceMappingFor(persistentProperty.getName());
if(null != propertyMapping && !propertyMapping.isExported()) {
return;
}
if(persistentProperty.isEntity() && maybeAddAssociationLink(repositories,
config,
baseEntityUri,
propertyMapping,
persistentProperty,
links)) {
return;
}
// Property is a normal or non-managed property.
String propertyName = (null != propertyMapping ? propertyMapping.getPath() : persistentProperty.getName());
Object propertyValue = wrapper.getProperty(persistentProperty);
try {
jgen.writeObjectField(propertyName, propertyValue);
} catch(IOException e) {
throw new IllegalStateException(e);
}
}
});
// Add associations as links
persistentEntity.doWithAssociations(new AssociationHandler() {
@Override public void doWithAssociation(Association association) {
PersistentProperty persistentProperty = association.getInverse();
ResourceMapping propertyMapping = entityMapping.getResourceMappingFor(persistentProperty.getName());
if(null != propertyMapping && !propertyMapping.isExported()) {
return;
}
if(maybeAddAssociationLink(repositories,
config,
baseEntityUri,
propertyMapping,
persistentProperty,
links)) {
return;
}
// Association Link was not added, probably because this isn't a managed type. Add value of property inline.
Object propertyValue = wrapper.getProperty(persistentProperty);
try {
jgen.writeObjectField(persistentProperty.getName(), propertyValue);
} catch(IOException e) {
throw new IllegalStateException(e);
}
}
});
jgen.writeArrayFieldStart("links");
for(Link l : links) {
jgen.writeObject(l);
}
jgen.writeEndArray();
} catch(IllegalStateException e) {
throw (IOException)e.getCause();
} finally {
jgen.writeEndObject();
}
}
}
}

View File

@@ -0,0 +1,116 @@
package org.springframework.data.rest.repository.json;
import static org.springframework.data.rest.core.util.UriUtils.*;
import static org.springframework.data.rest.repository.json.PersistentEntityJackson2Module.*;
import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*;
import static org.springframework.util.StringUtils.*;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.validation.constraints.NotNull;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.rest.config.ResourceMapping;
import org.springframework.data.rest.repository.annotation.Description;
import org.springframework.data.rest.repository.support.RepositoryInformationSupport;
import org.springframework.hateoas.Link;
/**
* @author Jon Brisbin
*/
public class PersistentEntityToJsonSchemaConverter
extends RepositoryInformationSupport
implements ConditionalGenericConverter,
InitializingBean {
private static final TypeDescriptor STRING_TYPE = TypeDescriptor.valueOf(String.class);
private static final TypeDescriptor SCHEMA_TYPE = TypeDescriptor.valueOf(JsonSchema.class);
private Set<ConvertiblePair> convertiblePairs = new HashSet<ConvertiblePair>();
@Override public void afterPropertiesSet() throws Exception {
for(Class<?> domainType : repositories) {
convertiblePairs.add(new ConvertiblePair(domainType, JsonSchema.class));
}
}
@Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return (Class.class.isAssignableFrom(sourceType.getType()) && JsonSchema.class.isAssignableFrom(targetType.getType()));
}
@Override public Set<ConvertiblePair> getConvertibleTypes() {
return convertiblePairs;
}
public JsonSchema convert(Class<?> domainType) {
return (JsonSchema)convert(domainType, STRING_TYPE, SCHEMA_TYPE);
}
@SuppressWarnings({"unchecked"})
@Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
PersistentEntity persistentEntity = repositories.getPersistentEntity((Class<?>)source);
final ResourceMapping repoMapping = getResourceMapping(config,
repositories.getRepositoryInformationFor(persistentEntity.getType()));
final ResourceMapping entityMapping = getResourceMapping(config, persistentEntity);
final URI baseEntityUri = buildUri(config.getBaseUri(), repoMapping.getPath(), "{id}");
String entityDesc = persistentEntity.getType().isAnnotationPresent(Description.class)
? ((Description)persistentEntity.getType().getAnnotation(Description.class)).value()
: null;
final JsonSchema jsonSchema = new JsonSchema(persistentEntity.getName(), entityDesc);
persistentEntity.doWithProperties(new PropertyHandler() {
@Override public void doWithPersistentProperty(PersistentProperty persistentProperty) {
Class<?> propertyType = persistentProperty.getType();
String type = uncapitalize(propertyType.getSimpleName());
boolean notNull = (persistentProperty.getField().isAnnotationPresent(Nonnull.class)
|| persistentProperty.getGetter().isAnnotationPresent(Nonnull.class))
|| (persistentProperty.getField().isAnnotationPresent(NotNull.class)
|| persistentProperty.getGetter().isAnnotationPresent(NotNull.class));
String desc = persistentProperty.getField().isAnnotationPresent(Description.class)
? persistentProperty.getField().getAnnotation(Description.class).value()
: persistentProperty.getGetter().isAnnotationPresent(Description.class)
? persistentProperty.getGetter().getAnnotation(Description.class).value()
: null;
JsonSchema.Property property;
if(persistentProperty.isCollectionLike()) {
property = new JsonSchema.ArrayProperty("array", desc, notNull);
} else {
property = new JsonSchema.Property(type, desc, notNull);
}
jsonSchema.addProperty(persistentProperty.getName(), property);
}
});
final List<Link> links = new ArrayList<Link>();
persistentEntity.doWithAssociations(new AssociationHandler() {
@Override public void doWithAssociation(Association association) {
PersistentProperty persistentProperty = association.getInverse();
ResourceMapping propertyMapping = entityMapping.getResourceMappingFor(persistentProperty.getName());
if(null != propertyMapping && !propertyMapping.isExported()) {
return;
}
maybeAddAssociationLink(repositories,
config,
baseEntityUri,
propertyMapping,
persistentProperty,
links);
}
});
jsonSchema.add(links);
return jsonSchema;
}
}

View File

@@ -0,0 +1,100 @@
package org.springframework.data.rest.repository.support;
import static org.springframework.beans.BeanUtils.*;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.BeanWrapper;
import org.springframework.data.repository.support.Repositories;
/**
* @author Jon Brisbin
*/
public class DomainObjectMerger {
private final Map<Class<?>, PersistentEntity> entities = new ConcurrentHashMap<Class<?>, PersistentEntity>();
private final Map<String, Object> defaultValues = new ConcurrentHashMap<String, Object>();
private final Repositories repositories;
private final ConversionService conversionService;
@Autowired
public DomainObjectMerger(Repositories repositories,
ConversionService conversionService) {
this.repositories = repositories;
this.conversionService = conversionService;
}
@SuppressWarnings({"unchecked"})
public void merge(Object from, Object target) {
if(null == from || null == target) {
return;
}
final BeanWrapper fromWrapper = BeanWrapper.create(from, conversionService);
final BeanWrapper targetWrapper = BeanWrapper.create(target, conversionService);
PersistentEntity entity = getPerisistentEntity(target.getClass());
Class<?> clazz = entity.getType();
final String clazzName = clazz.getSimpleName();
entity.doWithProperties(new PropertyHandler() {
@Override public void doWithPersistentProperty(PersistentProperty persistentProperty) {
String mapKey = clazzName + "." + persistentProperty.getName();
Object fromVal = fromWrapper.getProperty(persistentProperty);
Object defaultVal = defaultValues.get(mapKey);
if(null != fromVal && !fromVal.equals(defaultVal)) {
targetWrapper.setProperty(persistentProperty, fromVal);
}
}
});
entity.doWithAssociations(new AssociationHandler() {
@Override public void doWithAssociation(Association association) {
PersistentProperty persistentProperty = association.getInverse();
String mapKey = clazzName + "." + persistentProperty.getName();
Object fromVal = fromWrapper.getProperty(persistentProperty);
Object defaultVal = defaultValues.get(mapKey);
if(null != fromVal && !fromVal.equals(defaultVal)) {
targetWrapper.setProperty(persistentProperty, fromVal);
}
}
});
}
@SuppressWarnings({"unchecked"})
private PersistentEntity getPerisistentEntity(Class<?> clazz) {
PersistentEntity entity = entities.get(clazz);
if(null == entity) {
entity = repositories.getPersistentEntity(clazz);
final String clazzName = clazz.getSimpleName();
final BeanWrapper wrapper = BeanWrapper.create(instantiateClass(clazz), conversionService);
entity.doWithProperties(new PropertyHandler() {
@Override public void doWithPersistentProperty(PersistentProperty persistentProperty) {
Object val = wrapper.getProperty(persistentProperty);
if(null != val) {
defaultValues.put(clazzName + "." + persistentProperty.getName(), val);
}
}
});
entity.doWithAssociations(new AssociationHandler() {
@Override public void doWithAssociation(Association association) {
PersistentProperty persistentProperty = association.getInverse();
Object val = wrapper.getProperty(persistentProperty);
if(null != val) {
defaultValues.put(clazzName + "." + persistentProperty.getName(), val);
}
}
});
entities.put(clazz, entity);
}
return entity;
}
}

View File

@@ -0,0 +1,117 @@
package org.springframework.data.rest.repository.support;
import static org.springframework.data.rest.core.util.UriUtils.*;
import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*;
import java.net.URI;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.config.RepositoryRestConfiguration;
import org.springframework.data.rest.config.ResourceMapping;
import org.springframework.hateoas.Identifiable;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkBuilder;
import org.springframework.hateoas.core.AbstractEntityLinks;
import org.springframework.web.util.UriComponentsBuilder;
/**
* @author Jon Brisbin
*/
public class RepositoryEntityLinks extends AbstractEntityLinks {
private final URI baseUri;
private final Repositories repositories;
private final RepositoryRestConfiguration config;
public RepositoryEntityLinks(URI baseUri,
Repositories repositories,
RepositoryRestConfiguration config) {
this.baseUri = baseUri;
this.repositories = repositories;
this.config = config;
}
@Override public boolean supports(Class<?> delimiter) {
PersistentEntity persistentEntity = repositories.getPersistentEntity(delimiter);
return (null != persistentEntity);
}
@Override public LinkBuilder linkFor(Class<?> type) {
RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(type);
PersistentEntity persistentEntity = repositories.getPersistentEntity(type);
if(null == persistentEntity) {
throw new IllegalArgumentException(type + " is not managed by any repository.");
}
return new PersistentEntityLinkBuilder(baseUri, repoInfo, persistentEntity);
}
@Override public LinkBuilder linkFor(Class<?> type, Object... parameters) {
return linkFor(type);
}
@Override public Link linkToCollectionResource(Class<?> type) {
RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(type);
if(null == repoInfo) {
throw new IllegalArgumentException(type + " is not managed by any repository.");
}
ResourceMapping mapping = getResourceMapping(config, repoInfo);
return linkFor(type).withRel(mapping.getRel());
}
@Override public Link linkToSingleResource(Class<?> type, Object id) {
RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(type);
if(null == repoInfo) {
throw new IllegalArgumentException(type + " is not managed by any repository.");
}
ResourceMapping repoMapping = getResourceMapping(config, repoInfo);
PersistentEntity persistentEntity = repositories.getPersistentEntity(type);
ResourceMapping entityMapping = getResourceMapping(config, persistentEntity);
return linkFor(type).slash(id).withRel(repoMapping.getRel() + "." + entityMapping.getRel());
}
private class PersistentEntityLinkBuilder implements LinkBuilder {
private final UriComponentsBuilder builder;
private final ResourceMapping repoMapping;
private final ResourceMapping entityMapping;
private PersistentEntityLinkBuilder(URI baseUri,
RepositoryInformation repoInfo,
PersistentEntity persistentEntity) {
this.repoMapping = getResourceMapping(config, repoInfo);
this.entityMapping = getResourceMapping(config, persistentEntity);
this.builder = UriComponentsBuilder.fromUri(buildUri(baseUri, repoMapping.getPath()));
}
@Override public LinkBuilder slash(Object object) {
String path = String.format("%s", object);
if(object instanceof PersistentProperty) {
String propName = ((PersistentProperty)object).getName();
if(entityMapping.hasResourceMappingFor(propName)) {
path = entityMapping.getResourceMappingFor(propName).getPath();
}
}
builder.pathSegment(path);
return this;
}
@Override public LinkBuilder slash(Identifiable<?> identifiable) {
return slash(identifiable.getId());
}
@Override public URI toUri() {
return builder.build().toUri();
}
@Override public Link withRel(String rel) {
return new Link(builder.build().toUriString(), rel);
}
@Override public Link withSelfRel() {
return withRel("self");
}
}
}

View File

@@ -0,0 +1,76 @@
package org.springframework.data.rest.repository.support;
import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*;
import static org.springframework.util.ReflectionUtils.*;
import static org.springframework.util.StringUtils.*;
import java.lang.reflect.Method;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.config.RepositoryRestConfiguration;
import org.springframework.data.rest.config.ResourceMapping;
import org.springframework.data.rest.repository.invoke.RepositoryMethod;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* @author Jon Brisbin
*/
public abstract class RepositoryInformationSupport {
protected Repositories repositories;
protected RepositoryRestConfiguration config;
protected MultiValueMap<Class<?>, RepositoryMethod> repositoryMethods = new LinkedMultiValueMap<Class<?>, RepositoryMethod>();
public Repositories getRepositories() {
return repositories;
}
@Autowired
public void setRepositories(Repositories repositories) {
this.repositories = repositories;
for(Class<?> domainType : repositories) {
final RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(domainType);
doWithMethods(repoInfo.getRepositoryInterface(), new MethodCallback() {
@Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
repositoryMethods.add(repoInfo.getRepositoryInterface(), new RepositoryMethod(method));
}
});
}
}
public RepositoryRestConfiguration getConfig() {
return config;
}
@Autowired
public void setConfig(RepositoryRestConfiguration config) {
this.config = config;
}
protected RepositoryInformation findRepositoryInfoFor(String pathSegment) {
if(!hasText(pathSegment)) {
return null;
}
for(Class<?> domainType : repositories) {
RepositoryInformation repoInfo = findRepositoryInfoFor(domainType);
ResourceMapping mapping = getResourceMapping(config, repoInfo);
if(pathSegment.equals(mapping.getPath()) && mapping.isExported()) {
return repoInfo;
}
}
return null;
}
protected RepositoryInformation findRepositoryInfoFor(Class<?> domainType) {
PersistentEntity entity = repositories.getPersistentEntity(domainType);
if(null != entity) {
return repositories.getRepositoryInformationFor(domainType);
}
return null;
}
}

View File

@@ -0,0 +1,130 @@
package org.springframework.data.rest.repository.support;
import static org.springframework.core.annotation.AnnotationUtils.*;
import static org.springframework.util.StringUtils.*;
import java.lang.reflect.Method;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.rest.config.RepositoryRestConfiguration;
import org.springframework.data.rest.config.ResourceMapping;
import org.springframework.data.rest.repository.annotation.RestResource;
/**
* Helper methods to get the default rel and path values or to use values supplied by annotations.
*
* @author Jon Brisbin
*/
public abstract class ResourceMappingUtils {
protected ResourceMappingUtils() {
}
public static String findRel(Class<?> type) {
RestResource anno;
if(null != (anno = findAnnotation(type, RestResource.class))) {
if(hasText(anno.rel())) {
return anno.rel();
}
}
return uncapitalize(type.getSimpleName().replaceAll("Repository", ""));
}
public static String findRel(Method method) {
RestResource anno;
if(null != (anno = findAnnotation(method, RestResource.class))) {
if(hasText(anno.rel())) {
return anno.rel();
}
}
return method.getName();
}
public static String findPath(Class<?> type) {
RestResource anno;
if(null != (anno = findAnnotation(type, RestResource.class))) {
if(hasText(anno.path())) {
return anno.path();
}
}
return uncapitalize(type.getSimpleName().replaceAll("Repository", ""));
}
public static String findPath(Method method) {
RestResource anno;
if(null != (anno = findAnnotation(method, RestResource.class))) {
if(hasText(anno.path())) {
return anno.path();
}
}
return method.getName();
}
public static boolean findExported(Class<?> type) {
RestResource anno;
return null == (anno = findAnnotation(type, RestResource.class)) || anno.exported();
}
public static boolean findExported(Method method) {
RestResource anno;
return null == (anno = findAnnotation(method, RestResource.class)) || anno.exported();
}
public static ResourceMapping getResourceMapping(RepositoryRestConfiguration config,
PersistentEntity persistentEntity) {
if(null == persistentEntity) {
return null;
}
Class<?> domainType = persistentEntity.getType();
ResourceMapping mapping = (null != config ? config.getResourceMappingForDomainType(domainType) : null);
return merge(domainType, mapping);
}
public static ResourceMapping getResourceMapping(RepositoryRestConfiguration config,
RepositoryInformation repoInfo) {
if(null == repoInfo) {
return null;
}
Class<?> repoType = repoInfo.getRepositoryInterface();
ResourceMapping mapping = (null != config ? config.getResourceMappingForRepository(repoType) : null);
return merge(repoType, mapping);
}
public static ResourceMapping merge(Method method, ResourceMapping mapping) {
ResourceMapping defaultMapping = new ResourceMapping(
findRel(method),
findPath(method),
findExported(method)
);
if(null != mapping) {
return new ResourceMapping(
(null != mapping.getRel() ? mapping.getRel() : defaultMapping.getRel()),
(null != mapping.getPath() ? mapping.getPath() : defaultMapping.getPath()),
(mapping.isExported() != defaultMapping.isExported() ? mapping.isExported() : defaultMapping.isExported())
);
}
return defaultMapping;
}
public static ResourceMapping merge(Class<?> type, ResourceMapping mapping) {
ResourceMapping defaultMapping = new ResourceMapping(
findRel(type),
findPath(type),
findExported(type)
);
if(null != mapping) {
return new ResourceMapping(
(null != mapping.getRel() ? mapping.getRel() : defaultMapping.getRel()),
(null != mapping.getPath() ? mapping.getPath() : defaultMapping.getPath()),
(mapping.isExported() != defaultMapping.isExported() ? mapping.isExported() : defaultMapping.isExported()))
.addResourceMappings(mapping.getResourceMappings());
}
return defaultMapping;
}
}

View File

@@ -1,101 +0,0 @@
package org.springframework.data.rest.repository.spec
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.data.rest.repository.RepositoryExporter
import org.springframework.data.rest.repository.annotation.*
import org.springframework.data.rest.repository.context.*
import org.springframework.data.rest.repository.test.ApplicationConfig
import org.springframework.data.rest.repository.test.Person
import org.springframework.test.context.ContextConfiguration
import spock.lang.Specification
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@ContextConfiguration(classes = [ApplicationConfig, EventsApplicationConfig])
class ExtensionsSpec extends Specification {
@Autowired
ApplicationContext appCtx
@Autowired
PersonEventHandler handler
@Autowired
RepositoryExporter exporter
def "responds to ApplicationEvents in annotated handlers"() {
given:
def p = new Person("John Doe")
when:
appCtx.publishEvent(new BeforeSaveEvent(p))
appCtx.publishEvent(new AfterSaveEvent(p))
appCtx.publishEvent(new BeforeLinkSaveEvent(p, new Object()))
appCtx.publishEvent(new AfterLinkSaveEvent(p, new Object()))
appCtx.publishEvent(new BeforeDeleteEvent(p))
appCtx.publishEvent(new AfterDeleteEvent(p))
then:
handler.beforeSave
handler.afterSave
handler.beforeChildSave
handler.afterChildSave
handler.beforeDelete
handler.afterDelete
}
}
@Configuration
class EventsApplicationConfig {
@Bean AnnotatedHandlerBeanPostProcessor handlerBeanPostProcessor() {
return new AnnotatedHandlerBeanPostProcessor();
}
@Bean PersonEventHandler personEventHandler() {
new PersonEventHandler()
}
}
@RepositoryEventHandler(Person)
class PersonEventHandler {
def beforeSave = false
def afterSave = false
def beforeChildSave = false
def afterChildSave = false
def beforeDelete = false
def afterDelete = false
@HandleBeforeSave void handleBeforeSave(Person p) {
beforeSave = true
}
@HandleAfterSave void handleAfterSave(Person p) {
afterSave = true
}
@HandleBeforeLinkSave void handleBeforeChildSave(Person p, Object child) {
beforeChildSave = true
}
@HandleAfterLinkSave void handleAfterChildSave(Person p, Object child) {
afterChildSave = true
}
@HandleBeforeDelete void handleBeforeDelete(Person p) {
beforeDelete = true
}
@HandleAfterDelete void handleAfterDelete(Person p) {
afterDelete = true
}
}

View File

@@ -1,78 +0,0 @@
package org.springframework.data.rest.repository.spec
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.data.rest.repository.RepositoryExporter
import org.springframework.data.rest.repository.RepositoryMetadata
import org.springframework.data.rest.repository.test.ApplicationConfig
import org.springframework.data.rest.repository.test.Family
import org.springframework.data.rest.repository.test.FamilyRepository
import org.springframework.data.rest.repository.test.Person
import org.springframework.data.rest.repository.test.PersonRepository
import org.springframework.test.context.ContextConfiguration
import spock.lang.Specification
import javax.persistence.EntityManager
import javax.persistence.PersistenceContext
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@ContextConfiguration(classes = [ApplicationConfig])
class JpaMetadataSpec extends Specification {
@Autowired
ApplicationContext applicationContext
@PersistenceContext
EntityManager entityManager
@Autowired
List<RepositoryExporter> exporters
RepositoryMetadata metadata(name) {
exporters.find { null != it.repositoryMetadataFor(name) }?.repositoryMetadataFor(name)
}
def "finds repositories in ApplicationContext"() {
when: "find repo by String identifier"
def repo = metadata("person")?.repository()
then:
null != repo
repo instanceof PersonRepository
when: "find repo by domain Class<?>"
repo = metadata(Family)?.repository()
then:
null != repo
repo instanceof FamilyRepository
}
def "provides entity metadata"() {
given:
def personRepo = metadata(Person)?.repository()
def familyRepo = metadata(Family)?.repository()
def johnDoe = personRepo?.save(new Person("John Doe"))
def janeDoe = personRepo?.save(new Person("Jane Doe"))
def doeFamily = familyRepo?.save(new Family(
surname: "Doe",
members: [johnDoe, janeDoe]
))
when:
def personMeta = metadata(Person)?.entityMetadata()
def familyMeta = metadata(Family)?.entityMetadata()
then:
personMeta?.attribute("name")?.get(johnDoe) == "John Doe"
familyMeta?.attribute("surname")?.get(doeFamily) == "Doe"
familyMeta?.attribute("members")?.get(doeFamily)?.size() == 2
personMeta?.embeddedAttributes()?.size() == 1
familyMeta?.linkedAttributes()?.size() == 1
}
}

View File

@@ -0,0 +1,63 @@
package org.springframework.data.rest.config;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*;
import java.lang.reflect.Method;
import org.junit.Test;
import org.springframework.data.domain.Pageable;
import org.springframework.data.rest.repository.domain.jpa.AnnotatedPersonRepository;
import org.springframework.data.rest.repository.domain.jpa.PersonRepository;
import org.springframework.data.rest.repository.domain.jpa.PlainPersonRepository;
/**
* Ensure the {@link ResourceMapping} components convey the correct information.
*
* @author Jon Brisbin
*/
public class ResourceMappingUnitTests {
@Test
public void shouldDetectDefaultRelAndPath() throws Exception {
ResourceMapping mapping = new ResourceMapping(
findRel(PlainPersonRepository.class),
findPath(PlainPersonRepository.class),
findExported(PlainPersonRepository.class)
);
assertThat(mapping.getRel(), is("plainPerson"));
assertThat(mapping.getPath(), is("plainPerson"));
assertThat(mapping.isExported(), is(true));
}
@Test
public void shouldDetectAnnotatedRelAndPath() throws Exception {
ResourceMapping mapping = new ResourceMapping(
findRel(AnnotatedPersonRepository.class),
findPath(AnnotatedPersonRepository.class),
findExported(AnnotatedPersonRepository.class)
);
assertThat(mapping.getRel(), is("people"));
// The path is not set on the annotation so this should be the default from class name.
assertThat(mapping.getPath(), is("annotatedPerson"));
assertThat(mapping.isExported(), is(false));
}
@Test
public void shouldDetectAnnotatedRelAndPathOnMethod() throws Exception {
Method method = PersonRepository.class.getMethod("findByFirstName", String.class, Pageable.class);
ResourceMapping mapping = new ResourceMapping(
findRel(method),
findPath(method),
findExported(method)
);
assertThat(mapping.getRel(), is("firstname"));
assertThat(mapping.getPath(), is("firstname"));
assertThat(mapping.isExported(), is(true));
}
}

View File

@@ -0,0 +1,37 @@
package org.springframework.data.rest.repository;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.config.RepositoryRestConfiguration;
import org.springframework.data.rest.config.ResourceMapping;
import org.springframework.data.rest.repository.domain.jpa.ConfiguredPersonRepository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Tests to check that {@link ResourceMapping}s are handled correctly.
*
* @author Jon Brisbin
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = RepositoryTestsConfig.class)
public class RepositoryRestConfigurationIntegrationTests {
@Autowired
RepositoryRestConfiguration config;
@Test
public void shouldProvideResourceMappingForConfiguredRepository() throws Exception {
ResourceMapping mapping = config.getResourceMappingForRepository(ConfiguredPersonRepository.class);
assertThat(mapping, notNullValue());
assertThat(mapping.getRel(), is("people"));
assertThat(mapping.getPath(), is("people"));
assertThat(mapping.isExported(), is(false));
}
}

View File

@@ -0,0 +1,50 @@
package org.springframework.data.rest.repository;
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.Import;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.config.RepositoryRestConfiguration;
import org.springframework.data.rest.repository.domain.jpa.ConfiguredPersonRepository;
import org.springframework.data.rest.repository.domain.jpa.JpaRepositoryConfig;
import org.springframework.data.rest.repository.domain.jpa.Person;
import org.springframework.data.rest.repository.domain.jpa.PersonRepository;
/**
* @author Jon Brisbin
*/
@Configuration
@Import({JpaRepositoryConfig.class})
public class RepositoryTestsConfig {
@Autowired
private ApplicationContext appCtx;
@Bean public Repositories repositories() {
return new Repositories(appCtx);
}
@Bean public RepositoryRestConfiguration config() {
RepositoryRestConfiguration config = new RepositoryRestConfiguration();
config.addResourceMappingForDomainType(Person.class)
.setRel("person");
config.setResourceMappingForRepository(ConfiguredPersonRepository.class)
.setRel("people")
.setPath("people")
.setExported(false);
config.setResourceMappingForRepository(PersonRepository.class)
.setRel("people")
.setPath("people")
.addResourceMappingFor("findByFirstName")
.setRel("firstname")
.setPath("firstname");
return config;
}
}

View File

@@ -0,0 +1,2 @@
field.name.required = Field {0}.{1} is required.
no.userid = {0}s must be assigned initial userids.

View File

@@ -0,0 +1,73 @@
package org.springframework.data.rest.repository.context;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.rest.repository.domain.jpa.Person;
import org.springframework.data.rest.repository.domain.jpa.PersonRepository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Tests around the {@link org.springframework.context.ApplicationEvent} handling abstractions.
*
* @author Jon Brisbin
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = RepositoryEventTestsConfig.class)
public class RepositoryEventIntegrationTests {
@Autowired
ApplicationContext appCtx;
@Autowired
PersonRepository people;
Person person;
@Before
public void setup() {
person = people.save(new Person("Jane", "Doe"));
}
@Test(expected = RuntimeException.class)
public void shouldDispatchBeforeSave() throws Exception {
appCtx.publishEvent(new BeforeSaveEvent(person));
}
@Test(expected = RuntimeException.class)
public void shouldDispatchAfterSave() throws Exception {
appCtx.publishEvent(new AfterSaveEvent(person));
}
@Test(expected = RuntimeException.class)
public void shouldDispatchBeforeDelete() throws Exception {
appCtx.publishEvent(new BeforeDeleteEvent(person));
}
@Test(expected = RuntimeException.class)
public void shouldDispatchAfterDelete() throws Exception {
appCtx.publishEvent(new AfterDeleteEvent(person));
}
@Test(expected = RuntimeException.class)
public void shouldDispatchBeforeLinkSave() throws Exception {
appCtx.publishEvent(new BeforeLinkSaveEvent(person, new Object()));
}
@Test(expected = RuntimeException.class)
public void shouldDispatchAfterLinkSave() throws Exception {
appCtx.publishEvent(new AfterLinkSaveEvent(person, new Object()));
}
@Test(expected = RuntimeException.class)
public void shouldDispatchBeforeLinkDelete() throws Exception {
appCtx.publishEvent(new BeforeLinkDeleteEvent(person, new Object()));
}
@Test(expected = RuntimeException.class)
public void shouldDispatchAfterLinkDelete() throws Exception {
appCtx.publishEvent(new AfterLinkDeleteEvent(person, new Object()));
}
}

View File

@@ -0,0 +1,29 @@
package org.springframework.data.rest.repository.context;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.rest.repository.RepositoryTestsConfig;
import org.springframework.data.rest.repository.domain.jpa.AnnotatedPersonEventHandler;
import org.springframework.data.rest.repository.domain.jpa.PersonBeforeSaveHandler;
/**
* @author Jon Brisbin
*/
@Configuration
@Import({RepositoryTestsConfig.class})
public class RepositoryEventTestsConfig {
@Bean public PersonBeforeSaveHandler personBeforeSaveHandler() {
return new PersonBeforeSaveHandler();
}
@Bean public AnnotatedPersonEventHandler beforeSaveHandler() {
return new AnnotatedPersonEventHandler();
}
@Bean public AnnotatedHandlerBeanPostProcessor annotatedHandlerBeanPostProcessor() {
return new AnnotatedHandlerBeanPostProcessor();
}
}

View File

@@ -0,0 +1,29 @@
package org.springframework.data.rest.repository.context;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.rest.repository.RepositoryConstraintViolationException;
import org.springframework.data.rest.repository.domain.jpa.Person;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Tests to check the {@link org.springframework.validation.Validator} integration.
*
* @author Jon Brisbin
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ValidatorTestsConfig.class)
public class ValidatorIntegrationTests {
@Autowired
ApplicationContext appCtx;
@Test(expected = RepositoryConstraintViolationException.class)
public void shouldValidateLastName() throws Exception {
appCtx.publishEvent(new BeforeSaveEvent(new Person()));
}
}

View File

@@ -0,0 +1,19 @@
package org.springframework.data.rest.repository.context;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.rest.repository.RepositoryTestsConfig;
/**
* @author Jon Brisbin
*/
@Configuration
@Import({RepositoryTestsConfig.class})
public class ValidatorTestsConfig {
@Bean public ValidatingRepositoryEventListener validatingListener() {
return new ValidatingRepositoryEventListener();
}
}

View File

@@ -0,0 +1,41 @@
package org.springframework.data.rest.repository.domain.jpa;
import org.springframework.data.rest.repository.annotation.HandleAfterDelete;
import org.springframework.data.rest.repository.annotation.HandleAfterLinkDelete;
import org.springframework.data.rest.repository.annotation.HandleAfterLinkSave;
import org.springframework.data.rest.repository.annotation.HandleAfterSave;
import org.springframework.data.rest.repository.annotation.HandleBeforeDelete;
import org.springframework.data.rest.repository.annotation.HandleBeforeLinkDelete;
import org.springframework.data.rest.repository.annotation.HandleBeforeLinkSave;
import org.springframework.data.rest.repository.annotation.HandleBeforeSave;
import org.springframework.data.rest.repository.annotation.RepositoryEventHandler;
/**
* @author Jon Brisbin
*/
@RepositoryEventHandler(Person.class)
public class AnnotatedPersonEventHandler {
@HandleAfterDelete
@HandleAfterSave
public void handleAfter(Person p) {
throw new RuntimeException();
}
@HandleAfterLinkDelete
@HandleAfterLinkSave
public void handleAfterLink(Person p, Object o) {
throw new RuntimeException();
}
@HandleBeforeDelete
@HandleBeforeSave
public void handleBefore(Person p) {
throw new RuntimeException();
}
@HandleBeforeLinkDelete
@HandleBeforeLinkSave
public void handleBeforeLink(Person p, Object o) {
throw new RuntimeException();
}
}

View File

@@ -0,0 +1,13 @@
package org.springframework.data.rest.repository.domain.jpa;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.repository.annotation.RestResource;
/**
* A repository to manage {@link org.springframework.data.rest.repository.domain.jpa.Person}s.
*
* @author Jon Brisbin
*/
@RestResource(rel = "people", exported = false)
public interface AnnotatedPersonRepository extends CrudRepository<Person, Long> {
}

View File

@@ -0,0 +1,11 @@
package org.springframework.data.rest.repository.domain.jpa;
import org.springframework.data.repository.CrudRepository;
/**
* A repository to manage {@link Person}s.
*
* @author Jon Brisbin
*/
public interface ConfiguredPersonRepository extends CrudRepository<Person, Long> {
}

View File

@@ -1,13 +1,14 @@
package org.springframework.data.rest.repository.test;
package org.springframework.data.rest.repository.domain.jpa;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.rest.repository.jpa.JpaRepositoryExporter;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaDialect;
@@ -23,10 +24,16 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
* @author Jon Brisbin
*/
@Configuration
@ComponentScan(basePackageClasses = {ApplicationConfig.class})
@ComponentScan(basePackageClasses = {JpaRepositoryConfig.class})
@EnableJpaRepositories
@EnableTransactionManagement
public class ApplicationConfig {
public class JpaRepositoryConfig {
@Bean public MessageSource messageSource() {
ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
ms.setBasename("org.springframework.data.rest.repository.ValidationErrors");
return ms;
}
@Bean public DataSource dataSource() {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
@@ -42,7 +49,6 @@ public class ApplicationConfig {
factory.setJpaVendorAdapter(vendorAdapter);
factory.setPackagesToScan(getClass().getPackage().getName());
factory.setDataSource(dataSource());
factory.setPersistenceXmlLocation("/JpaMetadataSpec-persistence.xml");
factory.afterPropertiesSet();
@@ -58,9 +64,4 @@ public class ApplicationConfig {
txManager.setEntityManagerFactory(entityManagerFactory());
return txManager;
}
@Bean public JpaRepositoryExporter jpaRepositoryExporter() {
return new JpaRepositoryExporter();
}
}

View File

@@ -0,0 +1,82 @@
package org.springframework.data.rest.repository.domain.jpa;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.PrePersist;
/**
* An entity that represents a person.
*
* @author Jon Brisbin
*/
@Entity
public class Person {
@Id @GeneratedValue private Long id;
private String firstName;
private String lastName;
@OneToMany
private List<Person> siblings = Collections.emptyList();
private Date created;
public Person() {
}
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public Long getId() {
return id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public Person addSibling(Person p) {
if(siblings == Collections.EMPTY_LIST) {
siblings = new ArrayList<Person>();
}
siblings.add(p);
return this;
}
public List<Person> getSiblings() {
return siblings;
}
public void setSiblings(List<Person> siblings) {
this.siblings = siblings;
}
public Date getCreated() {
return created;
}
@PrePersist
private void prePersist() {
this.created = Calendar.getInstance().getTime();
}
}

View File

@@ -0,0 +1,12 @@
package org.springframework.data.rest.repository.domain.jpa;
import org.springframework.data.rest.repository.context.AbstractRepositoryEventListener;
/**
* @author Jon Brisbin
*/
public class PersonBeforeSaveHandler extends AbstractRepositoryEventListener<Person> {
@Override protected void onBeforeSave(Person person) {
throw new RuntimeException();
}
}

View File

@@ -0,0 +1,20 @@
package org.springframework.data.rest.repository.domain.jpa;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Jon Brisbin
*/
@Component
public class PersonLoader implements InitializingBean {
@Autowired
private PlainPersonRepository people;
@Override public void afterPropertiesSet() throws Exception {
people.save(new Person("John", "Doe"));
}
}

View File

@@ -0,0 +1,31 @@
package org.springframework.data.rest.repository.domain.jpa;
import static org.springframework.util.ClassUtils.*;
import static org.springframework.util.StringUtils.*;
import org.springframework.data.rest.repository.annotation.HandleBeforeSave;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
/**
* A test {@link Validator} that checks for non-blank names.
*
* @author Jon Brisbin
*/
@Component
@HandleBeforeSave
public class PersonNameValidator implements Validator {
@Override public boolean supports(Class<?> clazz) {
return isAssignable(clazz, Person.class);
}
@Override public void validate(Object target, Errors errors) {
Person p = (Person)target;
if(!hasText(p.getLastName())) {
errors.rejectValue("lastName", "blank", "Last name cannot be blank");
}
}
}

View File

@@ -0,0 +1,34 @@
package org.springframework.data.rest.repository.domain.jpa;
import java.util.Date;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.convert.ISO8601DateConverter;
import org.springframework.data.rest.repository.annotation.ConvertWith;
import org.springframework.data.rest.repository.annotation.RestResource;
/**
* A repository to manage {@link Person}s.
*
* @author Jon Brisbin
*/
@RestResource(rel = "people", path = "people")
public interface PersonRepository extends PagingAndSortingRepository<Person, Long> {
@RestResource(rel = "firstname", path = "firstname")
public Page<Person> findByFirstName(@Param("firstName") String firstName, Pageable pageable);
public Page<Person> findByCreatedGreaterThan(@Param("date") Date date, Pageable pageable);
@Query("select p from Person p where p.created > :date")
public Page<Person> findByCreatedUsingISO8601Date(@Param("date")
@ConvertWith(
ISO8601DateConverter.class)
Date date,
Pageable pageable);
}

View File

@@ -0,0 +1,11 @@
package org.springframework.data.rest.repository.domain.jpa;
import org.springframework.data.repository.CrudRepository;
/**
* A repository to manage {@link Person}s.
*
* @author Jon Brisbin
*/
public interface PlainPersonRepository extends CrudRepository<Person, Long> {
}

View File

@@ -0,0 +1,30 @@
package org.springframework.data.rest.repository.domain.mongodb;
import java.net.UnknownHostException;
import com.mongodb.Mongo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
/**
* @author Jon Brisbin
*/
@Configuration
@ComponentScan(basePackageClasses = {MongoDbRepositoryConfig.class})
@EnableMongoRepositories
public class MongoDbRepositoryConfig {
@Bean public MongoDbFactory mongoDbFactory() throws UnknownHostException {
return new SimpleMongoDbFactory(new Mongo("localhost"), "spring-data-rest");
}
@Bean public MongoTemplate mongoTemplate() throws UnknownHostException {
return new MongoTemplate(mongoDbFactory());
}
}

View File

@@ -0,0 +1,47 @@
package org.springframework.data.rest.repository.domain.mongodb;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
/**
* @author Jon Brisbin
*/
@Document
public class Profile {
@Id private String id;
private String name;
private String type;
public Profile() {
}
public Profile(String id, String name, String type) {
this.id = id;
this.name = name;
this.type = type;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public Profile setName(String name) {
this.name = name;
return this;
}
public String getType() {
return type;
}
public Profile setType(String type) {
this.type = type;
return this;
}
}

View File

@@ -0,0 +1,20 @@
package org.springframework.data.rest.repository.domain.mongodb;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Jon Brisbin
*/
@Component
public class ProfileLoader implements InitializingBean {
@Autowired
private ProfileRepository profiles;
@Override public void afterPropertiesSet() throws Exception {
profiles.save(new Profile("jdoe", "jdoe", "account"));
}
}

View File

@@ -0,0 +1,12 @@
package org.springframework.data.rest.repository.domain.mongodb;
import org.bson.types.ObjectId;
import org.springframework.data.repository.CrudRepository;
/**
* Repository for managing {@link Profile}s in MongoDB.
*
* @author Jon Brisbin
*/
public interface ProfileRepository extends CrudRepository<Profile, ObjectId> {
}

View File

@@ -0,0 +1,70 @@
package org.springframework.data.rest.repository.invoke;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.data.domain.Pageable;
import org.springframework.data.rest.convert.ISO8601DateConverter;
import org.springframework.data.rest.repository.domain.jpa.PersonRepository;
import org.springframework.format.support.DefaultFormattingConversionService;
/**
* @author Jon Brisbin
*/
public class MethodParameterConversionServiceUnitTests {
static final SimpleDateFormat ISO8601_FMT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
static final String[] DATE_S = new String[]{"2010-01-01T12:00:00-0600"};
static final Date DATE_D;
static {
try {
DATE_D = ISO8601_FMT.parse(DATE_S[0]);
} catch(ParseException e) {
throw new IllegalStateException(e);
}
}
MethodParameter findByCreatedGreaterThan;
MethodParameter findByCreatedUsingISO8601Date;
@Before
public void setup() throws NoSuchMethodException {
findByCreatedGreaterThan = new MethodParameter(PersonRepository.class.getMethod("findByCreatedGreaterThan",
Date.class,
Pageable.class), 0);
findByCreatedUsingISO8601Date = new MethodParameter(PersonRepository.class.getMethod("findByCreatedUsingISO8601Date",
Date.class,
Pageable.class), 0);
}
@SuppressWarnings({"deprecation"})
@Test
public void shouldConvertDateParameterUsingDefaultConverter() throws Exception {
ConfigurableConversionService cs = new DefaultFormattingConversionService();
MethodParameterConversionService conversionService = new MethodParameterConversionService(cs);
String dateStr = "01/01/2010";
assertThat(conversionService.canConvert(String.class, findByCreatedGreaterThan), is(true));
assertThat((Date)conversionService.convert(dateStr, findByCreatedGreaterThan), is(new Date(dateStr)));
}
@Test
public void shouldConvertDateParameterUsingSpecificConverter() throws Exception {
ConfigurableConversionService cs = new DefaultFormattingConversionService();
cs.addConverter(ISO8601DateConverter.INSTANCE);
MethodParameterConversionService conversionService = new MethodParameterConversionService(cs);
assertThat(conversionService.canConvert(String.class, findByCreatedUsingISO8601Date), is(true));
assertThat((Date)conversionService.convert(DATE_S, findByCreatedUsingISO8601Date), is(DATE_D));
}
}

View File

@@ -0,0 +1,73 @@
package org.springframework.data.rest.repository.invoke;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.springframework.util.ReflectionUtils.*;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.domain.Pageable;
import org.springframework.data.rest.repository.domain.jpa.PersonRepository;
import org.springframework.data.rest.repository.support.Methods;
import org.springframework.util.ReflectionUtils;
/**
* Tests to verify the integrity of the {@link RepositoryMethod} abstraction.
*
* @author Jon Brisbin
*/
public class RepositoryMethodUnitTests {
Map<String, RepositoryMethod> methods = new HashMap<String, RepositoryMethod>();
RepositoryMethod method;
@Before
public void setup() {
doWithMethods(PersonRepository.class,
new ReflectionUtils.MethodCallback() {
@Override public void doWith(Method method) throws IllegalArgumentException,
IllegalAccessException {
String name = method.getName();
RepositoryMethod repoMethod = new RepositoryMethod(method);
methods.put(name, repoMethod);
}
},
Methods.USER_METHODS);
method = methods.get("findByFirstName");
}
@Test
public void shouldFindSimpleQueryMethods() throws Exception {
assertThat(method, notNullValue());
}
@Test
public void shouldFindPageableInformationOnMethod() throws Exception {
assertThat(method, notNullValue());
assertThat(method.isPageable(), is(true));
}
@Test
public void shouldNotFindSortInformationOnMethod() throws Exception {
assertThat(method, notNullValue());
assertThat(method.isSortable(), is(false));
}
@Test
public void shouldProvideParameterClassTypes() throws Exception {
assertThat(method, notNullValue());
assertThat(method.getParameters().get(0).getParameterType(), is(typeCompatibleWith(String.class)));
assertThat(method.getParameters().get(1).getParameterType(), is(typeCompatibleWith(Pageable.class)));
}
@Test
public void shouldProvideParameterNames() throws Exception {
assertThat(method, notNullValue());
assertThat(method.getParameterNames(), contains("firstName", "arg1"));
}
}

View File

@@ -1,57 +0,0 @@
package org.springframework.data.rest.repository.test;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@Entity
public class Family {
@Id
@GeneratedValue
private Long id;
private String surname;
@OneToMany
private List<Person> members;
public Family() {
}
public Family(String surname) {
this.surname = surname;
}
public Long getId() {
return id;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
public List<Person> getMembers() {
return members;
}
public void setMembers(List<Person> members) {
this.members = members;
}
@Override public String toString() {
return "Family{" +
"id=" + id +
", surname='" + surname + '\'' +
", members=" + members +
'}';
}
}

View File

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

View File

@@ -1,44 +0,0 @@
package org.springframework.data.rest.repository.test;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
* @author Jon Brisbin
*/
@Entity
public class Person {
@Id
@GeneratedValue
private Long id;
private String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

View File

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

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="org.springframework.data.rest.repository.spec.PersonEventHandler"/>
<bean class="org.springframework.data.rest.repository.spec.PersonRenderHandler"/>
<bean class="org.springframework.data.rest.repository.context.AnnotatedHandlerRepositoryEventListener">
<property name="basePackage" value="org.springframework.data.rest.repository.spec"/>
</bean>
</beans>

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0">
<persistence-unit name="jpa.sample">
<class>org.springframework.data.rest.repository.test.Person</class>
<class>org.springframework.data.rest.repository.test.Family</class>
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/>
<property name="hibernate.connection.url" value="jdbc:hsqldb:mem:spring"/>
<property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver"/>
<property name="hibernate.connection.username" value="sa"/>
<property name="hibernate.connection.password" value=""/>
<property name="hibernate.hbm2ddl.auto" value="create-drop"/>
</properties>
</persistence-unit>
</persistence>

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.0.xsd">
<jdbc:embedded-database id="dataSource" type="HSQL"/>
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="generateDdl" value="true"/>
<property name="database" value="HSQL"/>
</bean>
</property>
<property name="persistenceUnitName" value="jpa.sample"/>
<property name="persistenceXmlLocation" value="/JpaMetadataSpec-persistence.xml"/>
</bean>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<jpa:repositories base-package="org.springframework.data.rest.repository.test"/>
<bean class="org.springframework.data.rest.repository.jpa.JpaRepositoryExporter"/>
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>
</beans>