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:
@@ -0,0 +1,285 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import static org.springframework.data.rest.core.util.UriUtils.*;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.validation.ConstraintViolationException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.core.convert.ConversionFailedException;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.data.repository.core.RepositoryInformation;
|
||||
import org.springframework.data.repository.support.DomainClassConverter;
|
||||
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.RepositoryConstraintViolationException;
|
||||
import org.springframework.data.rest.repository.invoke.MethodParameterConversionService;
|
||||
import org.springframework.data.rest.repository.support.ResourceMappingUtils;
|
||||
import org.springframework.data.rest.webmvc.support.BaseUriLinkBuilder;
|
||||
import org.springframework.data.rest.webmvc.support.ConstraintViolationExceptionMessage;
|
||||
import org.springframework.data.rest.webmvc.support.ExceptionMessage;
|
||||
import org.springframework.data.rest.webmvc.support.JsonpResponse;
|
||||
import org.springframework.data.rest.webmvc.support.RepositoryConstraintViolationExceptionMessage;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.LinkBuilder;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.Resources;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class AbstractRepositoryRestController implements ApplicationContextAware {
|
||||
|
||||
static final Resource<?> EMPTY_RESOURCE = new Resource<Object>(Collections.emptyList());
|
||||
static final Resources<Resource<?>> EMPTY_RESOURCES = new Resources<Resource<?>>(Collections.<Resource<?>>emptyList());
|
||||
static final Iterable<Resource<?>> EMPTY_RESOURCE_LIST = Collections.emptyList();
|
||||
static final TypeDescriptor STRING_TYPE = TypeDescriptor.valueOf(String.class);
|
||||
protected final Logger LOG = LoggerFactory.getLogger(getClass());
|
||||
protected final Repositories repositories;
|
||||
protected final RepositoryRestConfiguration config;
|
||||
protected final DomainClassConverter domainClassConverter;
|
||||
protected final ConversionService conversionService;
|
||||
protected final MethodParameterConversionService methodParameterConversionService;
|
||||
protected ApplicationContext applicationContext;
|
||||
|
||||
@Autowired
|
||||
public AbstractRepositoryRestController(Repositories repositories,
|
||||
RepositoryRestConfiguration config,
|
||||
DomainClassConverter domainClassConverter,
|
||||
ConversionService conversionService) {
|
||||
this.repositories = repositories;
|
||||
this.config = config;
|
||||
this.domainClassConverter = domainClassConverter;
|
||||
this.conversionService = conversionService;
|
||||
this.methodParameterConversionService = new MethodParameterConversionService(conversionService);
|
||||
}
|
||||
|
||||
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@ExceptionHandler({
|
||||
NullPointerException.class
|
||||
})
|
||||
@ResponseBody
|
||||
public ResponseEntity<?> handleNPE(NullPointerException npe) {
|
||||
return errorResponse(npe, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
@ExceptionHandler({
|
||||
ResourceNotFoundException.class
|
||||
})
|
||||
@ResponseBody
|
||||
public ResponseEntity<?> handleNotFound() {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
@ExceptionHandler({
|
||||
NoSuchMethodError.class,
|
||||
HttpRequestMethodNotSupportedException.class
|
||||
})
|
||||
@ResponseBody
|
||||
public ResponseEntity<?> handleNoSuchMethod() {
|
||||
return errorResponse(null, HttpStatus.METHOD_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
@ExceptionHandler({
|
||||
HttpMessageNotReadableException.class,
|
||||
HttpMessageNotWritableException.class
|
||||
})
|
||||
@ResponseBody
|
||||
public ResponseEntity<ExceptionMessage> handleNotReadable(HttpMessageNotReadableException e) {
|
||||
return badRequest(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle failures commonly thrown from code tries to read incoming data and convert or cast it to the right type.
|
||||
*
|
||||
* @param t
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws java.io.IOException
|
||||
*/
|
||||
@ExceptionHandler({
|
||||
InvocationTargetException.class,
|
||||
IllegalArgumentException.class,
|
||||
ClassCastException.class,
|
||||
ConversionFailedException.class
|
||||
})
|
||||
@ResponseBody
|
||||
public ResponseEntity<ExceptionMessage> handleMiscFailures(Throwable t) {
|
||||
return badRequest(t);
|
||||
}
|
||||
|
||||
@ExceptionHandler({
|
||||
ConstraintViolationException.class
|
||||
})
|
||||
@ResponseBody
|
||||
public ResponseEntity handleConstraintViolationException(ConstraintViolationException cve) {
|
||||
return response(null,
|
||||
new ConstraintViolationExceptionMessage(cve, applicationContext),
|
||||
HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
@ExceptionHandler({
|
||||
RepositoryConstraintViolationException.class
|
||||
})
|
||||
@ResponseBody
|
||||
public ResponseEntity handleRepositoryConstraintViolationException(RepositoryConstraintViolationException rcve) {
|
||||
return response(null,
|
||||
new RepositoryConstraintViolationExceptionMessage(rcve, applicationContext),
|
||||
HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a 409 Conflict in case of concurrent modification.
|
||||
*
|
||||
* @param ex
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@ExceptionHandler({
|
||||
OptimisticLockingFailureException.class,
|
||||
DataIntegrityViolationException.class
|
||||
})
|
||||
@ResponseBody
|
||||
public ResponseEntity handleConflict(Exception ex) {
|
||||
return errorResponse(null, ex, HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
protected <T> ResponseEntity<T> notFound() {
|
||||
return notFound(null, null);
|
||||
}
|
||||
|
||||
protected <T> ResponseEntity<T> notFound(HttpHeaders headers, T body) {
|
||||
return response(headers, body, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
protected <T extends Throwable> ResponseEntity<ExceptionMessage> badRequest(T throwable) {
|
||||
return badRequest(null, throwable);
|
||||
}
|
||||
|
||||
protected <T extends Throwable> ResponseEntity<ExceptionMessage> badRequest(HttpHeaders headers, T throwable) {
|
||||
return errorResponse(headers, throwable, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
public <T extends Throwable> ResponseEntity<ExceptionMessage> internalServerError(T throwable) {
|
||||
return internalServerError(null, throwable);
|
||||
}
|
||||
|
||||
public <T extends Throwable> ResponseEntity<ExceptionMessage> internalServerError(HttpHeaders headers, T throwable) {
|
||||
return errorResponse(headers, throwable, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
public <T extends Throwable> ResponseEntity<ExceptionMessage> errorResponse(T throwable,
|
||||
HttpStatus status) {
|
||||
return errorResponse(null, throwable, status);
|
||||
}
|
||||
|
||||
public <T extends Throwable> ResponseEntity<ExceptionMessage> errorResponse(HttpHeaders headers,
|
||||
T throwable,
|
||||
HttpStatus status) {
|
||||
LOG.error(throwable.getMessage(), throwable);
|
||||
return response(headers, new ExceptionMessage(throwable), status);
|
||||
}
|
||||
|
||||
public <T> ResponseEntity<T> response(HttpHeaders headers, T body, HttpStatus status) {
|
||||
HttpHeaders hdrs = new HttpHeaders();
|
||||
if(null != headers) {
|
||||
hdrs.putAll(headers);
|
||||
}
|
||||
return new ResponseEntity<T>(body, hdrs, status);
|
||||
}
|
||||
|
||||
public <R extends Resource<?>> ResponseEntity<Resource<?>> resourceResponse(HttpHeaders headers,
|
||||
R resource,
|
||||
HttpStatus status) {
|
||||
HttpHeaders hdrs = new HttpHeaders();
|
||||
if(null != headers) {
|
||||
hdrs.putAll(headers);
|
||||
}
|
||||
return new ResponseEntity<Resource<?>>(resource, hdrs, status);
|
||||
}
|
||||
|
||||
protected <T> JsonpResponse<T> jsonpWrapResponse(RepositoryRestRequest repoRequest,
|
||||
T response,
|
||||
HttpStatus status) {
|
||||
return jsonpWrapResponse(repoRequest, response, null, status);
|
||||
}
|
||||
|
||||
protected <T> JsonpResponse<T> jsonpWrapResponse(RepositoryRestRequest repoRequest,
|
||||
ResponseEntity<T> response) {
|
||||
return jsonpWrapResponse(repoRequest,
|
||||
response.getBody(),
|
||||
response.getHeaders(),
|
||||
response.getStatusCode());
|
||||
}
|
||||
|
||||
protected <T> JsonpResponse<T> jsonpWrapResponse(RepositoryRestRequest repoRequest,
|
||||
T response,
|
||||
HttpHeaders headers,
|
||||
HttpStatus status) {
|
||||
String callback = repoRequest.getRequest().getParameter(config.getJsonpParamName());
|
||||
String errback = repoRequest.getRequest().getParameter(config.getJsonpOnErrParamName());
|
||||
ResponseEntity<T> newResponse;
|
||||
if(null != headers) {
|
||||
newResponse = new ResponseEntity<T>(response, headers, status);
|
||||
} else {
|
||||
newResponse = new ResponseEntity<T>(response, status);
|
||||
}
|
||||
return new JsonpResponse<T>(newResponse,
|
||||
(null != callback ? callback : config.getJsonpParamName()),
|
||||
(null != errback ? errback : config.getJsonpOnErrParamName()));
|
||||
}
|
||||
|
||||
protected List<Link> queryMethodLinks(URI baseUri, Class<?> domainType) {
|
||||
List<Link> links = new ArrayList<Link>();
|
||||
RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(domainType);
|
||||
ResourceMapping repoMapping = ResourceMappingUtils.merge(
|
||||
repoInfo.getRepositoryInterface(),
|
||||
config.getResourceMappingForRepository(repoInfo.getRepositoryInterface())
|
||||
);
|
||||
for(Method method : repoInfo.getQueryMethods()) {
|
||||
LinkBuilder linkBuilder = BaseUriLinkBuilder.create(buildUri(baseUri, repoMapping.getPath(), "search"));
|
||||
ResourceMapping methodMapping = ResourceMappingUtils.merge(method,
|
||||
repoMapping.getResourceMappingFor(method.getName()));
|
||||
links.add(linkBuilder.slash(methodMapping.getPath())
|
||||
.withRel(repoMapping.getRel() + "." + methodMapping.getRel()));
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
protected Link resourceLink(RepositoryRestRequest repoRequest, Resource resource) {
|
||||
ResourceMapping repoMapping = repoRequest.getRepositoryResourceMapping();
|
||||
ResourceMapping entityMapping = repoRequest.getPersistentEntityResourceMapping();
|
||||
|
||||
Link selfLink = resource.getLink("self");
|
||||
String rel = repoMapping.getRel() + "." + entityMapping.getRel();
|
||||
return new Link(selfLink.getHref(), rel);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.data.rest.webmvc.json.JsonSchemaController;
|
||||
import org.springframework.data.rest.config.RepositoryRestConfiguration;
|
||||
import org.springframework.data.rest.webmvc.annotation.BaseURI;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
@@ -17,13 +18,12 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
*/
|
||||
public class BaseUriMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
@Autowired(required = false)
|
||||
private RepositoryRestConfiguration config = RepositoryRestConfiguration.DEFAULT;
|
||||
@Autowired
|
||||
private RepositoryRestConfiguration config;
|
||||
|
||||
@Override public boolean supportsParameter(MethodParameter parameter) {
|
||||
return (RepositoryRestController.class.isAssignableFrom(parameter.getDeclaringClass())
|
||||
|| JsonSchemaController.class.isAssignableFrom(parameter.getDeclaringClass()))
|
||||
&& parameter.getParameterType() == URI.class;
|
||||
return (null != parameter.getParameterAnnotation(BaseURI.class)
|
||||
&& parameter.getParameterType() == URI.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import static org.springframework.data.rest.core.util.UriUtils.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.codehaus.jackson.annotate.JsonAnyGetter;
|
||||
import org.springframework.data.rest.repository.AttributeMetadata;
|
||||
import org.springframework.data.rest.repository.RepositoryMetadata;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.Resource;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class EntityResource extends Resource<Map<String, Object>> {
|
||||
|
||||
public EntityResource(Map<String, Object> dto, Set<Link> links) {
|
||||
super(dto, links);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
public static EntityResource wrap(Object entity, RepositoryMetadata repoMeta, URI baseUri) {
|
||||
|
||||
Set<Link> links = new HashSet<Link>();
|
||||
for(Object attrName : repoMeta.entityMetadata().linkedAttributes().keySet()) {
|
||||
URI uri = buildUri(baseUri, attrName.toString());
|
||||
String rel = repoMeta.rel() + "." + entity.getClass().getSimpleName() + "." + attrName;
|
||||
links.add(new Link(uri.toString(), rel));
|
||||
}
|
||||
links.add(new Link(baseUri.toString(), "self"));
|
||||
|
||||
Map<String, Object> entityDto = new HashMap<String, Object>();
|
||||
for(Map.Entry<String, AttributeMetadata> attrMeta : ((Map<String, AttributeMetadata>)repoMeta.entityMetadata()
|
||||
.embeddedAttributes()).entrySet()) {
|
||||
String name = attrMeta.getKey();
|
||||
Object val;
|
||||
if(null != (val = attrMeta.getValue().get(entity))) {
|
||||
entityDto.put(name, val);
|
||||
}
|
||||
}
|
||||
|
||||
return new EntityResource(entityDto, links);
|
||||
}
|
||||
|
||||
@JsonAnyGetter
|
||||
@Override public Map<String, Object> getContent() {
|
||||
return super.getContent();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import static org.springframework.data.rest.core.util.UriUtils.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.rest.repository.AttributeMetadata;
|
||||
import org.springframework.data.rest.repository.EntityMetadata;
|
||||
import org.springframework.data.rest.repository.RepositoryMetadata;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link Converter} to turn domain entities into {@link Resource}s by segregating embedded entities (those entities
|
||||
* not managed by a {@link org.springframework.data.repository.Repository}) from linked or related entities (which
|
||||
* don't get inlined into an entity's representation but are replaced by links instead.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class EntityToResourceConverter implements Converter<Object, Resource> {
|
||||
|
||||
private final RepositoryRestConfiguration config;
|
||||
private final RepositoryMetadata repositoryMetadata;
|
||||
private final EntityMetadata entityMetadata;
|
||||
|
||||
public EntityToResourceConverter(RepositoryRestConfiguration config,
|
||||
RepositoryMetadata repositoryMetadata) {
|
||||
this.config = config;
|
||||
Assert.notNull(repositoryMetadata, "RepositoryMetadata cannot be null!");
|
||||
this.repositoryMetadata = repositoryMetadata;
|
||||
this.entityMetadata = repositoryMetadata.entityMetadata();
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Override public Resource convert(Object source) {
|
||||
if(null == repositoryMetadata || null == source) {
|
||||
return new Resource<Object>(source);
|
||||
}
|
||||
|
||||
Serializable id = (Serializable)repositoryMetadata.entityMetadata().idAttribute().get(source);
|
||||
URI selfUri = buildUri(config.getBaseUri(), repositoryMetadata.name(), String.format("%s", id));
|
||||
|
||||
Set<Link> links = new HashSet<Link>();
|
||||
for(Object attrName : entityMetadata.linkedAttributes().keySet()) {
|
||||
URI uri = buildUri(selfUri, attrName.toString());
|
||||
String rel = repositoryMetadata.rel() + "." + source.getClass().getSimpleName() + "." + attrName;
|
||||
links.add(new Link(uri.toString(), rel));
|
||||
}
|
||||
links.add(new Link(selfUri.toString(), "self"));
|
||||
|
||||
Map<String, Object> entityDto = new HashMap<String, Object>();
|
||||
for(Map.Entry<String, AttributeMetadata> attrMeta : ((Map<String, AttributeMetadata>)entityMetadata.embeddedAttributes())
|
||||
.entrySet()) {
|
||||
String name = attrMeta.getKey();
|
||||
Object val;
|
||||
if(null != (val = attrMeta.getValue().get(source))) {
|
||||
entityDto.put(name, val);
|
||||
}
|
||||
}
|
||||
|
||||
return new EntityResource(entityDto, links);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public abstract class MediaTypes {
|
||||
|
||||
private MediaTypes() {
|
||||
}
|
||||
|
||||
public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
|
||||
|
||||
public static final List<MediaType> ACCEPT_ALL_TYPES = Collections.singletonList(MediaType.ALL);
|
||||
public static final MediaType COMPACT_JSON = new MediaType("application",
|
||||
"x-spring-data-compact+json",
|
||||
ISO_8859_1);
|
||||
public static final MediaType VERBOSE_JSON = new MediaType("application",
|
||||
"x-spring-data-verbose+json",
|
||||
ISO_8859_1);
|
||||
public static final MediaType APPLICATION_JAVASCRIPT = new MediaType("application",
|
||||
"javascript",
|
||||
ISO_8859_1);
|
||||
public static final MediaType URI_LIST = new MediaType("text",
|
||||
"uri-list",
|
||||
ISO_8859_1);
|
||||
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.rest.config.RepositoryRestConfiguration;
|
||||
import org.springframework.data.rest.webmvc.support.PagingAndSorting;
|
||||
import org.springframework.data.web.PageableDefaults;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -27,8 +29,8 @@ public class PagingAndSortingMethodArgumentResolver implements HandlerMethodArgu
|
||||
|
||||
private static final int DEFAULT_PAGE = 1; // We're 1-based, not 0-based
|
||||
|
||||
@Autowired(required = false)
|
||||
private RepositoryRestConfiguration config = RepositoryRestConfiguration.DEFAULT;
|
||||
@Autowired
|
||||
private RepositoryRestConfiguration config;
|
||||
|
||||
@Override public boolean supportsParameter(MethodParameter parameter) {
|
||||
return ClassUtils.isAssignable(parameter.getParameterType(), PagingAndSorting.class);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import java.util.List;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.data.rest.repository.PersistentEntityResource;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class PersistentEntityResourceHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
@Autowired
|
||||
private RepositoryRestRequestHandlerMethodArgumentResolver repoRequestResolver;
|
||||
private final List<HttpMessageConverter<?>> messageConverters;
|
||||
|
||||
public PersistentEntityResourceHandlerMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverters) {
|
||||
this.messageConverters = messageConverters;
|
||||
}
|
||||
|
||||
@Override public boolean supportsParameter(MethodParameter parameter) {
|
||||
return PersistentEntityResource.class.isAssignableFrom(parameter.getParameterType());
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter,
|
||||
ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest,
|
||||
WebDataBinderFactory binderFactory) throws Exception {
|
||||
RepositoryRestRequest repoRequest = (RepositoryRestRequest)repoRequestResolver.resolveArgument(parameter,
|
||||
mavContainer,
|
||||
webRequest,
|
||||
binderFactory);
|
||||
|
||||
final ServletServerHttpRequest request = new ServletServerHttpRequest(webRequest.getNativeRequest(HttpServletRequest.class));
|
||||
for(HttpMessageConverter converter : messageConverters) {
|
||||
Class<?> domainType = repoRequest.getPersistentEntity().getType();
|
||||
if(!converter.canRead(domainType, request.getHeaders().getContentType())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Object obj = converter.read(domainType, request);
|
||||
return new PersistentEntityResource(repoRequest.getPersistentEntity(),
|
||||
obj);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.codehaus.jackson.JsonEncoding;
|
||||
import org.codehaus.jackson.JsonGenerator;
|
||||
import org.codehaus.jackson.map.Module;
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.data.rest.repository.RepositoryExporter;
|
||||
import org.springframework.data.rest.repository.RepositoryMetadata;
|
||||
import org.springframework.data.rest.repository.UriToDomainObjectUriResolver;
|
||||
import org.springframework.data.rest.webmvc.json.RepositoryAwareJacksonModule;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class RepositoryAwareMappingHttpMessageConverter
|
||||
extends MappingJacksonHttpMessageConverter
|
||||
implements ApplicationEventPublisherAware,
|
||||
InitializingBean {
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
@Autowired(required = false)
|
||||
protected List<ConversionService> conversionServices = Arrays.<ConversionService>asList(new DefaultFormattingConversionService());
|
||||
@Autowired(required = false)
|
||||
protected List<RepositoryExporter> repositoryExporters = Collections.emptyList();
|
||||
@Autowired(required = false)
|
||||
protected List<Module> modules = Collections.emptyList();
|
||||
@Autowired
|
||||
protected UriToDomainObjectUriResolver domainObjectResolver = null;
|
||||
@Autowired
|
||||
protected RepositoryAwareJacksonModule jacksonModule = null;
|
||||
protected ApplicationEventPublisher eventPublisher = null;
|
||||
|
||||
public RepositoryAwareMappingHttpMessageConverter() {
|
||||
setSupportedMediaTypes(Arrays.asList(
|
||||
MediaType.APPLICATION_JSON,
|
||||
MediaTypes.COMPACT_JSON,
|
||||
MediaTypes.VERBOSE_JSON
|
||||
));
|
||||
setObjectMapper(mapper);
|
||||
}
|
||||
|
||||
@Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
|
||||
this.eventPublisher = applicationEventPublisher;
|
||||
}
|
||||
|
||||
@Override public void afterPropertiesSet() throws Exception {
|
||||
boolean builtInModuleRegistered = false;
|
||||
for(Module m : modules) {
|
||||
mapper.registerModule(m);
|
||||
if(m.getClass() == RepositoryAwareJacksonModule.class) {
|
||||
builtInModuleRegistered = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!builtInModuleRegistered) {
|
||||
mapper.registerModule(jacksonModule);
|
||||
}
|
||||
}
|
||||
|
||||
public List<ConversionService> getConversionServices() {
|
||||
return conversionServices;
|
||||
}
|
||||
|
||||
public RepositoryAwareMappingHttpMessageConverter setConversionServices(List<ConversionService> conversionServices) {
|
||||
this.conversionServices = conversionServices;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<RepositoryExporter> getRepositoryExporters() {
|
||||
return repositoryExporters;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
public RepositoryAwareMappingHttpMessageConverter setRepositoryExporters(List<RepositoryExporter> repositoryExporters) {
|
||||
this.repositoryExporters = repositoryExporters;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<Module> getModules() {
|
||||
return modules;
|
||||
}
|
||||
|
||||
public RepositoryAwareMappingHttpMessageConverter setModules(List<Module> modules) {
|
||||
this.modules = modules;
|
||||
return this;
|
||||
}
|
||||
|
||||
public UriToDomainObjectUriResolver getDomainObjectResolver() {
|
||||
return domainObjectResolver;
|
||||
}
|
||||
|
||||
public RepositoryAwareMappingHttpMessageConverter setDomainObjectResolver(UriToDomainObjectUriResolver domainObjectResolver) {
|
||||
this.domainObjectResolver = domainObjectResolver;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override public boolean canWrite(Class<?> clazz, MediaType mediaType) {
|
||||
if(!canWrite(mediaType)) {
|
||||
return false;
|
||||
}
|
||||
return supports(clazz);
|
||||
}
|
||||
|
||||
@Override public boolean canRead(Class<?> clazz, MediaType mediaType) {
|
||||
if(!canRead(mediaType)) {
|
||||
return false;
|
||||
}
|
||||
return supports(clazz);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Override protected boolean supports(Class<?> clazz) {
|
||||
for(RepositoryExporter repoExp : repositoryExporters) {
|
||||
for(String repoName : new ArrayList<String>(repoExp.repositoryNames())) {
|
||||
RepositoryMetadata repoMeta = repoExp.repositoryMetadataFor(repoName);
|
||||
Class domainType = repoMeta.entityMetadata().type();
|
||||
if(domainType.isAssignableFrom(clazz)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override protected void writeInternal(Object object,
|
||||
HttpOutputMessage outputMessage) throws IOException,
|
||||
HttpMessageNotWritableException {
|
||||
JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
|
||||
// Believe it or not, this is the only way to get pretty-printing from Jackson in this configuration
|
||||
JsonGenerator jsonGenerator = mapper
|
||||
.getJsonFactory()
|
||||
.createJsonGenerator(outputMessage.getBody(), encoding)
|
||||
.useDefaultPrettyPrinter();
|
||||
try {
|
||||
mapper.writeValue(jsonGenerator, object);
|
||||
} catch(IOException ex) {
|
||||
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected Object readInternal(Class<?> clazz,
|
||||
HttpInputMessage inputMessage) throws IOException,
|
||||
HttpMessageNotReadableException {
|
||||
try {
|
||||
return mapper.readValue(inputMessage.getBody(), clazz);
|
||||
} catch(IOException ex) {
|
||||
throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import static java.util.Collections.*;
|
||||
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.data.repository.support.DomainClassConverter;
|
||||
import org.springframework.data.repository.support.Repositories;
|
||||
import org.springframework.data.rest.config.RepositoryRestConfiguration;
|
||||
import org.springframework.data.rest.repository.support.RepositoryEntityLinks;
|
||||
import org.springframework.data.rest.webmvc.support.JsonpResponse;
|
||||
import org.springframework.hateoas.EntityLinks;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/")
|
||||
public class RepositoryController extends AbstractRepositoryRestController {
|
||||
|
||||
public RepositoryController(Repositories repositories,
|
||||
RepositoryRestConfiguration config,
|
||||
DomainClassConverter domainClassConverter,
|
||||
ConversionService conversionService) {
|
||||
super(repositories, config, domainClassConverter, conversionService);
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
method = RequestMethod.GET,
|
||||
produces = {
|
||||
"application/json",
|
||||
"application/x-spring-data-compact+json"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public Resource<?> listRepositories(RepositoryRestRequest repoRequest)
|
||||
throws ResourceNotFoundException {
|
||||
EntityLinks linkBuilder = new RepositoryEntityLinks(repoRequest.getBaseUri(),
|
||||
repositories,
|
||||
config);
|
||||
Resource<?> links = new Resource<Object>(emptyList());
|
||||
for(Class<?> domainType : repositories) {
|
||||
links.add(linkBuilder.linkToCollectionResource(domainType));
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
method = RequestMethod.GET,
|
||||
produces = {
|
||||
"application/javascript"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public JsonpResponse<? extends Resource<?>> jsonpListRepositories(RepositoryRestRequest repoRequest)
|
||||
throws ResourceNotFoundException {
|
||||
return jsonpWrapResponse(repoRequest, listRepositories(repoRequest), HttpStatus.OK);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import static org.springframework.data.rest.core.util.UriUtils.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.model.BeanWrapper;
|
||||
import org.springframework.data.repository.support.DomainClassConverter;
|
||||
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.context.AfterDeleteEvent;
|
||||
import org.springframework.data.rest.repository.context.AfterSaveEvent;
|
||||
import org.springframework.data.rest.repository.context.BeforeDeleteEvent;
|
||||
import org.springframework.data.rest.repository.context.BeforeSaveEvent;
|
||||
import org.springframework.data.rest.repository.invoke.RepositoryMethodInvoker;
|
||||
import org.springframework.data.rest.repository.json.JsonSchema;
|
||||
import org.springframework.data.rest.repository.json.PersistentEntityToJsonSchemaConverter;
|
||||
import org.springframework.data.rest.repository.support.DomainObjectMerger;
|
||||
import org.springframework.data.rest.webmvc.support.JsonpResponse;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.Resources;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/{repository}")
|
||||
public class RepositoryEntityController extends AbstractRepositoryRestController {
|
||||
|
||||
@Autowired
|
||||
private DomainObjectMerger domainObjectMerger;
|
||||
@Autowired
|
||||
private PersistentEntityToJsonSchemaConverter jsonSchemaConverter;
|
||||
|
||||
public RepositoryEntityController(Repositories repositories,
|
||||
RepositoryRestConfiguration config,
|
||||
DomainClassConverter domainClassConverter,
|
||||
ConversionService conversionService) {
|
||||
super(repositories, config, domainClassConverter, conversionService);
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
value = "/schema",
|
||||
method = RequestMethod.GET,
|
||||
produces = {
|
||||
"application/schema+json"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public JsonSchema schema(RepositoryRestRequest repoRequest) {
|
||||
return jsonSchemaConverter.convert(repoRequest.getPersistentEntity().getType());
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
method = RequestMethod.GET,
|
||||
produces = {
|
||||
"application/json",
|
||||
"application/x-spring-data-verbose+json"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public Resources<Resource<?>> listEntities(RepositoryRestRequest repoRequest)
|
||||
throws ResourceNotFoundException {
|
||||
List<Resource<?>> resources = new ArrayList<Resource<?>>();
|
||||
List<Link> links = new ArrayList<Link>();
|
||||
|
||||
Iterable<?> results;
|
||||
RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker();
|
||||
boolean hasPagingParams = (null != repoRequest.getRequest().getParameter(config.getPageParamName()));
|
||||
boolean hasSortParams = (null != repoRequest.getRequest().getParameter(config.getSortParamName()));
|
||||
if(repoMethodInvoker.hasFindAllPageable() && hasPagingParams) {
|
||||
results = repoMethodInvoker.findAll(new PageRequest(repoRequest.getPagingAndSorting().getPageNumber(),
|
||||
repoRequest.getPagingAndSorting().getPageSize(),
|
||||
repoRequest.getPagingAndSorting().getSort()));
|
||||
} else if(repoMethodInvoker.hasFindAllSorted() && hasSortParams) {
|
||||
results = repoMethodInvoker.findAll(repoRequest.getPagingAndSorting().getSort());
|
||||
} else if(repoMethodInvoker.hasFindAll()) {
|
||||
results = repoMethodInvoker.findAll();
|
||||
} else {
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
for(Object o : results) {
|
||||
resources.add(new PersistentEntityResource<Object>(repoRequest.getPersistentEntity(),
|
||||
o,
|
||||
repoRequest.buildEntitySelfLink(o, conversionService))
|
||||
.setBaseUri(repoRequest.getBaseUri()));
|
||||
}
|
||||
|
||||
|
||||
if(!repoMethodInvoker.getQueryMethods().isEmpty()) {
|
||||
ResourceMapping repoMapping = repoRequest.getRepositoryResourceMapping();
|
||||
links.add(new Link(buildUri(repoRequest.getBaseUri(), repoMapping.getPath(), "search").toString(),
|
||||
repoMapping.getRel() + ".search"));
|
||||
}
|
||||
|
||||
return new Resources<Resource<?>>(resources, links);
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
method = RequestMethod.GET,
|
||||
produces = {
|
||||
"application/javascript"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public JsonpResponse<? extends Resources<Resource<?>>> jsonpListEntities(RepositoryRestRequest repoRequest)
|
||||
throws ResourceNotFoundException {
|
||||
return jsonpWrapResponse(repoRequest, listEntities(repoRequest), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
method = RequestMethod.GET,
|
||||
produces = {
|
||||
"application/x-spring-data-compact+json",
|
||||
"text/uri-list"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public Resources<Resource<?>> listEntitiesCompact(RepositoryRestRequest repoRequest)
|
||||
throws ResourceNotFoundException {
|
||||
Resources<Resource<?>> resources = listEntities(repoRequest);
|
||||
List<Link> links = new ArrayList<Link>(resources.getLinks());
|
||||
|
||||
for(Resource<?> resource : resources.getContent()) {
|
||||
PersistentEntityResource<?> persistentEntityResource = (PersistentEntityResource<?>)resource;
|
||||
links.add(resourceLink(repoRequest, persistentEntityResource));
|
||||
}
|
||||
|
||||
return new Resources<Resource<?>>(EMPTY_RESOURCE_LIST, links);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
method = RequestMethod.POST,
|
||||
consumes = {
|
||||
"application/json"
|
||||
},
|
||||
produces = {
|
||||
"application/json",
|
||||
"text/uri-list"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public ResponseEntity<Resource<?>> createNewEntity(RepositoryRestRequest repoRequest,
|
||||
PersistentEntityResource<?> incoming) {
|
||||
RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker();
|
||||
if(!repoMethodInvoker.hasSaveOne()) {
|
||||
throw new NoSuchMethodError();
|
||||
}
|
||||
|
||||
applicationContext.publishEvent(new BeforeSaveEvent(incoming.getContent()));
|
||||
Object obj = repoMethodInvoker.save(incoming.getContent());
|
||||
applicationContext.publishEvent(new AfterSaveEvent(obj));
|
||||
|
||||
Link selfLink = repoRequest.buildEntitySelfLink(obj, conversionService);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setLocation(URI.create(selfLink.getHref()));
|
||||
|
||||
return resourceResponse(headers,
|
||||
new PersistentEntityResource<Object>(repoRequest.getPersistentEntity(),
|
||||
obj,
|
||||
selfLink)
|
||||
.setBaseUri(repoRequest.getBaseUri()),
|
||||
HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
method = RequestMethod.POST,
|
||||
consumes = {
|
||||
"application/json"
|
||||
},
|
||||
produces = {
|
||||
"application/javascript"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public JsonpResponse<? extends Resource<?>> jsonpCreateNewEntity(RepositoryRestRequest repoRequest,
|
||||
PersistentEntityResource<?> incoming) {
|
||||
return jsonpWrapResponse(repoRequest, createNewEntity(repoRequest, incoming));
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
value = "/{id}",
|
||||
method = RequestMethod.GET,
|
||||
produces = {
|
||||
"application/json"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public Resource<?> getSingleEntity(RepositoryRestRequest repoRequest,
|
||||
@PathVariable String id)
|
||||
throws ResourceNotFoundException {
|
||||
RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker();
|
||||
if(!repoMethodInvoker.hasFindOne()) {
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
Object domainObj = domainClassConverter.convert(id,
|
||||
STRING_TYPE,
|
||||
TypeDescriptor.valueOf(repoRequest.getPersistentEntity()
|
||||
.getType()));
|
||||
if(null == domainObj) {
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
PersistentEntityResource per = PersistentEntityResource.wrap(repoRequest.getPersistentEntity(),
|
||||
domainObj,
|
||||
repoRequest.getBaseUri());
|
||||
per.add(repoRequest.buildEntitySelfLink(domainObj, conversionService));
|
||||
return per;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
value = "/{id}",
|
||||
method = RequestMethod.GET,
|
||||
produces = {
|
||||
"application/javascript"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public JsonpResponse<? extends Resource<?>> jsonpGetSingleEntity(RepositoryRestRequest repoRequest,
|
||||
@PathVariable String id)
|
||||
throws ResourceNotFoundException {
|
||||
return jsonpWrapResponse(repoRequest,
|
||||
getSingleEntity(repoRequest, id),
|
||||
HttpStatus.OK);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
value = "/{id}",
|
||||
method = RequestMethod.PUT,
|
||||
consumes = {
|
||||
"application/json"
|
||||
},
|
||||
produces = {
|
||||
"application/json",
|
||||
"text/uri-list"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public ResponseEntity<Resource<?>> updateEntity(RepositoryRestRequest repoRequest,
|
||||
PersistentEntityResource<?> incoming,
|
||||
@PathVariable String id)
|
||||
throws ResourceNotFoundException {
|
||||
RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker();
|
||||
if(!repoMethodInvoker.hasSaveOne() || !repoMethodInvoker.hasFindOne()) {
|
||||
throw new NoSuchMethodError();
|
||||
}
|
||||
|
||||
Object domainObj = domainClassConverter.convert(id,
|
||||
STRING_TYPE,
|
||||
TypeDescriptor.valueOf(repoRequest.getPersistentEntity()
|
||||
.getType()));
|
||||
if(null == domainObj) {
|
||||
BeanWrapper incomingWrapper = BeanWrapper.create(incoming.getContent(), conversionService);
|
||||
PersistentProperty idProp = incoming.getPersistentEntity().getIdProperty();
|
||||
incomingWrapper.setProperty(idProp, conversionService.convert(id, idProp.getType()));
|
||||
return createNewEntity(repoRequest, incoming);
|
||||
}
|
||||
|
||||
domainObjectMerger.merge(incoming.getContent(), domainObj);
|
||||
|
||||
applicationContext.publishEvent(new BeforeSaveEvent(incoming.getContent()));
|
||||
Object obj = repoMethodInvoker.save(domainObj);
|
||||
applicationContext.publishEvent(new AfterSaveEvent(obj));
|
||||
|
||||
PersistentEntityResource per = PersistentEntityResource.wrap(repoRequest.getPersistentEntity(),
|
||||
obj,
|
||||
repoRequest.getBaseUri());
|
||||
per.add(repoRequest.buildEntitySelfLink(obj, conversionService));
|
||||
return resourceResponse(null,
|
||||
per,
|
||||
HttpStatus.OK);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
value = "/{id}",
|
||||
method = RequestMethod.PUT,
|
||||
consumes = {
|
||||
"application/json"
|
||||
},
|
||||
produces = {
|
||||
"application/javascript"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public JsonpResponse<? extends Resource<?>> jsonpUpdateEntity(RepositoryRestRequest repoRequest,
|
||||
PersistentEntityResource<?> incoming,
|
||||
@PathVariable String id)
|
||||
throws ResourceNotFoundException {
|
||||
return jsonpWrapResponse(repoRequest, updateEntity(repoRequest, incoming, id));
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
value = "/{id}",
|
||||
method = RequestMethod.DELETE
|
||||
)
|
||||
@ResponseBody
|
||||
public ResponseEntity<?> deleteEntity(RepositoryRestRequest repoRequest,
|
||||
@PathVariable String id)
|
||||
throws ResourceNotFoundException {
|
||||
RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker();
|
||||
if(!repoMethodInvoker.hasFindOne() &&
|
||||
!(repoMethodInvoker.hasDeleteOne() || repoMethodInvoker.hasDeleteOneById())) {
|
||||
throw new NoSuchMethodError();
|
||||
}
|
||||
|
||||
Object domainObj = domainClassConverter.convert(id,
|
||||
STRING_TYPE,
|
||||
TypeDescriptor.valueOf(repoRequest.getPersistentEntity()
|
||||
.getType()));
|
||||
if(null == domainObj) {
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
applicationContext.publishEvent(new BeforeDeleteEvent(domainObj));
|
||||
if(repoMethodInvoker.hasDeleteOneById()) {
|
||||
Class<? extends Serializable> idType = (Class<? extends Serializable>)repoRequest.getPersistentEntity()
|
||||
.getIdProperty()
|
||||
.getType();
|
||||
Object idVal = conversionService.convert(id, idType);
|
||||
repoMethodInvoker.delete((Serializable)idVal);
|
||||
} else if(repoMethodInvoker.hasDeleteOne()) {
|
||||
repoMethodInvoker.delete(domainObj);
|
||||
}
|
||||
applicationContext.publishEvent(new AfterDeleteEvent(domainObj));
|
||||
|
||||
return new ResponseEntity<Object>(HttpStatus.NO_CONTENT);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
value = "{id}",
|
||||
method = RequestMethod.DELETE,
|
||||
produces = {
|
||||
"application/javascript"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public JsonpResponse<?> jsonpDeleteEntity(RepositoryRestRequest repoRequest,
|
||||
@PathVariable String id)
|
||||
throws ResourceNotFoundException {
|
||||
return jsonpWrapResponse(repoRequest, deleteEntity(repoRequest, id));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.data.rest.repository.support.RepositoryEntityLinks;
|
||||
import org.springframework.data.rest.repository.support.RepositoryInformationSupport;
|
||||
import org.springframework.hateoas.EntityLinks;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class RepositoryEntityLinksMethodArgumentResolver
|
||||
extends RepositoryInformationSupport
|
||||
implements HandlerMethodArgumentResolver {
|
||||
|
||||
@Autowired
|
||||
private BaseUriMethodArgumentResolver baseUriResolver;
|
||||
|
||||
@Override public boolean supportsParameter(MethodParameter parameter) {
|
||||
return EntityLinks.class.isAssignableFrom(parameter.getParameterType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter,
|
||||
ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest,
|
||||
WebDataBinderFactory binderFactory)
|
||||
throws Exception {
|
||||
URI baseUri = (URI)baseUriResolver.resolveArgument(parameter,
|
||||
mavContainer,
|
||||
webRequest,
|
||||
binderFactory);
|
||||
return new RepositoryEntityLinks(baseUri, repositories, config);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import static org.springframework.util.ClassUtils.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.data.repository.core.RepositoryInformation;
|
||||
import org.springframework.data.rest.repository.support.RepositoryInformationSupport;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
import org.springframework.web.util.UrlPathHelper;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class RepositoryInformationHandlerMethodArgumentResolver
|
||||
extends RepositoryInformationSupport
|
||||
implements HandlerMethodArgumentResolver {
|
||||
|
||||
@Override public boolean supportsParameter(MethodParameter parameter) {
|
||||
return isAssignable(parameter.getParameterType(), RepositoryInformation.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter,
|
||||
ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest,
|
||||
WebDataBinderFactory binderFactory) throws Exception {
|
||||
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
|
||||
String requestUri = new UrlPathHelper().getLookupPathForRequest(request);
|
||||
if(requestUri.startsWith("/")) {
|
||||
requestUri = requestUri.substring(1);
|
||||
}
|
||||
|
||||
String[] parts = requestUri.split("/");
|
||||
if(parts.length == 0) {
|
||||
// Root request
|
||||
return null;
|
||||
}
|
||||
|
||||
return findRepositoryInfoFor(parts[0]);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import static org.springframework.data.rest.core.util.UriUtils.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.model.BeanWrapper;
|
||||
import org.springframework.data.repository.core.RepositoryInformation;
|
||||
import org.springframework.data.repository.support.DomainClassConverter;
|
||||
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.context.AfterLinkDeleteEvent;
|
||||
import org.springframework.data.rest.repository.context.AfterLinkSaveEvent;
|
||||
import org.springframework.data.rest.repository.context.BeforeLinkDeleteEvent;
|
||||
import org.springframework.data.rest.repository.context.BeforeLinkSaveEvent;
|
||||
import org.springframework.data.rest.repository.invoke.RepositoryMethodInvoker;
|
||||
import org.springframework.data.rest.webmvc.support.JsonpResponse;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/{repository}/{id}/{property}")
|
||||
public class RepositoryPropertyReferenceController extends AbstractRepositoryRestController {
|
||||
|
||||
public RepositoryPropertyReferenceController(Repositories repositories,
|
||||
RepositoryRestConfiguration config,
|
||||
DomainClassConverter domainClassConverter,
|
||||
ConversionService conversionService) {
|
||||
super(repositories, config, domainClassConverter, conversionService);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
method = RequestMethod.GET,
|
||||
produces = {
|
||||
"application/json",
|
||||
"application/x-spring-data-verbose+json"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public ResponseEntity<Resource<?>> followPropertyReference(final RepositoryRestRequest repoRequest,
|
||||
@PathVariable String id,
|
||||
@PathVariable String property)
|
||||
throws ResourceNotFoundException, NoSuchMethodException {
|
||||
final HttpHeaders headers = new HttpHeaders();
|
||||
Function<ReferencedProperty, Resource<?>> handler = new Function<ReferencedProperty, Resource<?>>() {
|
||||
@Override public Resource<?> apply(ReferencedProperty prop) {
|
||||
if(prop.property.isCollectionLike()) {
|
||||
List<Resource<?>> resources = new ArrayList<Resource<?>>();
|
||||
PersistentEntity entity = repositories.getPersistentEntity(prop.propertyType);
|
||||
for(Object obj : ((Iterable)prop.propertyValue)) {
|
||||
PersistentEntityResource per = PersistentEntityResource.wrap(entity, obj, repoRequest.getBaseUri());
|
||||
Link selfLink = repoRequest.buildEntitySelfLink(obj, conversionService);
|
||||
per.add(selfLink);
|
||||
resources.add(per);
|
||||
}
|
||||
|
||||
return new Resource<Object>(resources);
|
||||
} else if(prop.property.isMap()) {
|
||||
Map<Object, Resource<?>> resources = new HashMap<Object, Resource<?>>();
|
||||
PersistentEntity entity = repositories.getPersistentEntity(prop.propertyType);
|
||||
for(Map.Entry<Object, Object> entry : ((Map<Object, Object>)prop.propertyValue).entrySet()) {
|
||||
PersistentEntityResource per = PersistentEntityResource.wrap(entity,
|
||||
entry.getValue(),
|
||||
repoRequest.getBaseUri());
|
||||
Link selfLink = repoRequest.buildEntitySelfLink(entry.getValue(), conversionService);
|
||||
per.add(selfLink);
|
||||
resources.put(entry.getKey(), per);
|
||||
}
|
||||
|
||||
return new Resource<Object>(resources);
|
||||
} else {
|
||||
PersistentEntityResource per = PersistentEntityResource.wrap(repositories.getPersistentEntity(prop.propertyType),
|
||||
prop.propertyValue,
|
||||
repoRequest.getBaseUri());
|
||||
Link selfLink = repoRequest.buildEntitySelfLink(prop.propertyValue, conversionService);
|
||||
per.add(selfLink);
|
||||
|
||||
headers.set("Content-Location", selfLink.getHref());
|
||||
|
||||
return new Resource<Object>(per);
|
||||
}
|
||||
}
|
||||
};
|
||||
Resource<?> responseResource = doWithReferencedProperty(repoRequest,
|
||||
id,
|
||||
property,
|
||||
handler);
|
||||
return resourceResponse(headers, responseResource, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
value = "/{propertyId}",
|
||||
method = RequestMethod.GET,
|
||||
produces = {
|
||||
"application/json",
|
||||
"application/x-spring-data-verbose+json"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public ResponseEntity<Resource<?>> followPropertyReference(final RepositoryRestRequest repoRequest,
|
||||
@PathVariable String id,
|
||||
@PathVariable String property,
|
||||
final @PathVariable String propertyId)
|
||||
throws ResourceNotFoundException, NoSuchMethodException {
|
||||
final HttpHeaders headers = new HttpHeaders();
|
||||
Function<ReferencedProperty, Resource<?>> handler = new Function<ReferencedProperty, Resource<?>>() {
|
||||
@Override public Resource<?> apply(ReferencedProperty prop) {
|
||||
if(prop.property.isCollectionLike()) {
|
||||
PersistentEntity entity = repositories.getPersistentEntity(prop.propertyType);
|
||||
for(Object obj : ((Iterable)prop.propertyValue)) {
|
||||
BeanWrapper propValWrapper = BeanWrapper.create(obj, conversionService);
|
||||
String sId = propValWrapper.getProperty(prop.entity.getIdProperty()).toString();
|
||||
if(propertyId.equals(sId)) {
|
||||
PersistentEntityResource per = PersistentEntityResource.wrap(entity, obj, repoRequest.getBaseUri());
|
||||
Link selfLink = repoRequest.buildEntitySelfLink(obj, conversionService);
|
||||
per.add(selfLink);
|
||||
headers.set("Content-Location", selfLink.getHref());
|
||||
return new Resource<Object>(per);
|
||||
}
|
||||
}
|
||||
} else if(prop.property.isMap()) {
|
||||
PersistentEntity entity = repositories.getPersistentEntity(prop.propertyType);
|
||||
for(Map.Entry<Object, Object> entry : ((Map<Object, Object>)prop.propertyValue).entrySet()) {
|
||||
BeanWrapper propValWrapper = BeanWrapper.create(entry.getValue(), conversionService);
|
||||
String sId = propValWrapper.getProperty(prop.entity.getIdProperty()).toString();
|
||||
if(propertyId.equals(sId)) {
|
||||
PersistentEntityResource per = PersistentEntityResource.wrap(entity,
|
||||
entry.getValue(),
|
||||
repoRequest.getBaseUri());
|
||||
Link selfLink = repoRequest.buildEntitySelfLink(entry.getValue(), conversionService);
|
||||
per.add(selfLink);
|
||||
headers.set("Content-Location", selfLink.getHref());
|
||||
return new Resource<Object>(per, selfLink);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return new Resource<Object>(prop.propertyValue);
|
||||
}
|
||||
throw new IllegalArgumentException(new ResourceNotFoundException());
|
||||
}
|
||||
};
|
||||
Resource<?> responseResource = doWithReferencedProperty(repoRequest,
|
||||
id,
|
||||
property,
|
||||
handler);
|
||||
return resourceResponse(headers, responseResource, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
method = RequestMethod.GET,
|
||||
produces = {
|
||||
"application/x-spring-data-compact+json",
|
||||
"text/uri-list"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public ResponseEntity<Resource<?>> followPropertyReferenceCompact(RepositoryRestRequest repoRequest,
|
||||
@PathVariable String id,
|
||||
@PathVariable String property)
|
||||
throws ResourceNotFoundException, NoSuchMethodException {
|
||||
ResponseEntity<Resource<?>> response = followPropertyReference(repoRequest, id, property);
|
||||
if(response.getStatusCode() != HttpStatus.OK) {
|
||||
return response;
|
||||
}
|
||||
|
||||
ResourceMapping repoMapping = repoRequest.getRepositoryResourceMapping();
|
||||
ResourceMapping entityMapping = repoRequest.getPersistentEntityResourceMapping();
|
||||
ResourceMapping propMapping = entityMapping.getResourceMappingFor(entityMapping.getNameForPath(property));
|
||||
String propRel = (null != propMapping ? propMapping.getRel() : property);
|
||||
|
||||
Resource<?> resource = response.getBody();
|
||||
|
||||
List<Link> links = new ArrayList<Link>();
|
||||
|
||||
URI entityBaseUri = buildUri(repoRequest.getBaseUri(),
|
||||
repoMapping.getPath(),
|
||||
id,
|
||||
property);
|
||||
|
||||
if(resource.getContent() instanceof Iterable) {
|
||||
for(Resource<?> res : (Iterable<Resource<?>>)resource.getContent()) {
|
||||
Link propLink = propertyReferenceLink(res, entityBaseUri, propRel);
|
||||
links.add(propLink);
|
||||
}
|
||||
} else if(resource.getContent() instanceof Map) {
|
||||
for(Map.Entry<Object, Resource<?>> entry : ((Map<Object, Resource<?>>)resource.getContent()).entrySet()) {
|
||||
Link l = new Link(entry.getValue().getLink("self").getHref(), conversionService.convert(entry.getKey(),
|
||||
String.class));
|
||||
links.add(l);
|
||||
}
|
||||
} else {
|
||||
links.add(new Link(entityBaseUri.toString(), propRel));
|
||||
}
|
||||
|
||||
return resourceResponse(null, new Resource<Object>(EMPTY_RESOURCE_LIST, links), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
method = RequestMethod.GET,
|
||||
produces = {
|
||||
"application/javascript"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public JsonpResponse<?> jsonpFollowPropertyReference(RepositoryRestRequest repoRequest,
|
||||
@PathVariable String id,
|
||||
@PathVariable String property)
|
||||
throws ResourceNotFoundException, NoSuchMethodException {
|
||||
return jsonpWrapResponse(repoRequest,
|
||||
followPropertyReference(repoRequest,
|
||||
id,
|
||||
property),
|
||||
HttpStatus.OK);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
value = "/{propertyId}",
|
||||
method = RequestMethod.GET,
|
||||
produces = {
|
||||
"application/javascript"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public JsonpResponse<?> jsonpFollowPropertyReference(RepositoryRestRequest repoRequest,
|
||||
@PathVariable String id,
|
||||
@PathVariable String property,
|
||||
@PathVariable String propertyId)
|
||||
throws ResourceNotFoundException, NoSuchMethodException {
|
||||
return jsonpWrapResponse(repoRequest,
|
||||
followPropertyReference(repoRequest,
|
||||
id,
|
||||
property,
|
||||
propertyId),
|
||||
HttpStatus.OK);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
method = {
|
||||
RequestMethod.POST,
|
||||
RequestMethod.PUT
|
||||
},
|
||||
consumes = {
|
||||
"application/json",
|
||||
"application/x-spring-data-compact+json",
|
||||
"text/uri-list"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public ResponseEntity<Resource<?>> createPropertyReference(final RepositoryRestRequest repoRequest,
|
||||
final @RequestBody Resource<Object> incoming,
|
||||
@PathVariable String id,
|
||||
@PathVariable String property)
|
||||
throws ResourceNotFoundException, NoSuchMethodException {
|
||||
final RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker();
|
||||
if(!repoMethodInvoker.hasSaveOne()) {
|
||||
throw new NoSuchMethodException();
|
||||
}
|
||||
Function<ReferencedProperty, Resource<?>> handler = new Function<ReferencedProperty, Resource<?>>() {
|
||||
@Override public Resource<?> apply(ReferencedProperty prop) {
|
||||
if(prop.property.isCollectionLike()) {
|
||||
Collection coll = new ArrayList();
|
||||
if("POST".equals(repoRequest.getRequest().getMethod())) {
|
||||
coll.addAll((Collection)prop.propertyValue);
|
||||
}
|
||||
for(Link l : incoming.getLinks()) {
|
||||
Object propVal = loadPropertyValue(prop.propertyType, l.getHref());
|
||||
coll.add(propVal);
|
||||
}
|
||||
prop.wrapper.setProperty(prop.property, coll);
|
||||
} else if(prop.property.isMap()) {
|
||||
Map m = new HashMap();
|
||||
if("POST".equals(repoRequest.getRequest().getMethod())) {
|
||||
m.putAll((Map)prop.propertyValue);
|
||||
}
|
||||
for(Link l : incoming.getLinks()) {
|
||||
Object propVal = loadPropertyValue(prop.propertyType, l.getHref());
|
||||
m.put(l.getRel(), propVal);
|
||||
}
|
||||
prop.wrapper.setProperty(prop.property, m);
|
||||
} else {
|
||||
if("POST".equals(repoRequest.getRequest().getMethod())) {
|
||||
throw new IllegalStateException(
|
||||
"Cannot POST a reference to this singular property since the property type is not a List or a Map.");
|
||||
}
|
||||
if(incoming.getLinks().size() != 1) {
|
||||
throw new IllegalArgumentException(
|
||||
"Must send only 1 link to update a property reference that isn't a List or a Map.");
|
||||
}
|
||||
Object propVal = loadPropertyValue(prop.propertyType, incoming.getLinks().get(0).getHref());
|
||||
prop.wrapper.setProperty(prop.property, propVal);
|
||||
}
|
||||
|
||||
applicationContext.publishEvent(new BeforeLinkSaveEvent(prop.wrapper.getBean(), prop.propertyValue));
|
||||
Object result = repoMethodInvoker.save(prop.wrapper.getBean());
|
||||
applicationContext.publishEvent(new AfterLinkSaveEvent(result, prop.propertyValue));
|
||||
return null;
|
||||
}
|
||||
};
|
||||
doWithReferencedProperty(repoRequest,
|
||||
id,
|
||||
property,
|
||||
handler);
|
||||
return resourceResponse(null, EMPTY_RESOURCE, HttpStatus.NO_CONTENT);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
method = {
|
||||
RequestMethod.POST,
|
||||
RequestMethod.PUT
|
||||
},
|
||||
consumes = {
|
||||
"application/javascript"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public JsonpResponse<?> jsonpCreatePropertyReference(final RepositoryRestRequest repoRequest,
|
||||
final @RequestBody Resource<Object> incoming,
|
||||
@PathVariable String id,
|
||||
@PathVariable String property)
|
||||
throws ResourceNotFoundException, NoSuchMethodException {
|
||||
return jsonpWrapResponse(repoRequest, createPropertyReference(repoRequest,
|
||||
incoming,
|
||||
id,
|
||||
property));
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
value = "/{propertyId}",
|
||||
method = RequestMethod.DELETE
|
||||
)
|
||||
@ResponseBody
|
||||
public ResponseEntity<Resource<?>> deletePropertyReference(final RepositoryRestRequest repoRequest,
|
||||
@PathVariable String id,
|
||||
@PathVariable String property,
|
||||
final @PathVariable String propertyId)
|
||||
throws ResourceNotFoundException, NoSuchMethodException {
|
||||
final RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker();
|
||||
if(!repoMethodInvoker.hasDeleteOne()) {
|
||||
throw new NoSuchMethodException();
|
||||
}
|
||||
|
||||
Function<ReferencedProperty, Resource<?>> handler = new Function<ReferencedProperty, Resource<?>>() {
|
||||
@Override public Resource<?> apply(ReferencedProperty prop) {
|
||||
if(null == prop.propertyValue) {
|
||||
return null;
|
||||
}
|
||||
if(prop.property.isCollectionLike()) {
|
||||
Collection coll = new ArrayList();
|
||||
for(Object obj : (Collection)prop.propertyValue) {
|
||||
BeanWrapper propValWrapper = BeanWrapper.create(obj, conversionService);
|
||||
String s = (String)propValWrapper.getProperty(prop.entity.getIdProperty(), String.class, false);
|
||||
if(!propertyId.equals(s)) {
|
||||
coll.add(obj);
|
||||
}
|
||||
}
|
||||
prop.wrapper.setProperty(prop.property, coll);
|
||||
} else if(prop.property.isMap()) {
|
||||
Map m = new HashMap();
|
||||
for(Map.Entry<Object, Object> entry : ((Map<Object, Object>)prop.propertyValue).entrySet()) {
|
||||
BeanWrapper propValWrapper = BeanWrapper.create(entry.getValue(), conversionService);
|
||||
String s = (String)propValWrapper.getProperty(prop.entity.getIdProperty(), String.class, false);
|
||||
if(!propertyId.equals(s)) {
|
||||
m.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
prop.wrapper.setProperty(prop.property, m);
|
||||
} else {
|
||||
prop.wrapper.setProperty(prop.property, null);
|
||||
}
|
||||
|
||||
applicationContext.publishEvent(new BeforeLinkDeleteEvent(prop.wrapper.getBean(), prop.propertyValue));
|
||||
Object result = repoMethodInvoker.save(prop.wrapper.getBean());
|
||||
applicationContext.publishEvent(new AfterLinkDeleteEvent(result, prop.propertyValue));
|
||||
return null;
|
||||
}
|
||||
};
|
||||
doWithReferencedProperty(repoRequest,
|
||||
id,
|
||||
property,
|
||||
handler);
|
||||
|
||||
return resourceResponse(null, EMPTY_RESOURCE, HttpStatus.NO_CONTENT);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
value = "/{propertyId}",
|
||||
method = RequestMethod.DELETE,
|
||||
produces = {
|
||||
"application/javascript"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public JsonpResponse<?> jsonpDeletePropertyReference(final RepositoryRestRequest repoRequest,
|
||||
@PathVariable String id,
|
||||
@PathVariable String property,
|
||||
final @PathVariable String propertyId)
|
||||
throws ResourceNotFoundException, NoSuchMethodException {
|
||||
return jsonpWrapResponse(repoRequest, deletePropertyReference(repoRequest,
|
||||
id,
|
||||
property,
|
||||
propertyId));
|
||||
}
|
||||
|
||||
private Link propertyReferenceLink(Resource<?> resource,
|
||||
URI baseUri,
|
||||
String rel) {
|
||||
Link selfLink = resource.getLink("self");
|
||||
String objId = selfLink.getHref().substring(selfLink.getHref().lastIndexOf('/') + 1);
|
||||
return new Link(buildUri(baseUri, objId).toString(), rel);
|
||||
}
|
||||
|
||||
private Object loadPropertyValue(Class<?> type, String href) {
|
||||
String id = href.substring(href.lastIndexOf('/') + 1);
|
||||
return domainClassConverter.convert(id,
|
||||
STRING_TYPE,
|
||||
TypeDescriptor.valueOf(type));
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
private Resource<?> doWithReferencedProperty(RepositoryRestRequest repoRequest,
|
||||
String id,
|
||||
String propertyPath,
|
||||
Function<ReferencedProperty, Resource<?>> handler)
|
||||
throws ResourceNotFoundException, NoSuchMethodException {
|
||||
RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker();
|
||||
if(!repoMethodInvoker.hasFindOne()) {
|
||||
throw new NoSuchMethodException();
|
||||
}
|
||||
|
||||
Object domainObj = domainClassConverter.convert(id,
|
||||
STRING_TYPE,
|
||||
TypeDescriptor.valueOf(repoRequest.getPersistentEntity()
|
||||
.getType()));
|
||||
if(null == domainObj) {
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
String propertyName = repoRequest.getPersistentEntityResourceMapping().getNameForPath(propertyPath);
|
||||
PersistentProperty prop = repoRequest.getPersistentEntity().getPersistentProperty(propertyName);
|
||||
if(null == prop) {
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
BeanWrapper wrapper = BeanWrapper.create(domainObj, conversionService);
|
||||
Object propVal = wrapper.getProperty(prop);
|
||||
if(null == propVal) {
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
return handler.apply(new ReferencedProperty(prop,
|
||||
propVal,
|
||||
wrapper));
|
||||
}
|
||||
|
||||
private class ReferencedProperty {
|
||||
final PersistentEntity entity;
|
||||
final PersistentProperty property;
|
||||
final Class<?> propertyType;
|
||||
final Object propertyValue;
|
||||
final BeanWrapper wrapper;
|
||||
final RepositoryInformation propertyRepoInfo;
|
||||
final Object propertyRepo;
|
||||
final RepositoryMethodInvoker repoMethodInvoker;
|
||||
|
||||
private ReferencedProperty(PersistentProperty property,
|
||||
Object propertyValue,
|
||||
BeanWrapper wrapper) {
|
||||
this.property = property;
|
||||
this.propertyValue = propertyValue;
|
||||
this.wrapper = wrapper;
|
||||
if(property.isCollectionLike()) {
|
||||
this.propertyType = property.getComponentType();
|
||||
} else if(property.isMap()) {
|
||||
this.propertyType = property.getMapValueType();
|
||||
} else {
|
||||
this.propertyType = property.getType();
|
||||
}
|
||||
this.propertyRepoInfo = repositories.getRepositoryInformationFor(propertyType);
|
||||
this.entity = repositories.getPersistentEntity(propertyType);
|
||||
this.propertyRepo = repositories.getRepositoryFor(entity.getType());
|
||||
this.repoMethodInvoker = new RepositoryMethodInvoker(propertyRepo, propertyRepoInfo, entity);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import java.net.URI;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Central configuration helper class for the REST exporter. If something within the REST exporter is configurable,
|
||||
* there is a property here you can use to set the value.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class RepositoryRestConfiguration {
|
||||
|
||||
public static final RepositoryRestConfiguration DEFAULT = new 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;
|
||||
|
||||
/**
|
||||
* The base URI against which the exporter should calculate its links.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public URI getBaseUri() {
|
||||
return baseUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* The base URI against which the exporter should calculate its links.
|
||||
*
|
||||
* @param baseUri
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public int getDefaultPageSize() {
|
||||
return defaultPageSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default size of {@link org.springframework.data.domain.Pageable}s.
|
||||
*
|
||||
* @param defaultPageSize
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public String getPageParamName() {
|
||||
return pageParamName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name of the URL query string parameter that indicates what page to return.
|
||||
*
|
||||
* @param pageParamName
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public String getSortParamName() {
|
||||
return sortParamName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name of the URL query string parameter that indicates what direction to sort results.
|
||||
*
|
||||
* @param sortParamName
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public RepositoryRestConfiguration setJsonpOnErrParamName(String jsonpOnErrParamName) {
|
||||
this.jsonpOnErrParamName = jsonpOnErrParamName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link MediaType} to use as a default when none is specified.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public MediaType getDefaultMediaType() {
|
||||
return defaultMediaType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link MediaType} to use as a default when none is specified.
|
||||
*
|
||||
* @param defaultMediaType
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public boolean isDumpErrors() {
|
||||
return dumpErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether exception messages should be logged to the body of the response as a JSON object.
|
||||
*
|
||||
* @param dumpErrors
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public RepositoryRestConfiguration setDumpErrors(boolean dumpErrors) {
|
||||
this.dumpErrors = dumpErrors;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
|
||||
/**
|
||||
* Special {@link DispatcherServlet} subclass that certain exporter components can recognize.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class RepositoryRestDispatcherServlet extends DispatcherServlet {
|
||||
public RepositoryRestDispatcherServlet(WebApplicationContext webApplicationContext) {
|
||||
super(webApplicationContext);
|
||||
setContextClass(AnnotationConfigWebApplicationContext.class);
|
||||
setContextConfigLocation(RepositoryRestMvcConfiguration.class.getName());
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
|
||||
/**
|
||||
* Convenience {@link DispatcherServlet} that sets the 'contextClass' and 'contextConfigLocation' properties to the
|
||||
* correct values for using the REST exporter in a web.xml file.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class RepositoryRestExporterServlet extends DispatcherServlet {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public RepositoryRestExporterServlet() {
|
||||
configure();
|
||||
}
|
||||
|
||||
public RepositoryRestExporterServlet(WebApplicationContext webApplicationContext) {
|
||||
super(webApplicationContext);
|
||||
configure();
|
||||
}
|
||||
|
||||
private void configure() {
|
||||
setContextClass(AnnotationConfigWebApplicationContext.class);
|
||||
setContextConfigLocation(RepositoryRestMvcConfiguration.class.getName());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,15 +4,14 @@ import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.data.rest.webmvc.json.JsonSchemaController;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
|
||||
|
||||
/**
|
||||
* {@link RequestMappingHandlerAdapter} implementation that adds a couple argument resolvers for controller method
|
||||
* parameters used in the REST exporter controller. Also only looks for handler methods in the {@link
|
||||
* RepositoryRestController} class to help isolate this handler adapter from other handler adapters the user might have
|
||||
* parameters used in the REST exporter controller. Also only looks for handler methods in the Spring Data REST
|
||||
* provided controller classes to help isolate this handler adapter from other handler adapters the user might have
|
||||
* configured in their Spring MVC context.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
@@ -32,9 +31,12 @@ public class RepositoryRestHandlerAdapter extends ResourceProcessorInvokingHandl
|
||||
}
|
||||
|
||||
@Override protected boolean supportsInternal(HandlerMethod handlerMethod) {
|
||||
Class<?> controllerType = handlerMethod.getBeanType();
|
||||
return super.supportsInternal(handlerMethod)
|
||||
&& (RepositoryRestController.class.isAssignableFrom(handlerMethod.getBeanType())
|
||||
|| JsonSchemaController.class.isAssignableFrom(handlerMethod.getBeanType()));
|
||||
&& (RepositoryController.class.isAssignableFrom(controllerType)
|
||||
|| RepositoryEntityController.class.isAssignableFrom(controllerType)
|
||||
|| RepositoryPropertyReferenceController.class.isAssignableFrom(controllerType)
|
||||
|| RepositorySearchController.class.isAssignableFrom(controllerType));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*;
|
||||
import static org.springframework.util.StringUtils.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.EntityManagerFactory;
|
||||
import javax.persistence.PersistenceContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.data.rest.repository.RepositoryExporter;
|
||||
import org.springframework.data.rest.webmvc.json.JsonSchemaController;
|
||||
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.http.MediaType;
|
||||
import org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
@@ -26,39 +33,83 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl
|
||||
public class RepositoryRestHandlerMapping extends RequestMappingHandlerMapping {
|
||||
|
||||
@Autowired
|
||||
private EntityManagerFactory entityManagerFactory;
|
||||
@Autowired(required = false)
|
||||
private List<RepositoryExporter> repositoryExporters = Collections.emptyList();
|
||||
private Set<String> repositoryNames = new HashSet<String>();
|
||||
private Repositories repositories;
|
||||
@Autowired
|
||||
private RepositoryRestConfiguration config;
|
||||
private EntityManagerFactory entityManagerFactory;
|
||||
|
||||
public RepositoryRestHandlerMapping() {
|
||||
setOrder(Ordered.LOWEST_PRECEDENCE);
|
||||
}
|
||||
|
||||
@PersistenceContext
|
||||
public void setEntityManager(EntityManager entityManager) {
|
||||
this.entityManagerFactory = entityManager.getEntityManagerFactory();
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Override
|
||||
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
|
||||
if(repositoryNames.isEmpty() && !repositoryExporters.isEmpty()) {
|
||||
for(RepositoryExporter re : repositoryExporters) {
|
||||
repositoryNames.addAll(re.repositoryNames());
|
||||
protected HandlerMethod lookupHandlerMethod(String lookupPath,
|
||||
HttpServletRequest origRequest) throws Exception {
|
||||
String acceptType = origRequest.getHeader("Accept");
|
||||
List<MediaType> acceptHeaderTypes = MediaType.parseMediaTypes(acceptType);
|
||||
List<MediaType> acceptableTypes = new ArrayList<MediaType>();
|
||||
for(MediaType mt : acceptHeaderTypes) {
|
||||
if(("*".equals(mt.getType()) && ("*".equals(mt.getSubtype()))
|
||||
|| ("application".equals(mt.getType()) && "*".equals(mt.getSubtype())))) {
|
||||
mt = config.getDefaultMediaType();
|
||||
}
|
||||
if(!acceptableTypes.contains(mt)) {
|
||||
acceptableTypes.add(mt);
|
||||
}
|
||||
}
|
||||
String[] parts = lookupPath.split("/");
|
||||
if(parts.length == 0) {
|
||||
// Root request
|
||||
return super.lookupHandlerMethod(lookupPath, request);
|
||||
if(acceptableTypes.size() > 1) {
|
||||
acceptType = collectionToDelimitedString(acceptableTypes, ",");
|
||||
} else if(acceptableTypes.size() == 1) {
|
||||
acceptType = acceptableTypes.get(0).toString();
|
||||
} else {
|
||||
if(repositoryNames.contains(parts[1])) {
|
||||
acceptType = config.getDefaultMediaType().toString();
|
||||
}
|
||||
|
||||
HttpServletRequest request = new DefaultAcceptTypeHttpServletRequest(origRequest, acceptType);
|
||||
|
||||
if(acceptType.contains("javascript")) {
|
||||
if(null != request.getParameter(config.getJsonpParamName())
|
||||
|| null != request.getParameter(config.getJsonpOnErrParamName())) {
|
||||
return super.lookupHandlerMethod(lookupPath, request);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
String requestUri = lookupPath;
|
||||
if(requestUri.startsWith("/")) {
|
||||
requestUri = requestUri.substring(1);
|
||||
}
|
||||
if(!hasText(requestUri)) {
|
||||
return super.lookupHandlerMethod(lookupPath, request);
|
||||
}
|
||||
String[] parts = requestUri.split("/");
|
||||
if(parts.length == 0) {
|
||||
// Root request
|
||||
return super.lookupHandlerMethod(lookupPath, request);
|
||||
}
|
||||
|
||||
for(Class<?> domainType : repositories) {
|
||||
RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(domainType);
|
||||
ResourceMapping mapping = getResourceMapping(config, repoInfo);
|
||||
if(mapping.getPath().equals(parts[0]) && mapping.isExported()) {
|
||||
return super.lookupHandlerMethod(lookupPath, request);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override protected boolean isHandler(Class<?> beanType) {
|
||||
return (RepositoryRestController.class.isAssignableFrom(beanType)
|
||||
|| JsonSchemaController.class.isAssignableFrom(beanType));
|
||||
return (RepositoryController.class.isAssignableFrom(beanType)
|
||||
|| RepositoryEntityController.class.isAssignableFrom(beanType)
|
||||
|| RepositoryPropertyReferenceController.class.isAssignableFrom(beanType)
|
||||
|| RepositorySearchController.class.isAssignableFrom(beanType));
|
||||
}
|
||||
|
||||
@Override protected void extendInterceptors(List<Object> interceptors) {
|
||||
@@ -69,4 +120,22 @@ public class RepositoryRestHandlerMapping extends RequestMappingHandlerMapping {
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultAcceptTypeHttpServletRequest extends HttpServletRequestWrapper {
|
||||
private final String defaultAcceptType;
|
||||
|
||||
private DefaultAcceptTypeHttpServletRequest(HttpServletRequest request,
|
||||
String defaultAcceptType) {
|
||||
super(request);
|
||||
this.defaultAcceptType = defaultAcceptType;
|
||||
}
|
||||
|
||||
@Override public String getHeader(String name) {
|
||||
if("accept".equals(name.toLowerCase())) {
|
||||
return defaultAcceptType;
|
||||
} else {
|
||||
return super.getHeader(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportResource;
|
||||
import org.springframework.data.rest.repository.UriToDomainObjectUriResolver;
|
||||
import org.springframework.data.rest.repository.context.AnnotatedHandlerBeanPostProcessor;
|
||||
import org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener;
|
||||
import org.springframework.data.rest.repository.jpa.JpaRepositoryExporter;
|
||||
import org.springframework.data.rest.webmvc.json.JsonSchemaController;
|
||||
import org.springframework.data.rest.webmvc.json.RepositoryAwareJacksonModule;
|
||||
import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
|
||||
|
||||
/**
|
||||
* Main Spring MVC configuration for the REST exporter. Can be subclassed and any of these methods overridden to
|
||||
* provide
|
||||
* custom configuration for your environment. More than likely, however, it won't be necessary to do this as most
|
||||
* user-configurable properties are defined on the {@link RepositoryRestConfiguration} bean, which you can define in
|
||||
* your own <code>ApplicationContext</code> (which can take the form of an XML file in the classpath at location
|
||||
* 'META-INF/spring-data-rest/' with a name that ends with '-export.xml').
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@Configuration
|
||||
@ImportResource("classpath*:META-INF/spring-data-rest/**/*-export.xml")
|
||||
public class RepositoryRestMvcConfiguration {
|
||||
|
||||
/**
|
||||
* {@link org.springframework.data.rest.repository.RepositoryExporter} implementation for exporting JPA repositories.
|
||||
*/
|
||||
@Autowired(required = false)
|
||||
protected JpaRepositoryExporter customJpaRepositoryExporter;
|
||||
|
||||
/**
|
||||
* {@link org.springframework.context.ApplicationListener} implementation for invoking {@link
|
||||
* org.springframework.validation.Validator} instances assigned to specific domain types.
|
||||
*/
|
||||
@Autowired(required = false)
|
||||
protected ValidatingRepositoryEventListener validatingRepositoryEventListener;
|
||||
|
||||
/**
|
||||
* Main configuration for the REST exporter.
|
||||
*/
|
||||
@Autowired(required = false)
|
||||
protected RepositoryRestConfiguration repositoryRestConfig = RepositoryRestConfiguration.DEFAULT;
|
||||
|
||||
/**
|
||||
* For getting access to the {@link javax.persistence.EntityManagerFactory}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public PersistenceAnnotationBeanPostProcessor persistenceAnnotationBeanPostProcessor() {
|
||||
return new PersistenceAnnotationBeanPostProcessor();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link org.springframework.beans.factory.config.BeanPostProcessor} to turn beans annotated as {@link
|
||||
* org.springframework.data.rest.repository.annotation.RepositoryEventHandler}s.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public AnnotatedHandlerBeanPostProcessor annotatedHandlerBeanPostProcessor() {
|
||||
return new AnnotatedHandlerBeanPostProcessor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the pre-defined {@link JpaRepositoryExporter} defined by the user or create a default one.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public JpaRepositoryExporter jpaRepositoryExporter() {
|
||||
if(null == customJpaRepositoryExporter) {
|
||||
return new JpaRepositoryExporter();
|
||||
}
|
||||
|
||||
return customJpaRepositoryExporter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the pre-defined {@link ValidatingRepositoryEventListener} defined by the user or create a default one.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public ValidatingRepositoryEventListener validatingRepositoryEventListener() {
|
||||
return (null == validatingRepositoryEventListener
|
||||
? new ValidatingRepositoryEventListener()
|
||||
: validatingRepositoryEventListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* A special Jackson {@link org.codehaus.jackson.map.Module} implementation that configures converters for entities.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public RepositoryAwareJacksonModule jacksonModule() {
|
||||
return new RepositoryAwareJacksonModule();
|
||||
}
|
||||
|
||||
/**
|
||||
* Special Repository-aware {@link org.springframework.http.converter.HttpMessageConverter} that can deal with
|
||||
* entities and links.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public RepositoryAwareMappingHttpMessageConverter mappingHttpMessageConverter() {
|
||||
return new RepositoryAwareMappingHttpMessageConverter();
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link org.springframework.data.rest.core.UriResolver} implementation that takes a {@link java.net.URI} and
|
||||
* turns
|
||||
* it
|
||||
* into a top-level domain object.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public UriToDomainObjectUriResolver domainObjectResolver() {
|
||||
return new UriToDomainObjectUriResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* The main REST exporter Spring MVC controller.
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
@Bean public RepositoryRestController repositoryRestController() throws Exception {
|
||||
return new RepositoryRestController();
|
||||
}
|
||||
|
||||
@Bean public JsonSchemaController jsonSchemaController() {
|
||||
return new JsonSchemaController();
|
||||
}
|
||||
|
||||
@Bean public BaseUriMethodArgumentResolver baseUriMethodArgumentResolver() {
|
||||
return new BaseUriMethodArgumentResolver();
|
||||
}
|
||||
|
||||
@Bean public PagingAndSortingMethodArgumentResolver pagingAndSortingMethodArgumentResolver() {
|
||||
return new PagingAndSortingMethodArgumentResolver();
|
||||
}
|
||||
|
||||
@Bean public ServerHttpRequestMethodArgumentResolver serverHttpRequestMethodArgumentResolver() {
|
||||
return new ServerHttpRequestMethodArgumentResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Special {@link org.springframework.web.servlet.HandlerAdapter} that only recognizes handler methods defined in the
|
||||
* {@link RepositoryRestController} class.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public RepositoryRestHandlerAdapter repositoryExporterHandlerAdapter() {
|
||||
return new RepositoryRestHandlerAdapter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Special {@link org.springframework.web.servlet.HandlerMapping} that only recognizes handler methods defined in the
|
||||
* {@link RepositoryRestController} class.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public RepositoryRestHandlerMapping repositoryExporterHandlerMapping() {
|
||||
return new RepositoryRestHandlerMapping();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bean for looking up methods annotated with {@link org.springframework.web.bind.annotation.ExceptionHandler}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver() {
|
||||
ExceptionHandlerExceptionResolver er = new ExceptionHandlerExceptionResolver();
|
||||
er.setCustomArgumentResolvers(
|
||||
Arrays.<HandlerMethodArgumentResolver>asList(new ServerHttpRequestMethodArgumentResolver())
|
||||
);
|
||||
return er;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import static org.springframework.data.rest.core.util.UriUtils.*;
|
||||
import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
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.invoke.RepositoryMethodInvoker;
|
||||
import org.springframework.data.rest.webmvc.support.PagingAndSorting;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
class RepositoryRestRequest {
|
||||
|
||||
private final RepositoryRestConfiguration config;
|
||||
private final HttpServletRequest request;
|
||||
private final PagingAndSorting pagingAndSorting;
|
||||
private final URI baseUri;
|
||||
private final RepositoryInformation repoInfo;
|
||||
private final ResourceMapping repoMapping;
|
||||
private final Link repoLink;
|
||||
private final Object repository;
|
||||
private final RepositoryMethodInvoker repoMethodInvoker;
|
||||
private final PersistentEntity persistentEntity;
|
||||
private final ResourceMapping entityMapping;
|
||||
|
||||
public RepositoryRestRequest(RepositoryRestConfiguration config,
|
||||
Repositories repositories,
|
||||
HttpServletRequest request,
|
||||
PagingAndSorting pagingAndSorting,
|
||||
URI baseUri,
|
||||
RepositoryInformation repoInfo) {
|
||||
this.config = config;
|
||||
this.request = request;
|
||||
this.pagingAndSorting = pagingAndSorting;
|
||||
this.baseUri = baseUri;
|
||||
this.repoInfo = repoInfo;
|
||||
this.repoMapping = getResourceMapping(config, repoInfo);
|
||||
if(null == repoMapping) {
|
||||
this.repoLink = null;
|
||||
this.repository = null;
|
||||
this.repoMethodInvoker = null;
|
||||
this.persistentEntity = null;
|
||||
this.entityMapping = null;
|
||||
} else {
|
||||
this.repoLink = new Link(buildUri(baseUri, repoMapping.getPath()).toString(), repoMapping.getRel());
|
||||
this.repository = repositories.getRepositoryFor(repoInfo.getDomainType());
|
||||
this.persistentEntity = repositories.getPersistentEntity(repoInfo.getDomainType());
|
||||
this.repoMethodInvoker = new RepositoryMethodInvoker(repository, repoInfo, persistentEntity);
|
||||
this.entityMapping = getResourceMapping(config, persistentEntity);
|
||||
}
|
||||
}
|
||||
|
||||
HttpServletRequest getRequest() {
|
||||
return request;
|
||||
}
|
||||
|
||||
PagingAndSorting getPagingAndSorting() {
|
||||
return pagingAndSorting;
|
||||
}
|
||||
|
||||
URI getBaseUri() {
|
||||
return baseUri;
|
||||
}
|
||||
|
||||
RepositoryInformation getRepositoryInformation() {
|
||||
return repoInfo;
|
||||
}
|
||||
|
||||
ResourceMapping getRepositoryResourceMapping() {
|
||||
return repoMapping;
|
||||
}
|
||||
|
||||
Link getRepositoryLink() {
|
||||
return repoLink;
|
||||
}
|
||||
|
||||
Object getRepository() {
|
||||
return repository;
|
||||
}
|
||||
|
||||
RepositoryMethodInvoker getRepositoryMethodInvoker() {
|
||||
return repoMethodInvoker;
|
||||
}
|
||||
|
||||
PersistentEntity getPersistentEntity() {
|
||||
return persistentEntity;
|
||||
}
|
||||
|
||||
ResourceMapping getPersistentEntityResourceMapping() {
|
||||
return entityMapping;
|
||||
}
|
||||
|
||||
void addNextLink(Page page, List<Link> links) {
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromUri(baseUri);
|
||||
// Add existing query parameters
|
||||
addQueryParameters(request, builder);
|
||||
|
||||
builder.queryParam(config.getPageParamName(), page.getNumber() + 1)
|
||||
.queryParam(config.getLimitParamName(), pagingAndSorting.getPageSize());
|
||||
|
||||
links.add(new Link(builder.build().toString(), "page.next"));
|
||||
}
|
||||
|
||||
void addPrevLink(Page page, List<Link> links) {
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromUri(baseUri);
|
||||
// Add existing query parameters
|
||||
addQueryParameters(request, builder);
|
||||
|
||||
builder.queryParam(config.getPageParamName(), page.getNumber() - 1)
|
||||
.queryParam(config.getLimitParamName(), pagingAndSorting.getPageSize());
|
||||
|
||||
links.add(new Link(builder.build().toString(), "page.previous"));
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"}) Link buildEntitySelfLink(Object o, ConversionService conversionService) {
|
||||
BeanWrapper bean = BeanWrapper.create(o, conversionService);
|
||||
Object id = bean.getProperty(persistentEntity.getIdProperty());
|
||||
URI uri = buildUri(baseUri, repoMapping.getPath(), id.toString());
|
||||
return new Link(uri.toString(), "self");
|
||||
}
|
||||
|
||||
private void addQueryParameters(HttpServletRequest request,
|
||||
UriComponentsBuilder builder) {
|
||||
for(Enumeration<String> names = request.getParameterNames(); names.hasMoreElements(); ) {
|
||||
String name = names.nextElement();
|
||||
String value = request.getParameter(name);
|
||||
if(name.equals(config.getPageParamName()) || name.equals(config.getLimitParamName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.queryParam(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import java.net.URI;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
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.webmvc.support.PagingAndSorting;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class RepositoryRestRequestHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
@Autowired
|
||||
private RepositoryRestConfiguration config;
|
||||
@Autowired
|
||||
private Repositories repositories;
|
||||
@Autowired
|
||||
private RepositoryInformationHandlerMethodArgumentResolver repoInfoResolver;
|
||||
@Autowired
|
||||
private PagingAndSortingMethodArgumentResolver pagingAndSortingResolver;
|
||||
@Autowired
|
||||
private BaseUriMethodArgumentResolver baseUriResolver;
|
||||
|
||||
@Override public boolean supportsParameter(MethodParameter parameter) {
|
||||
return RepositoryRestRequest.class.isAssignableFrom(parameter.getParameterType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter,
|
||||
ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest,
|
||||
WebDataBinderFactory binderFactory) throws Exception {
|
||||
PagingAndSorting pagingAndSorting = (PagingAndSorting)pagingAndSortingResolver.resolveArgument(parameter,
|
||||
mavContainer,
|
||||
webRequest,
|
||||
binderFactory);
|
||||
RepositoryInformation repoInfo = (RepositoryInformation)repoInfoResolver.resolveArgument(parameter,
|
||||
mavContainer,
|
||||
webRequest,
|
||||
binderFactory);
|
||||
URI baseUri = (URI)baseUriResolver.resolveArgument(parameter,
|
||||
mavContainer,
|
||||
webRequest,
|
||||
binderFactory);
|
||||
|
||||
return new RepositoryRestRequest(config,
|
||||
repositories,
|
||||
webRequest.getNativeRequest(HttpServletRequest.class),
|
||||
pagingAndSorting,
|
||||
baseUri,
|
||||
repoInfo);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.repository.support.DomainClassConverter;
|
||||
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.BaseUriAwareResource;
|
||||
import org.springframework.data.rest.repository.PersistentEntityResource;
|
||||
import org.springframework.data.rest.repository.invoke.RepositoryMethod;
|
||||
import org.springframework.data.rest.repository.invoke.RepositoryMethodInvoker;
|
||||
import org.springframework.data.rest.webmvc.support.JsonpResponse;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/{repository}/search")
|
||||
public class RepositorySearchController extends AbstractRepositoryRestController {
|
||||
|
||||
public RepositorySearchController(Repositories repositories,
|
||||
RepositoryRestConfiguration config,
|
||||
DomainClassConverter domainClassConverter,
|
||||
ConversionService conversionService) {
|
||||
super(repositories, config, domainClassConverter, conversionService);
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
method = RequestMethod.GET,
|
||||
produces = {
|
||||
"application/json",
|
||||
"application/x-spring-data-compact+json"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public Resource<?> list(RepositoryRestRequest repoRequest) {
|
||||
List<Link> links = new ArrayList<Link>();
|
||||
links.addAll(queryMethodLinks(repoRequest.getBaseUri(),
|
||||
repoRequest.getPersistentEntity().getType()));
|
||||
return new Resource<Object>(Collections.emptyList(), links);
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
method = RequestMethod.GET,
|
||||
produces = {
|
||||
"application/javascript"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public JsonpResponse<?> jsonpList(RepositoryRestRequest repoRequest) {
|
||||
return jsonpWrapResponse(repoRequest, list(repoRequest), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@RequestMapping(
|
||||
value = "/{method}",
|
||||
method = RequestMethod.GET,
|
||||
produces = {
|
||||
"application/json",
|
||||
"application/x-spring-data-verbose+json"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public Resource<?> query(RepositoryRestRequest repoRequest,
|
||||
@PathVariable String method)
|
||||
throws ResourceNotFoundException {
|
||||
RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker();
|
||||
if(repoMethodInvoker.getQueryMethods().isEmpty()) {
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
ResourceMapping repoMapping = repoRequest.getRepositoryResourceMapping();
|
||||
String methodName = repoMapping.getNameForPath(method);
|
||||
RepositoryMethod repoMethod = repoMethodInvoker.getQueryMethods().get(methodName);
|
||||
if(null == repoMethod) {
|
||||
for(RepositoryMethod queryMethod : repoMethodInvoker.getQueryMethods().values()) {
|
||||
String path = findPath(queryMethod.getMethod());
|
||||
if(path.equals(method)) {
|
||||
repoMethod = queryMethod;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(null == repoMethod) {
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
List<MethodParameter> methodParams = repoMethod.getParameters();
|
||||
Object[] paramValues = new Object[methodParams.size()];
|
||||
if(!methodParams.isEmpty()) {
|
||||
for(int i = 0; i < paramValues.length; i++) {
|
||||
MethodParameter param = methodParams.get(i);
|
||||
if(Pageable.class.isAssignableFrom(param.getParameterType())) {
|
||||
paramValues[i] = new PageRequest(repoRequest.getPagingAndSorting().getPageNumber(),
|
||||
repoRequest.getPagingAndSorting().getPageSize(),
|
||||
repoRequest.getPagingAndSorting().getSort());
|
||||
} else if(Sort.class.isAssignableFrom(param.getParameterType())) {
|
||||
paramValues[i] = repoRequest.getPagingAndSorting().getSort();
|
||||
} else {
|
||||
String paramName = repoMethod.getParameterNames().get(i);
|
||||
String[] queryParamVals = repoRequest.getRequest().getParameterValues(paramName);
|
||||
if(null == queryParamVals) {
|
||||
if(paramName.startsWith("arg")) {
|
||||
throw new IllegalArgumentException("No @Param annotation found on query method "
|
||||
+ repoMethod.getMethod().getName()
|
||||
+ " for parameter " + param.getParameterName());
|
||||
} else {
|
||||
throw new IllegalArgumentException("No query parameter specified for "
|
||||
+ repoMethod.getMethod().getName() + " param '"
|
||||
+ paramName + "'");
|
||||
}
|
||||
}
|
||||
paramValues[i] = methodParameterConversionService.convert(queryParamVals, param);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BaseUriAwareResource resources;
|
||||
List<Link> links = new ArrayList<Link>();
|
||||
Object result = repoMethodInvoker.invokeQueryMethod(repoMethod, paramValues);
|
||||
if(result instanceof Page) {
|
||||
Page page = (Page)result;
|
||||
if(page.hasPreviousPage()) {
|
||||
repoRequest.addPrevLink(page, links);
|
||||
}
|
||||
if(page.hasNextPage()) {
|
||||
repoRequest.addNextLink(page, links);
|
||||
}
|
||||
if(page.hasContent()) {
|
||||
resources = entitiesToResource(repoRequest, page.getContent());
|
||||
} else {
|
||||
resources = new BaseUriAwareResource(EMPTY_RESOURCE_LIST);
|
||||
}
|
||||
} else if(result instanceof Iterable) {
|
||||
resources = entitiesToResource(repoRequest, (Iterable)result);
|
||||
} else if(null == result) {
|
||||
resources = new BaseUriAwareResource(EMPTY_RESOURCE_LIST);
|
||||
} else {
|
||||
PersistentEntityResource per = PersistentEntityResource.wrap(repoRequest.getPersistentEntity(),
|
||||
result,
|
||||
repoRequest.getBaseUri());
|
||||
per.add(repoRequest.buildEntitySelfLink(result, conversionService));
|
||||
resources = per;
|
||||
}
|
||||
resources.setBaseUri(repoRequest.getBaseUri())
|
||||
.add(links);
|
||||
|
||||
return resources;
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
value = "/{method}",
|
||||
method = RequestMethod.GET,
|
||||
produces = {
|
||||
"application/x-spring-data-compact+json"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public Resource<?> queryCompact(RepositoryRestRequest repoRequest,
|
||||
@PathVariable String method)
|
||||
throws ResourceNotFoundException {
|
||||
List<Link> links = new ArrayList<Link>();
|
||||
|
||||
Resource<?> resource = query(repoRequest, method);
|
||||
links.addAll(resource.getLinks());
|
||||
|
||||
if(resource.getContent() instanceof Iterable) {
|
||||
Iterable iter = (Iterable)resource.getContent();
|
||||
for(Object obj : iter) {
|
||||
if(null != obj && obj instanceof Resource) {
|
||||
Resource res = (Resource)obj;
|
||||
links.add(resourceLink(repoRequest, res));
|
||||
}
|
||||
}
|
||||
} else if(resource.getContent() instanceof Resource) {
|
||||
Resource res = (Resource)resource.getContent();
|
||||
links.add(resourceLink(repoRequest, res));
|
||||
}
|
||||
|
||||
return new Resource<Object>(EMPTY_RESOURCE_LIST, links);
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
value = "/{method}",
|
||||
method = RequestMethod.GET,
|
||||
produces = {
|
||||
"application/javascript"
|
||||
}
|
||||
)
|
||||
@ResponseBody
|
||||
public JsonpResponse<? extends Resource<?>> jsonpQuery(RepositoryRestRequest repoRequest,
|
||||
@PathVariable String method)
|
||||
throws ResourceNotFoundException {
|
||||
return jsonpWrapResponse(repoRequest, query(repoRequest, method), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
private BaseUriAwareResource entitiesToResource(RepositoryRestRequest repoRequest, Iterable entities) {
|
||||
List<Resource<?>> resources = new ArrayList<Resource<?>>();
|
||||
for(Object obj : entities) {
|
||||
if(null == obj) {
|
||||
resources.add(null);
|
||||
break;
|
||||
}
|
||||
|
||||
PersistentEntity persistentEntity = repositories.getPersistentEntity(obj.getClass());
|
||||
if(null == persistentEntity) {
|
||||
resources.add(new BaseUriAwareResource<Object>(obj)
|
||||
.setBaseUri(repoRequest.getBaseUri()));
|
||||
continue;
|
||||
}
|
||||
|
||||
PersistentEntityResource per = PersistentEntityResource.wrap(persistentEntity, obj, repoRequest.getBaseUri());
|
||||
per.add(repoRequest.buildEntitySelfLink(obj, conversionService));
|
||||
resources.add(per);
|
||||
}
|
||||
return new BaseUriAwareResource(resources)
|
||||
.setBaseUri(repoRequest.getBaseUri());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
/**
|
||||
* Indicates a resource was not found.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class ResourceNotFoundException extends Exception {
|
||||
public ResourceNotFoundException() {
|
||||
super("Resource not found");
|
||||
}
|
||||
|
||||
public ResourceNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ResourceNotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.rest.repository.invoke.RepositoryMethodResponse;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.converter.AbstractHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A special {@link org.springframework.http.converter.HttpMessageConverter} that can take various input formats and
|
||||
* produce a plain-text list of URIs (or read the same).
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class UriListHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
|
||||
|
||||
public UriListHttpMessageConverter() {
|
||||
super(MediaTypes.URI_LIST);
|
||||
}
|
||||
|
||||
@Override protected boolean supports(Class<?> clazz) {
|
||||
return (RepositoryMethodResponse.class.isAssignableFrom(clazz)
|
||||
|| Resource.class.isAssignableFrom(clazz)
|
||||
|| Set.class.isAssignableFrom(clazz));
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Override
|
||||
protected Object readInternal(Class<?> clazz,
|
||||
HttpInputMessage inputMessage) throws IOException,
|
||||
HttpMessageNotReadableException {
|
||||
Assert.isTrue((Resource.class.isAssignableFrom(clazz) || Set.class.isAssignableFrom(clazz)),
|
||||
"Cannot read a text/uri-list into a " + clazz);
|
||||
|
||||
String rel = inputMessage.getHeaders().getFirst("x-spring-data-urilist-rel");
|
||||
if(null == rel && inputMessage instanceof ServletServerHttpRequest) {
|
||||
rel = ((ServletServerHttpRequest)inputMessage).getURI().getPath().substring(1).replaceAll("/", ".");
|
||||
}
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputMessage.getBody()));
|
||||
|
||||
Set<Link> links = new HashSet<Link>();
|
||||
String line;
|
||||
while(null != (line = reader.readLine())) {
|
||||
links.add(new Link(URI.create(line.trim()).toString(), rel));
|
||||
}
|
||||
|
||||
return (Set.class.isAssignableFrom(clazz) ? links : new Resource<String>("", links));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeInternal(Object links, HttpOutputMessage outputMessage)
|
||||
throws IOException,
|
||||
HttpMessageNotWritableException {
|
||||
OutputStream body = outputMessage.getBody();
|
||||
if(links instanceof Set) {
|
||||
for(Object o : (Set)links) {
|
||||
if(o instanceof Link) {
|
||||
body.write(((Link)o).getHref().getBytes());
|
||||
} else {
|
||||
body.write(o.toString().getBytes());
|
||||
}
|
||||
body.write('\n');
|
||||
}
|
||||
} else if(links instanceof RepositoryMethodResponse) {
|
||||
writeInternal(((RepositoryMethodResponse)links).getLinks(), outputMessage);
|
||||
} else if(links instanceof Resource) {
|
||||
writeInternal(((Resource)links).getLinks(), outputMessage);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.springframework.data.rest.webmvc.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Marker annotation to denote which {@link java.net.URI} parameter should be resolved to the request base URI.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@Target({ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface BaseURI {
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
package org.springframework.data.rest.webmvc.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportResource;
|
||||
import org.springframework.core.convert.support.ConfigurableConversionService;
|
||||
import org.springframework.data.repository.support.DomainClassConverter;
|
||||
import org.springframework.data.rest.config.RepositoryRestConfiguration;
|
||||
import org.springframework.data.rest.convert.ISO8601DateConverter;
|
||||
import org.springframework.data.rest.convert.UUIDConverter;
|
||||
import org.springframework.data.rest.repository.UriDomainClassConverter;
|
||||
import org.springframework.data.rest.repository.context.AnnotatedHandlerBeanPostProcessor;
|
||||
import org.springframework.data.rest.repository.context.RepositoriesFactoryBean;
|
||||
import org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener;
|
||||
import org.springframework.data.rest.repository.json.PersistentEntityJackson2Module;
|
||||
import org.springframework.data.rest.repository.json.PersistentEntityToJsonSchemaConverter;
|
||||
import org.springframework.data.rest.repository.support.DomainObjectMerger;
|
||||
import org.springframework.data.rest.webmvc.BaseUriMethodArgumentResolver;
|
||||
import org.springframework.data.rest.webmvc.PagingAndSortingMethodArgumentResolver;
|
||||
import org.springframework.data.rest.webmvc.PersistentEntityResourceHandlerMethodArgumentResolver;
|
||||
import org.springframework.data.rest.webmvc.RepositoryController;
|
||||
import org.springframework.data.rest.webmvc.RepositoryEntityController;
|
||||
import org.springframework.data.rest.webmvc.RepositoryEntityLinksMethodArgumentResolver;
|
||||
import org.springframework.data.rest.webmvc.RepositoryInformationHandlerMethodArgumentResolver;
|
||||
import org.springframework.data.rest.webmvc.RepositoryPropertyReferenceController;
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter;
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping;
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestRequestHandlerMethodArgumentResolver;
|
||||
import org.springframework.data.rest.webmvc.RepositorySearchController;
|
||||
import org.springframework.data.rest.webmvc.ServerHttpRequestMethodArgumentResolver;
|
||||
import org.springframework.data.rest.webmvc.convert.JsonpResponseHttpMessageConverter;
|
||||
import org.springframework.data.rest.webmvc.convert.UriListHttpMessageConverter;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
|
||||
|
||||
/**
|
||||
* Main application configuration for Spring Data REST. To customize how the exporter works, subclass this and override
|
||||
* any of the {@literal configure*} methods.
|
||||
* <p/>
|
||||
* Any XML files located in the classpath under the {@literal META-INF/spring-data-rest/} path will be automatically
|
||||
* found and loaded into this {@link org.springframework.context.ApplicationContext}.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@Configuration
|
||||
@ImportResource("classpath*:META-INF/spring-data-rest/**/*.xml")
|
||||
public class RepositoryRestMvcConfiguration {
|
||||
|
||||
private static final boolean IS_HIBERNATE4_MODULE_AVAILABLE = ClassUtils.isPresent(
|
||||
"com.fasterxml.jackson.datatype.hibernate4.Hibernate4Module",
|
||||
RepositoryRestMvcConfiguration.class.getClassLoader()
|
||||
);
|
||||
private static final boolean IS_JODA_MODULE_AVAILABLE = ClassUtils.isPresent(
|
||||
"com.fasterxml.jackson.datatype.joda.JodaModule",
|
||||
RepositoryRestMvcConfiguration.class.getClassLoader()
|
||||
);
|
||||
|
||||
@Bean public RepositoriesFactoryBean repositories() {
|
||||
return new RepositoriesFactoryBean();
|
||||
}
|
||||
|
||||
@Bean public DefaultFormattingConversionService defaultConversionService() {
|
||||
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
|
||||
conversionService.addConverter(UUIDConverter.INSTANCE);
|
||||
conversionService.addConverter(ISO8601DateConverter.INSTANCE);
|
||||
configureConversionService(conversionService);
|
||||
return conversionService;
|
||||
}
|
||||
|
||||
@Bean public DomainClassConverter<?> domainClassConverter() {
|
||||
return new DomainClassConverter<DefaultFormattingConversionService>(defaultConversionService());
|
||||
}
|
||||
|
||||
@Bean public UriDomainClassConverter uriDomainClassConverter() {
|
||||
return new UriDomainClassConverter();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link org.springframework.context.ApplicationListener} implementation for invoking {@link
|
||||
* org.springframework.validation.Validator} instances assigned to specific domain types.
|
||||
*/
|
||||
@Bean public ValidatingRepositoryEventListener validatingRepositoryEventListener() {
|
||||
ValidatingRepositoryEventListener listener = new ValidatingRepositoryEventListener();
|
||||
configureValidatingRepositoryEventListener(listener);
|
||||
return listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main configuration for the REST exporter.
|
||||
*/
|
||||
@Bean public RepositoryRestConfiguration config() {
|
||||
RepositoryRestConfiguration config = new RepositoryRestConfiguration();
|
||||
configureRepositoryRestConfiguration(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* For getting access to the {@link javax.persistence.EntityManagerFactory}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public PersistenceAnnotationBeanPostProcessor persistenceAnnotationBeanPostProcessor() {
|
||||
return new PersistenceAnnotationBeanPostProcessor();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link org.springframework.beans.factory.config.BeanPostProcessor} to turn beans annotated as {@link
|
||||
* org.springframework.data.rest.repository.annotation.RepositoryEventHandler}s.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public AnnotatedHandlerBeanPostProcessor annotatedHandlerBeanPostProcessor() {
|
||||
return new AnnotatedHandlerBeanPostProcessor();
|
||||
}
|
||||
|
||||
/**
|
||||
* For merging incoming objects materialized from JSON with existing domain objects loaded from the repository.
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
@Bean public DomainObjectMerger domainObjectMerger() throws Exception {
|
||||
return new DomainObjectMerger(
|
||||
repositories().getObject(),
|
||||
defaultConversionService()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The controller that handles top-level requests for listing what repositories are available.
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
@Bean public RepositoryController repositoryController() throws Exception {
|
||||
return new RepositoryController(
|
||||
repositories().getObject(),
|
||||
config(),
|
||||
domainClassConverter(),
|
||||
defaultConversionService()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The controller responsible for handling requests to display or those that modify an entity.
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
@Bean public RepositoryEntityController repositoryEntityController() throws Exception {
|
||||
return new RepositoryEntityController(
|
||||
repositories().getObject(),
|
||||
config(),
|
||||
domainClassConverter(),
|
||||
defaultConversionService()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The controller responsible for managing links of property references.
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
@Bean public RepositoryPropertyReferenceController propertyReferenceController() throws Exception {
|
||||
return new RepositoryPropertyReferenceController(
|
||||
repositories().getObject(),
|
||||
config(),
|
||||
domainClassConverter(),
|
||||
defaultConversionService()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The controller responsible for performing searches.
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
@Bean public RepositorySearchController repositorySearchController() throws Exception {
|
||||
return new RepositorySearchController(
|
||||
repositories().getObject(),
|
||||
config(),
|
||||
domainClassConverter(),
|
||||
defaultConversionService()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the base {@link java.net.URI} under which this application is configured.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public BaseUriMethodArgumentResolver baseUriMethodArgumentResolver() {
|
||||
return new BaseUriMethodArgumentResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the paging and sorting information from the query parameters based on the current configuration settings.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public PagingAndSortingMethodArgumentResolver pagingAndSortingMethodArgumentResolver() {
|
||||
return new PagingAndSortingMethodArgumentResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns an {@link javax.servlet.http.HttpServletRequest} into a {@link org.springframework.http.server.ServerHttpRequest}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public ServerHttpRequestMethodArgumentResolver serverHttpRequestMethodArgumentResolver() {
|
||||
return new ServerHttpRequestMethodArgumentResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the {@link org.springframework.data.repository.core.RepositoryInformation} for this request.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public RepositoryInformationHandlerMethodArgumentResolver repoInfoMethodArgumentResolver() {
|
||||
return new RepositoryInformationHandlerMethodArgumentResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience resolver that pulls together all the information needed to service a request.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public RepositoryRestRequestHandlerMethodArgumentResolver repoRequestArgumentResolver() {
|
||||
return new RepositoryRestRequestHandlerMethodArgumentResolver();
|
||||
}
|
||||
|
||||
@Bean public RepositoryEntityLinksMethodArgumentResolver entityLinksMethodArgumentResolver() {
|
||||
return new RepositoryEntityLinksMethodArgumentResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads incoming JSON into an entity.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public PersistentEntityResourceHandlerMethodArgumentResolver persistentEntityArgumentResolver() {
|
||||
List<HttpMessageConverter<?>> messageConverters = defaultMessageConverters();
|
||||
configureHttpMessageConverters(messageConverters);
|
||||
|
||||
return new PersistentEntityResourceHandlerMethodArgumentResolver(messageConverters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a domain class into a {@link org.springframework.data.rest.repository.json.JsonSchema}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public PersistentEntityToJsonSchemaConverter jsonSchemaConverter() {
|
||||
return new PersistentEntityToJsonSchemaConverter();
|
||||
}
|
||||
|
||||
/**
|
||||
* The Jackson {@link ObjectMapper} used internally.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public ObjectMapper objectMapper() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
|
||||
// Our special PersistentEntityResource Module
|
||||
objectMapper.registerModule(persistentEntityJackson2Module());
|
||||
// Hibernate types
|
||||
if(IS_HIBERNATE4_MODULE_AVAILABLE) {
|
||||
objectMapper.registerModule(new com.fasterxml.jackson.datatype.hibernate4.Hibernate4Module());
|
||||
}
|
||||
// JODA time
|
||||
if(IS_JODA_MODULE_AVAILABLE) {
|
||||
objectMapper.registerModule(new com.fasterxml.jackson.datatype.joda.JodaModule());
|
||||
}
|
||||
// Configure custom Modules
|
||||
configureJacksonObjectMapper(objectMapper);
|
||||
|
||||
return objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link HttpMessageConverter} used by Spring MVC to read and write JSON data.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public MappingJackson2HttpMessageConverter jacksonHttpMessageConverter() {
|
||||
MappingJackson2HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter();
|
||||
jacksonConverter.setObjectMapper(objectMapper());
|
||||
jacksonConverter.setSupportedMediaTypes(Arrays.asList(
|
||||
MediaType.APPLICATION_JSON,
|
||||
MediaType.valueOf("application/schema+json"),
|
||||
MediaType.valueOf("application/x-spring-data-verbose+json"),
|
||||
MediaType.valueOf("application/x-spring-data-compact+json")
|
||||
));
|
||||
return jacksonConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link HttpMessageConverter} used to create JSONP responses.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public JsonpResponseHttpMessageConverter jsonpHttpMessageConverter() {
|
||||
return new JsonpResponseHttpMessageConverter(jacksonHttpMessageConverter());
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link HttpMessageConverter} used to create {@literal text/uri-list} responses.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public UriListHttpMessageConverter uriListHttpMessageConverter() {
|
||||
return new UriListHttpMessageConverter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Special {@link org.springframework.web.servlet.HandlerAdapter} that only recognizes handler methods defined in
|
||||
* the provided controller classes.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public RepositoryRestHandlerAdapter repositoryExporterHandlerAdapter() {
|
||||
List<HttpMessageConverter<?>> messageConverters = defaultMessageConverters();
|
||||
configureHttpMessageConverters(messageConverters);
|
||||
|
||||
RepositoryRestHandlerAdapter handlerAdapter = new RepositoryRestHandlerAdapter();
|
||||
handlerAdapter.setMessageConverters(messageConverters);
|
||||
handlerAdapter.setCustomArgumentResolvers(defaultMethodArgumentResolvers());
|
||||
|
||||
return handlerAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Special {@link org.springframework.web.servlet.HandlerMapping} that only recognizes handler methods defined in
|
||||
* the provided controller classes.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public RepositoryRestHandlerMapping repositoryExporterHandlerMapping() {
|
||||
return new RepositoryRestHandlerMapping();
|
||||
}
|
||||
|
||||
/**
|
||||
* Jackson module responsible for intelligently serializing and deserializing JSON that corresponds to an entity.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public PersistentEntityJackson2Module persistentEntityJackson2Module() {
|
||||
return new PersistentEntityJackson2Module(defaultConversionService());
|
||||
}
|
||||
|
||||
/**
|
||||
* Bean for looking up methods annotated with {@link org.springframework.web.bind.annotation.ExceptionHandler}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver() {
|
||||
ExceptionHandlerExceptionResolver er = new ExceptionHandlerExceptionResolver();
|
||||
er.setCustomArgumentResolvers(defaultMethodArgumentResolvers());
|
||||
|
||||
List<HttpMessageConverter<?>> messageConverters = defaultMessageConverters();
|
||||
configureHttpMessageConverters(messageConverters);
|
||||
|
||||
er.setMessageConverters(messageConverters);
|
||||
configureExceptionHandlerExceptionResolver(er);
|
||||
|
||||
return er;
|
||||
}
|
||||
|
||||
private List<HttpMessageConverter<?>> defaultMessageConverters() {
|
||||
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
|
||||
messageConverters.add(jacksonHttpMessageConverter());
|
||||
messageConverters.add(jsonpHttpMessageConverter());
|
||||
messageConverters.add(uriListHttpMessageConverter());
|
||||
return messageConverters;
|
||||
}
|
||||
|
||||
private List<HandlerMethodArgumentResolver> defaultMethodArgumentResolvers() {
|
||||
return Arrays.asList(baseUriMethodArgumentResolver(),
|
||||
pagingAndSortingMethodArgumentResolver(),
|
||||
serverHttpRequestMethodArgumentResolver(),
|
||||
repoInfoMethodArgumentResolver(),
|
||||
repoRequestArgumentResolver(),
|
||||
persistentEntityArgumentResolver(),
|
||||
entityLinksMethodArgumentResolver());
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method to add additional configuration.
|
||||
*
|
||||
* @param config
|
||||
* Main configuration bean.
|
||||
*/
|
||||
protected void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method to add your own converters.
|
||||
*
|
||||
* @param conversionService
|
||||
* Default ConversionService bean.
|
||||
*/
|
||||
protected void configureConversionService(ConfigurableConversionService conversionService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method to add validators manually.
|
||||
*
|
||||
* @param validatingListener
|
||||
* The {@link org.springframework.context.ApplicationListener} responsible for invoking {@link
|
||||
* org.springframework.validation.Validator} instances.
|
||||
*/
|
||||
protected void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link ExceptionHandlerExceptionResolver}.
|
||||
*
|
||||
* @param exceptionResolver
|
||||
* The default exception resolver on which you can add custom argument resolvers.
|
||||
*/
|
||||
protected void configureExceptionHandlerExceptionResolver(ExceptionHandlerExceptionResolver exceptionResolver) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the available {@link HttpMessageConverter}s by adding your own.
|
||||
*
|
||||
* @param messageConverters
|
||||
* The converters to be used by the system.
|
||||
*/
|
||||
protected void configureHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the Jackson {@link ObjectMapper} directly.
|
||||
*
|
||||
* @param objectMapper
|
||||
* The {@literal ObjectMapper} to be used by the system.
|
||||
*/
|
||||
protected void configureJacksonObjectMapper(ObjectMapper objectMapper) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.springframework.data.rest.webmvc.convert;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.rest.webmvc.support.JsonpResponse;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class JsonpResponseHttpMessageConverter implements HttpMessageConverter<JsonpResponse<?>> {
|
||||
|
||||
private static final MediaType APPLICATION_JAVASCRIPT = MediaType.valueOf("application/javascript");
|
||||
private static final List<MediaType> SUPPORTED_TYPES = Arrays.asList(
|
||||
APPLICATION_JAVASCRIPT
|
||||
);
|
||||
|
||||
private final MappingJackson2HttpMessageConverter jacksonConverter;
|
||||
|
||||
public JsonpResponseHttpMessageConverter(MappingJackson2HttpMessageConverter jacksonConverter) {
|
||||
this.jacksonConverter = jacksonConverter;
|
||||
}
|
||||
|
||||
@Override public boolean canRead(Class<?> clazz, MediaType mediaType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override public boolean canWrite(Class<?> clazz, MediaType mediaType) {
|
||||
return JsonpResponse.class.isAssignableFrom(clazz) && mediaType.getSubtype().contains("javascript");
|
||||
}
|
||||
|
||||
@Override public List<MediaType> getSupportedMediaTypes() {
|
||||
return SUPPORTED_TYPES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonpResponse<?> read(Class<? extends JsonpResponse<?>> clazz,
|
||||
HttpInputMessage inputMessage) throws IOException,
|
||||
HttpMessageNotReadableException {
|
||||
throw new HttpMessageNotReadableException("JSONP messages are not readable.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(JsonpResponse<?> jsonpResponse,
|
||||
MediaType contentType,
|
||||
final HttpOutputMessage outputMessage) throws IOException,
|
||||
HttpMessageNotWritableException {
|
||||
final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
bytes.write((jsonpResponse.getCallbackParam() + "(").getBytes());
|
||||
|
||||
jacksonConverter.write(jsonpResponse.getResponseEntity().getBody(),
|
||||
MediaType.APPLICATION_JSON,
|
||||
new HttpOutputMessage() {
|
||||
@Override public OutputStream getBody() throws IOException {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@Override public HttpHeaders getHeaders() {
|
||||
return outputMessage.getHeaders();
|
||||
}
|
||||
});
|
||||
|
||||
bytes.write(");".getBytes());
|
||||
|
||||
byte[] byteArray = bytes.toByteArray();
|
||||
|
||||
outputMessage.getHeaders().setContentType(APPLICATION_JAVASCRIPT);
|
||||
outputMessage.getHeaders().setContentLength(byteArray.length);
|
||||
outputMessage.getBody().flush();
|
||||
outputMessage.getBody().write(byteArray);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.springframework.data.rest.webmvc.convert;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class UriListHttpMessageConverter implements HttpMessageConverter<Resource<?>> {
|
||||
|
||||
private static final List<MediaType> MEDIA_TYPES = new ArrayList<MediaType>();
|
||||
|
||||
static {
|
||||
MEDIA_TYPES.add(MediaType.parseMediaType("text/uri-list"));
|
||||
}
|
||||
|
||||
@Override public boolean canRead(Class<?> clazz, MediaType mediaType) {
|
||||
if(null == mediaType) {
|
||||
return false;
|
||||
}
|
||||
return Resource.class.isAssignableFrom(clazz) && mediaType.getSubtype().contains("uri-list");
|
||||
}
|
||||
|
||||
@Override public boolean canWrite(Class<?> clazz, MediaType mediaType) {
|
||||
return canRead(clazz, mediaType);
|
||||
}
|
||||
|
||||
@Override public List<MediaType> getSupportedMediaTypes() {
|
||||
return MEDIA_TYPES;
|
||||
}
|
||||
|
||||
@Override public Resource<?> read(Class<? extends Resource<?>> clazz,
|
||||
HttpInputMessage inputMessage)
|
||||
throws IOException,
|
||||
HttpMessageNotReadableException {
|
||||
List<Link> links = new ArrayList<Link>();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputMessage.getBody()));
|
||||
String line;
|
||||
while(null != (line = reader.readLine())) {
|
||||
links.add(new Link(line));
|
||||
}
|
||||
return new Resource<Object>(Collections.emptyList(), links);
|
||||
}
|
||||
|
||||
@Override public void write(Resource<?> resource,
|
||||
MediaType contentType,
|
||||
HttpOutputMessage outputMessage)
|
||||
throws IOException,
|
||||
HttpMessageNotWritableException {
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputMessage.getBody()));
|
||||
for(Link link : resource.getLinks()) {
|
||||
writer.write(link.getHref());
|
||||
writer.newLine();
|
||||
}
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc.json;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.codehaus.jackson.JsonEncoding;
|
||||
import org.codehaus.jackson.JsonGenerator;
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.springframework.data.rest.webmvc.MediaTypes;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
|
||||
|
||||
/**
|
||||
* Utility class for creating a custom-configured {@see MappingJacksonHttpMessageConverter} that has our own
|
||||
* serializers and {@see MediaType} mappings on it.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public abstract class JacksonUtil {
|
||||
|
||||
private JacksonUtil() {
|
||||
}
|
||||
|
||||
public static MappingJacksonHttpMessageConverter createJacksonHttpMessageConverter(final ObjectMapper objectMapper) {
|
||||
// We want to support all our custom types of JSON and also the catch-all
|
||||
MappingJacksonHttpMessageConverter jsonConverter = new MappingJacksonHttpMessageConverter() {
|
||||
{
|
||||
setSupportedMediaTypes(Arrays.asList(
|
||||
MediaType.APPLICATION_JSON,
|
||||
MediaTypes.COMPACT_JSON,
|
||||
MediaTypes.VERBOSE_JSON
|
||||
));
|
||||
}
|
||||
|
||||
@Override public boolean canRead(Class<?> clazz, MediaType mediaType) {
|
||||
if(!canRead(mediaType)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override public boolean canWrite(Class<?> clazz, MediaType mediaType) {
|
||||
if(!canWrite(mediaType)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeInternal(Object object,
|
||||
HttpOutputMessage outputMessage) throws IOException,
|
||||
HttpMessageNotWritableException {
|
||||
JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
|
||||
// Believe it or not, this is the only way to get pretty-printing from Jackson in this configuration
|
||||
JsonGenerator jsonGenerator = objectMapper
|
||||
.getJsonFactory()
|
||||
.createJsonGenerator(outputMessage.getBody(), encoding)
|
||||
.useDefaultPrettyPrinter();
|
||||
try {
|
||||
objectMapper.writeValue(jsonGenerator, object);
|
||||
} catch(IOException ex) {
|
||||
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
};
|
||||
jsonConverter.setObjectMapper(objectMapper);
|
||||
|
||||
return jsonConverter;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc.json;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.codehaus.jackson.map.SerializationConfig;
|
||||
import org.codehaus.jackson.schema.JsonSchema;
|
||||
import org.springframework.data.rest.repository.RepositoryExporterSupport;
|
||||
import org.springframework.data.rest.repository.RepositoryMetadata;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* A controller to output JSON schema based on Jackson's schema generator.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class JsonSchemaController extends RepositoryExporterSupport<JsonSchemaController> {
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
{
|
||||
mapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, true);
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
value = "/{repository}/schema",
|
||||
method = RequestMethod.GET,
|
||||
produces = "application/json"
|
||||
)
|
||||
@ResponseBody
|
||||
public ResponseEntity<?> schemaForRepository(URI baseUri,
|
||||
@PathVariable String repository) throws IOException {
|
||||
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
|
||||
if(null == repoMeta) {
|
||||
return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
JsonSchema schema = mapper.generateJsonSchema(repoMeta.domainType());
|
||||
|
||||
URI schemaUri = UriComponentsBuilder.fromUri(baseUri)
|
||||
.pathSegment(repository, "schema")
|
||||
.build()
|
||||
.toUri();
|
||||
URI requestUri = UriComponentsBuilder.fromUri(baseUri)
|
||||
.pathSegment(repository)
|
||||
.build()
|
||||
.toUri();
|
||||
Resource<JsonSchema> resource = new Resource<JsonSchema>(schema,
|
||||
new Link(schemaUri.toString(), "self"),
|
||||
new Link(requestUri.toString(), repoMeta.rel()));
|
||||
|
||||
String output = mapper.writeValueAsString(resource);
|
||||
|
||||
return new ResponseEntity<String>(output, HttpStatus.OK);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,385 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc.json;
|
||||
|
||||
import static org.springframework.data.rest.core.util.UriUtils.*;
|
||||
import static org.springframework.data.util.ClassTypeInformation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.Multimap;
|
||||
import org.codehaus.jackson.JsonGenerationException;
|
||||
import org.codehaus.jackson.JsonGenerator;
|
||||
import org.codehaus.jackson.JsonParser;
|
||||
import org.codehaus.jackson.JsonProcessingException;
|
||||
import org.codehaus.jackson.JsonToken;
|
||||
import org.codehaus.jackson.Version;
|
||||
import org.codehaus.jackson.map.DeserializationContext;
|
||||
import org.codehaus.jackson.map.KeyDeserializer;
|
||||
import org.codehaus.jackson.map.SerializerProvider;
|
||||
import org.codehaus.jackson.map.deser.std.StdDeserializer;
|
||||
import org.codehaus.jackson.map.module.SimpleDeserializers;
|
||||
import org.codehaus.jackson.map.module.SimpleKeyDeserializers;
|
||||
import org.codehaus.jackson.map.module.SimpleModule;
|
||||
import org.codehaus.jackson.map.module.SimpleSerializers;
|
||||
import org.codehaus.jackson.map.ser.std.SerializerBase;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.GenericConversionService;
|
||||
import org.springframework.data.rest.repository.AttributeMetadata;
|
||||
import org.springframework.data.rest.repository.RepositoryExporter;
|
||||
import org.springframework.data.rest.repository.RepositoryMetadata;
|
||||
import org.springframework.data.rest.repository.UriToDomainObjectUriResolver;
|
||||
import org.springframework.data.rest.webmvc.EntityToResourceConverter;
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestConfiguration;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.ResourceProcessor;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
|
||||
/**
|
||||
* Special implementation of a Jackson {@link org.codehaus.jackson.map.Module} to handle properly serializing and
|
||||
* deserializing entities with links.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class RepositoryAwareJacksonModule extends SimpleModule implements InitializingBean {
|
||||
|
||||
@Autowired(required = false)
|
||||
private RepositoryRestConfiguration config = RepositoryRestConfiguration.DEFAULT;
|
||||
@Autowired(required = false)
|
||||
protected List<RepositoryExporter> repositoryExporters = Collections.emptyList();
|
||||
@Autowired(required = false)
|
||||
private List<ConversionService> conversionServices = Collections.emptyList();
|
||||
@Autowired(required = false)
|
||||
private List<ResourceProcessor<Resource<?>>> resourceProcessors = Collections.emptyList();
|
||||
private Multimap<Class<?>, ResourceProcessor<Resource<?>>> resourceProcessorMap = ArrayListMultimap.create();
|
||||
@Autowired
|
||||
private UriToDomainObjectUriResolver domainObjectResolver;
|
||||
private final GenericConversionService conversionService = new GenericConversionService();
|
||||
|
||||
private final SimpleSerializers sers = new SimpleSerializers();
|
||||
private final SimpleDeserializers dsers = new SimpleDeserializers();
|
||||
private final SimpleSerializers keySers = new SimpleSerializers();
|
||||
private final SimpleKeyDeserializers keyDsers = new SimpleKeyDeserializers();
|
||||
|
||||
public RepositoryAwareJacksonModule() {
|
||||
super("RepositoryAwareJacksonModule", Version.unknownVersion());
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Override public void afterPropertiesSet() throws Exception {
|
||||
for(RepositoryExporter repoExp : repositoryExporters) {
|
||||
for(String repoName : new ArrayList<String>(repoExp.repositoryNames())) {
|
||||
RepositoryMetadata repoMeta = repoExp.repositoryMetadataFor(repoName);
|
||||
Class domainType = repoMeta.entityMetadata().type();
|
||||
TypeInformation<?> domainTypeInfo = from(domainType);
|
||||
|
||||
for(ResourceProcessor<Resource<?>> rp : resourceProcessors) {
|
||||
TypeInformation<?> resourceType = from(rp.getClass())
|
||||
.getSuperTypeInformation(ResourceProcessor.class)
|
||||
.getComponentType();
|
||||
Class<?> processorType = resourceType.getType();
|
||||
TypeInformation<?> componentType = resourceType.getComponentType();
|
||||
|
||||
if(Resource.class.isAssignableFrom(processorType) && componentType.isAssignableFrom(domainTypeInfo)) {
|
||||
resourceProcessorMap.put(domainType, rp);
|
||||
}
|
||||
}
|
||||
|
||||
conversionService.addConverter(domainType, Resource.class, new EntityToResourceConverter(config, repoMeta));
|
||||
|
||||
sers.addSerializer(domainType, new DomainObjectToResourceSerializer(domainType));
|
||||
keySers.addSerializer(domainType, new DomainObjectToStringKeySerializer(domainType, repoMeta));
|
||||
|
||||
dsers.addDeserializer(domainType, new LinkToDomainObjectDeserializer(domainType, repoMeta));
|
||||
keyDsers.addDeserializer(domainType, new KeyToDomainObjectDeserializer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void setupModule(SetupContext context) {
|
||||
context.addSerializers(sers);
|
||||
context.addKeySerializers(keySers);
|
||||
context.addDeserializers(dsers);
|
||||
context.addKeyDeserializers(keyDsers);
|
||||
}
|
||||
|
||||
private class DomainObjectToResourceSerializer extends SerializerBase<Object> {
|
||||
private DomainObjectToResourceSerializer(Class<Object> t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Override public void serialize(Object value,
|
||||
JsonGenerator jgen,
|
||||
SerializerProvider provider) throws IOException,
|
||||
JsonGenerationException {
|
||||
if(null == value) {
|
||||
provider.defaultSerializeNull(jgen);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!conversionService.canConvert(value.getClass(), Resource.class)) {
|
||||
provider.defaultSerializeValue(value, jgen);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process the resource first to catch user stuff
|
||||
Resource<?> resource = new Resource<Object>(value);
|
||||
for(ResourceProcessor<Resource<?>> rp : resourceProcessorMap.get(value.getClass())) {
|
||||
resource = rp.process(resource);
|
||||
}
|
||||
// Maybe convert the resource so we can extract linked properties
|
||||
if(null == resource.getContent()) {
|
||||
provider.defaultSerializeNull(jgen);
|
||||
return;
|
||||
}
|
||||
|
||||
Class<?> sourceType = resource.getContent().getClass();
|
||||
ConversionService entityConversionSvc = conversionService;
|
||||
for(ConversionService cs : conversionServices) {
|
||||
if(cs.canConvert(sourceType, Resource.class)) {
|
||||
entityConversionSvc = cs;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(entityConversionSvc.canConvert(sourceType, Resource.class)) {
|
||||
List<Link> links = resource.getLinks();
|
||||
resource = entityConversionSvc.convert(value, Resource.class);
|
||||
resource.add(links);
|
||||
}
|
||||
|
||||
jgen.writeObject(resource);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class DomainObjectToStringKeySerializer extends SerializerBase<Object> {
|
||||
|
||||
private final RepositoryMetadata repoMeta;
|
||||
private final AttributeMetadata idAttr;
|
||||
|
||||
private DomainObjectToStringKeySerializer(Class<Object> t, RepositoryMetadata repoMeta) {
|
||||
super(t);
|
||||
this.repoMeta = repoMeta;
|
||||
if(null != repoMeta) {
|
||||
idAttr = repoMeta.entityMetadata().idAttribute();
|
||||
} else {
|
||||
idAttr = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void serialize(Object value,
|
||||
JsonGenerator jgen,
|
||||
SerializerProvider provider) throws IOException,
|
||||
JsonGenerationException {
|
||||
if(null == value) {
|
||||
provider.defaultSerializeNull(jgen);
|
||||
return;
|
||||
}
|
||||
if(null == repoMeta) {
|
||||
provider.defaultSerializeValue(value, jgen);
|
||||
return;
|
||||
}
|
||||
|
||||
Serializable serId = (Serializable)idAttr.get(value);
|
||||
String sId = null;
|
||||
for(ConversionService cs : conversionServices) {
|
||||
if(cs.canConvert(idAttr.type(), String.class)) {
|
||||
sId = cs.convert(serId, String.class);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(null == sId) {
|
||||
sId = serId.toString();
|
||||
}
|
||||
|
||||
URI href = buildUri(config.getBaseUri(), repoMeta.name(), sId);
|
||||
|
||||
jgen.writeString("@" + href.toString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class LinkToDomainObjectDeserializer extends StdDeserializer<Object> {
|
||||
|
||||
protected final RepositoryMetadata repoMeta;
|
||||
|
||||
private LinkToDomainObjectDeserializer(Class<?> vc, RepositoryMetadata repoMeta) {
|
||||
super(vc);
|
||||
this.repoMeta = repoMeta;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Override public Object deserialize(JsonParser jp,
|
||||
DeserializationContext ctxt) throws IOException,
|
||||
JsonProcessingException {
|
||||
Object entity = BeanUtils.instantiateClass(getValueClass());
|
||||
for(JsonToken tok = jp.nextToken(); tok != JsonToken.END_OBJECT; tok = jp.nextToken()) {
|
||||
String name = jp.getCurrentName();
|
||||
switch(tok) {
|
||||
case FIELD_NAME: {
|
||||
// Read the attribute metadata
|
||||
AttributeMetadata attrMeta = repoMeta.entityMetadata().attribute(name);
|
||||
Object val = null;
|
||||
|
||||
if(name.startsWith("@http")) {
|
||||
entity = domainObjectResolver.resolve(
|
||||
config.getBaseUri(),
|
||||
URI.create(name.substring(1))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if("href".equals(name)) {
|
||||
entity = domainObjectResolver.resolve(
|
||||
config.getBaseUri(),
|
||||
URI.create(jp.nextTextValue())
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if("rel".equals(name)) {
|
||||
// rel is currently ignored
|
||||
continue;
|
||||
}
|
||||
|
||||
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 == attrMeta) {
|
||||
// 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(attrMeta.isCollectionLike()) {
|
||||
Collection c = attrMeta.asCollection(entity);
|
||||
if(null == c || c == Collections.emptyList()) {
|
||||
c = new ArrayList();
|
||||
}
|
||||
|
||||
if((tok = jp.nextToken()) == JsonToken.START_ARRAY) {
|
||||
while((tok = jp.nextToken()) != JsonToken.END_ARRAY) {
|
||||
Object cval = jp.readValueAs(attrMeta.elementType());
|
||||
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(attrMeta.isSetLike()) {
|
||||
Set s = attrMeta.asSet(entity);
|
||||
if(null == s || s == Collections.emptySet()) {
|
||||
s = new HashSet();
|
||||
}
|
||||
|
||||
if((tok = jp.nextToken()) == JsonToken.START_ARRAY) {
|
||||
while((tok = jp.nextToken()) != JsonToken.END_ARRAY) {
|
||||
Object sval = jp.readValueAs(attrMeta.elementType());
|
||||
s.add(sval);
|
||||
}
|
||||
|
||||
val = s;
|
||||
|
||||
} else if(tok == JsonToken.VALUE_NULL) {
|
||||
val = null;
|
||||
} else {
|
||||
throw new HttpMessageNotReadableException("Cannot read a JSON " + tok + " as a Set.");
|
||||
}
|
||||
} else if(attrMeta.isMapLike()) {
|
||||
Map m = attrMeta.asMap(entity);
|
||||
if(null == m || m == Collections.emptyMap()) {
|
||||
m = new HashMap();
|
||||
}
|
||||
|
||||
if((tok = jp.nextToken()) == JsonToken.START_OBJECT) {
|
||||
do {
|
||||
name = jp.getCurrentName();
|
||||
Object mkey = (
|
||||
name.startsWith("@http")
|
||||
? domainObjectResolver.resolve(
|
||||
config.getBaseUri(),
|
||||
URI.create(name.substring(1))
|
||||
)
|
||||
: name
|
||||
);
|
||||
tok = jp.nextToken();
|
||||
Object mval = jp.readValueAs(attrMeta.elementType());
|
||||
|
||||
m.put(mkey, 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(attrMeta.type());
|
||||
}
|
||||
}
|
||||
|
||||
if(null != val) {
|
||||
attrMeta.set(val, entity);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class KeyToDomainObjectDeserializer extends KeyDeserializer {
|
||||
@Override public Object deserializeKey(String key,
|
||||
DeserializationContext ctxt) throws IOException,
|
||||
JsonProcessingException {
|
||||
if(key.startsWith("@http")) {
|
||||
return domainObjectResolver.resolve(
|
||||
config.getBaseUri(),
|
||||
URI.create(key.substring(1))
|
||||
);
|
||||
} else {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.springframework.data.rest.webmvc.support;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.springframework.hateoas.core.LinkBuilderSupport;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class BaseUriLinkBuilder extends LinkBuilderSupport {
|
||||
|
||||
public BaseUriLinkBuilder(UriComponentsBuilder builder) {
|
||||
super(builder);
|
||||
}
|
||||
|
||||
public static BaseUriLinkBuilder create(URI baseUri) {
|
||||
return new BaseUriLinkBuilder(UriComponentsBuilder.fromUri(baseUri));
|
||||
}
|
||||
|
||||
@Override protected BaseUriLinkBuilder getThis() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override protected BaseUriLinkBuilder createNewInstance(UriComponentsBuilder builder) {
|
||||
return new BaseUriLinkBuilder(builder);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.springframework.data.rest.webmvc.support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.ConstraintViolationException;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.springframework.context.MessageSource;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class ConstraintViolationExceptionMessage {
|
||||
|
||||
private final ConstraintViolationException cve;
|
||||
private final List<ConstraintViolationMessage> messages = new ArrayList<ConstraintViolationMessage>();
|
||||
|
||||
public ConstraintViolationExceptionMessage(ConstraintViolationException cve, MessageSource msgSrc) {
|
||||
this.cve = cve;
|
||||
for(ConstraintViolation cv : cve.getConstraintViolations()) {
|
||||
messages.add(new ConstraintViolationMessage(cv, msgSrc));
|
||||
}
|
||||
}
|
||||
|
||||
@JsonProperty("cause")
|
||||
public String getCause() {
|
||||
return cve.getMessage();
|
||||
}
|
||||
|
||||
@JsonProperty("messages")
|
||||
public List<ConstraintViolationMessage> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.springframework.data.rest.webmvc.support;
|
||||
|
||||
import static java.lang.String.*;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.springframework.context.MessageSource;
|
||||
|
||||
/**
|
||||
* A helper class to encapsulate {@link ConstraintViolation} errors.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class ConstraintViolationMessage {
|
||||
|
||||
private final ConstraintViolation<?> violation;
|
||||
private final String message;
|
||||
|
||||
public ConstraintViolationMessage(ConstraintViolation<?> violation, MessageSource msgSrc) {
|
||||
this.violation = violation;
|
||||
this.message = msgSrc.getMessage(violation.getMessageTemplate(),
|
||||
new Object[]{
|
||||
violation.getLeafBean().getClass().getSimpleName(),
|
||||
violation.getPropertyPath().toString(),
|
||||
violation.getInvalidValue()
|
||||
},
|
||||
violation.getMessage(),
|
||||
null);
|
||||
}
|
||||
|
||||
@JsonProperty("entity")
|
||||
public String getEntity() {
|
||||
return violation.getRootBean().getClass().getName();
|
||||
}
|
||||
|
||||
@JsonProperty("message")
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
@JsonProperty("invalidValue")
|
||||
public String getInvalidValue() {
|
||||
return format("%s", violation.getInvalidValue());
|
||||
}
|
||||
|
||||
@JsonProperty("property")
|
||||
public String getProperty() {
|
||||
return violation.getPropertyPath().toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.springframework.data.rest.webmvc.support;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* A helper that renders an {@link Exception} JSON-friendly.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class ExceptionMessage {
|
||||
|
||||
private final Throwable exception;
|
||||
|
||||
public ExceptionMessage(Throwable exception) {
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
@JsonProperty("message")
|
||||
public String getMessage() {
|
||||
return exception.getMessage();
|
||||
}
|
||||
|
||||
@JsonProperty("cause")
|
||||
public ExceptionMessage getCause() {
|
||||
if(null != exception.getCause()) {
|
||||
return new ExceptionMessage(exception.getCause());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.springframework.data.rest.webmvc.support;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletRegistration;
|
||||
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestDispatcherServlet;
|
||||
|
||||
/**
|
||||
* Helper class to HttpServletRequest helpers.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public abstract class HttpRequestUtils {
|
||||
|
||||
/**
|
||||
* Strip a servlet registration mapping from the request URI.
|
||||
*
|
||||
* @param requestUri
|
||||
* The request URI to strip.
|
||||
* @param ctx
|
||||
* The servlet context in which to search for registration mappings.
|
||||
*
|
||||
* @return The stripped request URI.
|
||||
*/
|
||||
public static String stripRegistrationMapping(String requestUri,
|
||||
ServletContext ctx) {
|
||||
for(ServletRegistration reg : ctx.getServletRegistrations().values()) {
|
||||
if(reg.getClassName().equals(RepositoryRestDispatcherServlet.class.getName())
|
||||
|| reg.getName().equals("rest-exporter")) {
|
||||
for(String mapping : reg.getMappings()) {
|
||||
if(mapping.contains("*")) {
|
||||
mapping = mapping.substring(0, mapping.indexOf('*'));
|
||||
}
|
||||
if(requestUri.startsWith(mapping)) {
|
||||
return requestUri.replaceAll(mapping, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return requestUri;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.springframework.data.rest.webmvc.support;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class JsonpResponse<T> {
|
||||
|
||||
private final ResponseEntity<T> responseEntity;
|
||||
private final String callbackParam;
|
||||
private final String errbackParam;
|
||||
|
||||
public JsonpResponse(ResponseEntity<T> responseEntity,
|
||||
String callbackParam,
|
||||
String errbackParam) {
|
||||
this.responseEntity = responseEntity;
|
||||
this.callbackParam = callbackParam;
|
||||
this.errbackParam = errbackParam;
|
||||
}
|
||||
|
||||
public ResponseEntity<T> getResponseEntity() {
|
||||
return responseEntity;
|
||||
}
|
||||
|
||||
public String getCallbackParam() {
|
||||
return callbackParam;
|
||||
}
|
||||
|
||||
public String getErrbackParam() {
|
||||
return errbackParam;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
package org.springframework.data.rest.webmvc.support;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
@@ -7,6 +7,7 @@ import java.util.Iterator;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.rest.config.RepositoryRestConfiguration;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.springframework.data.rest.webmvc.support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.data.rest.repository.RepositoryConstraintViolationException;
|
||||
import org.springframework.validation.FieldError;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class RepositoryConstraintViolationExceptionMessage {
|
||||
|
||||
private final RepositoryConstraintViolationException violationException;
|
||||
private final List<String> errors = new ArrayList<String>();
|
||||
|
||||
public RepositoryConstraintViolationExceptionMessage(RepositoryConstraintViolationException violationException,
|
||||
MessageSource msgSrc) {
|
||||
this.violationException = violationException;
|
||||
|
||||
for(FieldError fe : violationException.getErrors().getFieldErrors()) {
|
||||
List<Object> args = new ArrayList<Object>();
|
||||
args.add(fe.getObjectName());
|
||||
args.add(fe.getField());
|
||||
args.add(fe.getRejectedValue());
|
||||
if(null != fe.getArguments()) {
|
||||
for(Object o : fe.getArguments()) {
|
||||
args.add(o);
|
||||
}
|
||||
}
|
||||
|
||||
String msg = msgSrc.getMessage(fe.getCode(),
|
||||
args.toArray(),
|
||||
fe.getDefaultMessage(),
|
||||
null);
|
||||
this.errors.add(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonProperty("errors")
|
||||
public List<String> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc.spec
|
||||
|
||||
import org.codehaus.jackson.map.ObjectMapper
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.context.ApplicationContext
|
||||
import org.springframework.data.rest.test.ApplicationConfig
|
||||
import org.springframework.data.rest.test.ApplicationRestConfig
|
||||
import org.springframework.data.rest.test.webmvc.Address
|
||||
import org.springframework.data.rest.test.webmvc.AddressRepository
|
||||
import org.springframework.data.rest.test.webmvc.CustomerRepository
|
||||
import org.springframework.data.rest.test.webmvc.Person
|
||||
import org.springframework.data.rest.test.webmvc.PersonRepository
|
||||
import org.springframework.data.rest.test.webmvc.ProfileRepository
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestConfiguration
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestController
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.http.server.ServletServerHttpRequest
|
||||
import org.springframework.mock.web.MockHttpServletRequest
|
||||
import org.springframework.orm.jpa.EntityManagerHolder
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import spock.lang.Specification
|
||||
|
||||
import javax.persistence.EntityManagerFactory
|
||||
|
||||
import static org.springframework.transaction.support.TransactionSynchronizationManager.*
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@ContextConfiguration(classes = [ApplicationConfig, ApplicationRestConfig])
|
||||
abstract class BaseSpec extends Specification {
|
||||
|
||||
@Autowired ApplicationContext appCtx
|
||||
@Autowired RepositoryRestConfiguration config
|
||||
@Autowired RepositoryRestController controller
|
||||
@Autowired EntityManagerFactory emf
|
||||
@Autowired PersonRepository people
|
||||
@Autowired AddressRepository addresses
|
||||
@Autowired CustomerRepository customers
|
||||
@Autowired ProfileRepository profiles
|
||||
URI baseUri
|
||||
|
||||
def mapper = new ObjectMapper()
|
||||
|
||||
@Transactional
|
||||
def setup() {
|
||||
baseUri = URI.create("http://localhost:8080/data")
|
||||
config.baseUri = baseUri
|
||||
|
||||
if (!hasResource(emf)) {
|
||||
bindResource(emf, new EntityManagerHolder(emf.createEntityManager()))
|
||||
}
|
||||
}
|
||||
|
||||
def readJson(ResponseEntity entity) {
|
||||
mapper.readValue((byte[]) entity.body, Map)
|
||||
}
|
||||
|
||||
def createJsonRequest(method, path, query, obj) {
|
||||
createRequest(method, path, query, "application/json", mapper.writeValueAsString(obj))
|
||||
}
|
||||
|
||||
def createUriListRequest(method, path, query, obj) {
|
||||
createRequest(method, path, query, "text/uri-list", obj.join("\n"))
|
||||
}
|
||||
|
||||
def createRequest(method, path, query) {
|
||||
createRequest(method, path, query, null, null)
|
||||
}
|
||||
|
||||
def createRequest(method, path, query, contentType, content) {
|
||||
def req = new MockHttpServletRequest(
|
||||
serverPort: 8080,
|
||||
requestURI: "/data/$path",
|
||||
method: method
|
||||
)
|
||||
if (query) {
|
||||
query.collect { String k, String v -> req.addParameter(k, v)}
|
||||
}
|
||||
if (contentType) {
|
||||
req.contentType = contentType
|
||||
}
|
||||
if (content) {
|
||||
req.content = content
|
||||
}
|
||||
|
||||
new ServletServerHttpRequest(req)
|
||||
}
|
||||
|
||||
def newPerson() {
|
||||
def p = people.save(new Person(name: "John Doe"))
|
||||
def a = newAddress("Univille")
|
||||
p.addresses = [a]
|
||||
people.save(p)
|
||||
}
|
||||
|
||||
def newAddress(city) {
|
||||
addresses.save(new Address(
|
||||
["1234 W. 1st St."] as String[],
|
||||
city,
|
||||
"ST",
|
||||
"12345"
|
||||
))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc.spec
|
||||
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.data.rest.webmvc.PagingAndSorting
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestConfiguration
|
||||
import org.springframework.http.HttpStatus
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
class DiscoverySpec extends BaseSpec {
|
||||
|
||||
def "exposes configured repositories for discovery"() {
|
||||
|
||||
given:
|
||||
def request = createRequest("GET", "", null)
|
||||
|
||||
when:
|
||||
def response = controller.listRepositories(request, baseUri)
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.OK
|
||||
|
||||
when:
|
||||
def links = readJson(response).links
|
||||
|
||||
then:
|
||||
links.size() == 6
|
||||
|
||||
}
|
||||
|
||||
def "lists entities for discovery"() {
|
||||
|
||||
given:
|
||||
(1..20).each { newPerson() }
|
||||
def pageSort = new PagingAndSorting(RepositoryRestConfiguration.DEFAULT, new PageRequest(0, 10))
|
||||
def request = createRequest("GET", "people", null)
|
||||
|
||||
when:
|
||||
def response = controller.listEntities(request, pageSort, baseUri, "people")
|
||||
def body = readJson(response)
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.OK
|
||||
body.content.size() == 10
|
||||
body.page.totalPages > 1
|
||||
body.page.number == 1
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc.spec
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.data.rest.repository.RepositoryConstraintViolationException
|
||||
import org.springframework.data.rest.test.webmvc.Customer
|
||||
import org.springframework.data.rest.test.webmvc.Person
|
||||
import org.springframework.data.rest.test.webmvc.TestRepositoryEventListener
|
||||
import org.springframework.http.HttpStatus
|
||||
|
||||
import javax.validation.ConstraintViolationException
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
class EventsSpec extends BaseSpec {
|
||||
|
||||
@Autowired TestRepositoryEventListener listener
|
||||
|
||||
def "cannot save invalid entity"() {
|
||||
|
||||
given:
|
||||
def person = new Person()
|
||||
def request = createJsonRequest("POST", "people", null, person)
|
||||
|
||||
when:
|
||||
try {
|
||||
controller.create(request, baseUri, "people")
|
||||
} catch (RepositoryConstraintViolationException e) {
|
||||
controller.handleValidationFailure(e, request)
|
||||
throw e
|
||||
}
|
||||
|
||||
then:
|
||||
thrown(RepositoryConstraintViolationException)
|
||||
|
||||
}
|
||||
|
||||
def "handles JSR-303 validation errors"() {
|
||||
|
||||
given:
|
||||
def cust = new Customer()
|
||||
def request = createJsonRequest("POST", "customer", null, cust)
|
||||
|
||||
when:
|
||||
try {
|
||||
controller.create(request, baseUri, "customer")
|
||||
} catch (ConstraintViolationException e) {
|
||||
controller.handleJsr303ValidationFailure(e, request)
|
||||
throw e
|
||||
}
|
||||
|
||||
then:
|
||||
thrown(ConstraintViolationException)
|
||||
|
||||
}
|
||||
|
||||
def "captures before and after events"() {
|
||||
|
||||
given:
|
||||
def person = new Person(name: "John Doe")
|
||||
def request = createJsonRequest("POST", "people", ["returnBody": "true"], person)
|
||||
def persId
|
||||
listener.handlers << { evt, p ->
|
||||
if (evt == "afterSave")
|
||||
persId = "${p.id}"
|
||||
}
|
||||
|
||||
when:
|
||||
def response = controller.create(request, baseUri, "people")
|
||||
def returnedId = response.headers.getFirst('Location').tokenize("/").last()
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.CREATED
|
||||
persId == returnedId
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc.spec
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
class JsonpSpec extends BaseSpec {
|
||||
|
||||
def "wraps response with JSONP"() {
|
||||
|
||||
given:
|
||||
def person = newPerson()
|
||||
def request = createRequest("GET", "people/${person.id}", ["callback": "jsonp_callback"])
|
||||
|
||||
when:
|
||||
def response = controller.entity(request, baseUri, "people", "${person.id}")
|
||||
def body = new String(response.body)
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.OK
|
||||
body?.startsWith("jsonp_callback")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc.spec
|
||||
|
||||
import org.springframework.data.rest.test.webmvc.Customer
|
||||
import org.springframework.http.HttpStatus
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
class NestedObjectSpec extends BaseSpec {
|
||||
|
||||
def "saves nested object"() {
|
||||
|
||||
given:
|
||||
def customer = customers.save(new Customer(userid: "jdoe"))
|
||||
def jsonObj = [
|
||||
"customers": [
|
||||
["rel": "customer.Customer", "href": "http://localhost:8080/data/customer/" + customer.id]
|
||||
]
|
||||
]
|
||||
def request = createJsonRequest("PUT", "customerTracker/1", null, jsonObj)
|
||||
def getReq = createRequest("GET", "customerTracker/1/customers", null)
|
||||
|
||||
when:
|
||||
def response = controller.create(request, baseUri, "customerTracker")
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.CREATED
|
||||
|
||||
when:
|
||||
def getResp = controller.propertyOfEntity(getReq, baseUri, "customerTracker", "1", "customers")
|
||||
def jsonResp = readJson(getResp)
|
||||
|
||||
then:
|
||||
getResp.statusCode == HttpStatus.OK
|
||||
jsonResp.content[0].userid == "jdoe"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc.spec
|
||||
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.data.rest.test.webmvc.Person
|
||||
import org.springframework.data.rest.webmvc.PagingAndSorting
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestConfiguration
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import spock.lang.Shared
|
||||
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
class QueryMethodsSpec extends BaseSpec {
|
||||
|
||||
@Shared
|
||||
def pageSort = new PagingAndSorting(RepositoryRestConfiguration.DEFAULT, new PageRequest(0, 10))
|
||||
|
||||
def "exposes query method links to discovery"() {
|
||||
|
||||
given:
|
||||
def request = createRequest("GET", "people/search", null)
|
||||
|
||||
when:
|
||||
def response = controller.listQueryMethods(request, baseUri, "people")
|
||||
def body = readJson(response)
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.OK
|
||||
body.links.size() == 5
|
||||
|
||||
}
|
||||
|
||||
@Transactional
|
||||
def "invokes query methods"() {
|
||||
|
||||
given:
|
||||
people.save(new Person(name: "John Doe"))
|
||||
people.save(new Person(name: "Bill Doe"))
|
||||
def request = createRequest("GET", "people/search/nameStartsWith", ["name": "John"])
|
||||
|
||||
when:
|
||||
def response = controller.query(request, pageSort, baseUri, "people", "nameStartsWith")
|
||||
def body = readJson(response)
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.OK
|
||||
body.content.size() > 0
|
||||
|
||||
}
|
||||
|
||||
@Transactional
|
||||
def "blows up on empty query parameters"() {
|
||||
|
||||
given:
|
||||
def request = createRequest("GET", "people/search/nameStartsWith", null)
|
||||
|
||||
when:
|
||||
controller.query(request, pageSort, baseUri, "people", "nameStartsWith")
|
||||
|
||||
then:
|
||||
thrown(InvocationTargetException)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc.spec
|
||||
|
||||
import org.springframework.data.rest.test.webmvc.Person
|
||||
import org.springframework.data.rest.test.webmvc.Profile
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.web.util.UriComponentsBuilder
|
||||
import spock.lang.Shared
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
class RelationshipsSpec extends BaseSpec {
|
||||
|
||||
@Shared
|
||||
Long persId
|
||||
@Shared
|
||||
Long addrId
|
||||
@Shared
|
||||
Long profileId
|
||||
|
||||
def setup() {
|
||||
def person = people.save(new Person(name: "John Doe"))
|
||||
persId = person.id
|
||||
def addr = newAddress("Uniontown")
|
||||
addrId = addr.id
|
||||
def profile = profiles.save(new Profile(type: "socialmedia", url: "http://socialmedia.com", person: person))
|
||||
person.profiles = ["socialmedia": profile]
|
||||
people.save(person)
|
||||
profileId = profile.id
|
||||
}
|
||||
|
||||
@Transactional
|
||||
def "saves entity relationship"() {
|
||||
|
||||
given:
|
||||
def request = createUriListRequest(
|
||||
"POST",
|
||||
"people/$persId/addresses",
|
||||
null,
|
||||
[UriComponentsBuilder.fromUri(baseUri).pathSegment("address", "$addrId").build().toUriString()]
|
||||
)
|
||||
|
||||
when:
|
||||
def response = controller.updatePropertyOfEntity(request, baseUri, "people", "$persId", "addresses")
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.CREATED
|
||||
|
||||
when:
|
||||
request = createRequest("GET", "people/$persId/addresses/$addrId", null)
|
||||
response = controller.linkedEntity(request, baseUri, "people", "$persId", "addresses", "$addrId")
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.OK
|
||||
readJson(response).city == "Uniontown"
|
||||
|
||||
}
|
||||
|
||||
@Transactional
|
||||
def "cannot delete a required relationship"() {
|
||||
|
||||
when:
|
||||
def request = createRequest("DELETE", "profile/$profileId/person", null)
|
||||
def response = controller.clearLinks(request, "profile", "$profileId", "person")
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.METHOD_NOT_ALLOWED
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc.spec
|
||||
|
||||
import org.codehaus.jackson.map.ObjectMapper
|
||||
import org.springframework.context.support.ClassPathXmlApplicationContext
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.data.rest.test.webmvc.Address
|
||||
import org.springframework.data.rest.webmvc.PagingAndSorting
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestConfiguration
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestController
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.server.ServletServerHttpRequest
|
||||
import org.springframework.mock.web.MockHttpServletRequest
|
||||
import org.springframework.mock.web.MockServletConfig
|
||||
import org.springframework.mock.web.MockServletContext
|
||||
import org.springframework.orm.jpa.EntityManagerHolder
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager
|
||||
import org.springframework.ui.ExtendedModelMap
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext
|
||||
import org.springframework.web.util.UriComponentsBuilder
|
||||
import spock.lang.Ignore
|
||||
import spock.lang.Shared
|
||||
import spock.lang.Specification
|
||||
|
||||
import javax.persistence.EntityManagerFactory
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jbrisbin@vmware.com>
|
||||
*/
|
||||
@Ignore
|
||||
class RepositoryRestControllerSpec extends Specification {
|
||||
|
||||
@Shared
|
||||
UriComponentsBuilder uriBuilder
|
||||
@Shared
|
||||
ObjectMapper mapper = new ObjectMapper()
|
||||
@Shared
|
||||
RepositoryRestController controller
|
||||
@Shared
|
||||
PagingAndSorting pageSort
|
||||
@Shared
|
||||
EntityManagerFactory emf
|
||||
|
||||
MockHttpServletRequest createRequest(String method, String path) {
|
||||
return new MockHttpServletRequest(
|
||||
serverPort: 8080,
|
||||
requestURI: "/data/$path",
|
||||
method: method
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to set up things similarly to how they get loaded in the webapp.
|
||||
*/
|
||||
def setupSpec() {
|
||||
def servletConfig = new MockServletConfig()
|
||||
def servletContext = new MockServletContext()
|
||||
|
||||
def parentCtx = new ClassPathXmlApplicationContext("classpath*:META-INF/spring-data-rest/**/*-export.xml")
|
||||
|
||||
def webAppCtx = new AnnotationConfigWebApplicationContext()
|
||||
webAppCtx.servletConfig = servletConfig
|
||||
webAppCtx.servletContext = servletContext
|
||||
webAppCtx.configLocations = [RepositoryRestMvcConfiguration.name] as String[]
|
||||
webAppCtx.parent = parentCtx
|
||||
webAppCtx.refresh()
|
||||
|
||||
emf = webAppCtx.getBean(EntityManagerFactory)
|
||||
controller = webAppCtx.getBean(RepositoryRestController)
|
||||
pageSort = new PagingAndSorting(RepositoryRestConfiguration.DEFAULT, new PageRequest(0, 1000))
|
||||
uriBuilder = UriComponentsBuilder.fromUriString("http://localhost:8080/data")
|
||||
}
|
||||
|
||||
def setup() {
|
||||
if (!TransactionSynchronizationManager.hasResource(emf)) {
|
||||
TransactionSynchronizationManager.bindResource(emf, new EntityManagerHolder(emf.createEntityManager()))
|
||||
}
|
||||
}
|
||||
|
||||
def "API Test"() {
|
||||
|
||||
given:
|
||||
def model = new ExtendedModelMap()
|
||||
|
||||
when: "listing available repositories"
|
||||
def req = createRequest("POST", "people")
|
||||
def response = controller.listRepositories(new ServletServerHttpRequest(req), uriBuilder)
|
||||
def reposLinks = mapper.readValue(response.body, Map)?._links
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.OK
|
||||
reposLinks?.size() == 4
|
||||
|
||||
when: "adding an entity"
|
||||
model.clear()
|
||||
def data = mapper.writeValueAsBytes([name: "John Doe"])
|
||||
req.content = data
|
||||
response = controller.create(new ServletServerHttpRequest(req), req, uriBuilder, "people")
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.CREATED
|
||||
|
||||
when: "getting a specific entity"
|
||||
model.clear()
|
||||
req = createRequest("GET", "people/1")
|
||||
response = controller.entity(new ServletServerHttpRequest(req), uriBuilder, "people", "1")
|
||||
def entityData = mapper.readValue(response.body, Map)
|
||||
|
||||
then:
|
||||
entityData?.name == "John Doe"
|
||||
|
||||
when: "updating an entity"
|
||||
req = createRequest("PUT", "people/1")
|
||||
data = mapper.writeValueAsBytes([name: "Johnnie Doe", version: 0])
|
||||
req.content = data
|
||||
response = controller.createOrUpdate(new ServletServerHttpRequest(req), uriBuilder, "people", "1")
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.NO_CONTENT
|
||||
|
||||
when: "listing available entities"
|
||||
response = controller.listEntities(new ServletServerHttpRequest(req), pageSort, uriBuilder, "people")
|
||||
def selfLink = mapper.readValue(response.body, Map)?.results[0]?._links[2]
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.OK
|
||||
selfLink.href == "http://localhost:8080/data/people/1"
|
||||
|
||||
when: "creating a child entity"
|
||||
req = createRequest("POST", "address")
|
||||
data = mapper.writeValueAsBytes(new Address(["1 W. 1st St."] as String[], "Univille", "ST", "12345"))
|
||||
req.content = data
|
||||
response = controller.create(new ServletServerHttpRequest(req), req, uriBuilder, "address")
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.CREATED
|
||||
|
||||
when: "linking child to parent entity"
|
||||
req = createRequest("POST", "people/1/addresses")
|
||||
req.contentType = "text/uri-list"
|
||||
data = "http://localhost:8080/data/address/1".bytes
|
||||
req.content = data
|
||||
response = controller.updatePropertyOfEntity(new ServletServerHttpRequest(req), uriBuilder, "people", "1", "addresses")
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.CREATED
|
||||
|
||||
when: "getting property of an entity"
|
||||
response = controller.propertyOfEntity(new ServletServerHttpRequest(req), uriBuilder, "people", "1", "addresses")
|
||||
def addrLinks = mapper.readValue((byte[]) response.body, Map)?._links
|
||||
|
||||
then:
|
||||
null != addrLinks
|
||||
addrLinks.size() == 1
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc.spec
|
||||
|
||||
import org.springframework.core.MethodParameter
|
||||
import org.springframework.data.rest.test.ApplicationConfig
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration
|
||||
import org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler
|
||||
import org.springframework.hateoas.Link
|
||||
import org.springframework.hateoas.Resource
|
||||
import org.springframework.hateoas.ResourceProcessor
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import org.springframework.web.method.support.HandlerMethodReturnValueHandler
|
||||
import spock.lang.Specification
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@ContextConfiguration(classes = [ApplicationConfig, RepositoryRestMvcConfiguration])
|
||||
class ResourceProcessorSpec extends Specification {
|
||||
|
||||
static STRING_RESOURCE_PARAM = new MethodParameter(ResourceProcessorSpec.getMethod("createStringResource"), -1)
|
||||
static LONG_RESOURCE_PARAM = new MethodParameter(ResourceProcessorSpec.getMethod("createLongResource"), -1)
|
||||
static SPECIAL_STRING_RESOURCE_PARAM = new MethodParameter(ResourceProcessorSpec.getMethod("createSpecialStringResource"), -1)
|
||||
static SPECIAL_LONG_RESOURCE_PARAM = new MethodParameter(ResourceProcessorSpec.getMethod("createSpecialLongResource"), -1)
|
||||
|
||||
HandlerMethodReturnValueHandler delegateHandler
|
||||
List<ResourceProcessor<?>> processors = []
|
||||
boolean handleReturnValueCalled
|
||||
HandlerMethodReturnValueHandler resourceHandler
|
||||
|
||||
def setup() {
|
||||
delegateHandler = Mock(HandlerMethodReturnValueHandler)
|
||||
delegateHandler.handleReturnValue(_, _, null, null) >> { handleReturnValueCalled = true }
|
||||
|
||||
processors << new SpecialStringResourceProcessor() <<
|
||||
new SpecialLongResourceProcessor() <<
|
||||
new StringResourceProcessor() <<
|
||||
new LongResourceProcessor()
|
||||
|
||||
resourceHandler = new ResourceProcessorHandlerMethodReturnValueHandler(delegateHandler, processors)
|
||||
}
|
||||
|
||||
Resource<String> createStringResource() {
|
||||
new Resource<String>("string-resource")
|
||||
}
|
||||
|
||||
Resource<Long> createLongResource() {
|
||||
new Resource<Long>(1L)
|
||||
}
|
||||
|
||||
StringResource createSpecialStringResource() {
|
||||
new StringResource("special-string-resource")
|
||||
}
|
||||
|
||||
LongResource createSpecialLongResource() {
|
||||
new LongResource(1L)
|
||||
}
|
||||
|
||||
def "processes simple String resource"() {
|
||||
|
||||
given:
|
||||
def resource = createStringResource()
|
||||
|
||||
when:
|
||||
resourceHandler.handleReturnValue(resource, STRING_RESOURCE_PARAM, null, null)
|
||||
|
||||
then:
|
||||
null != resource.getLink("string-resource")
|
||||
handleReturnValueCalled
|
||||
|
||||
}
|
||||
|
||||
def "process simple Long resource"() {
|
||||
|
||||
given:
|
||||
def resource = createLongResource()
|
||||
|
||||
when:
|
||||
resourceHandler.handleReturnValue(resource, LONG_RESOURCE_PARAM, null, null)
|
||||
|
||||
then:
|
||||
null != resource.getLink("long-resource")
|
||||
handleReturnValueCalled
|
||||
|
||||
}
|
||||
|
||||
def "process specialized String resource"() {
|
||||
|
||||
given:
|
||||
def resource = createSpecialStringResource()
|
||||
|
||||
when:
|
||||
resourceHandler.handleReturnValue(resource, SPECIAL_STRING_RESOURCE_PARAM, null, null)
|
||||
|
||||
then:
|
||||
null != resource.getLink("special-string-resource")
|
||||
handleReturnValueCalled
|
||||
|
||||
}
|
||||
|
||||
def "process specialized Long resource"() {
|
||||
|
||||
given:
|
||||
def resource = createSpecialLongResource()
|
||||
|
||||
when:
|
||||
resourceHandler.handleReturnValue(resource, SPECIAL_LONG_RESOURCE_PARAM, null, null)
|
||||
|
||||
then:
|
||||
null != resource.getLink("special-long-resource")
|
||||
handleReturnValueCalled
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class StringResourceProcessor implements ResourceProcessor<Resource<String>> {
|
||||
@Override Resource<String> process(Resource<String> resource) {
|
||||
resource.add(new Link("http://localhost:8080/string-resource", "string-resource"))
|
||||
resource
|
||||
}
|
||||
}
|
||||
|
||||
class LongResourceProcessor implements ResourceProcessor<Resource<Long>> {
|
||||
@Override Resource<Long> process(Resource<Long> resource) {
|
||||
resource.add(new Link("http://localhost:8080/long-resource", "long-resource"))
|
||||
resource
|
||||
}
|
||||
}
|
||||
|
||||
class StringResource extends Resource<String> {
|
||||
StringResource(String content, Link... links) {
|
||||
super(content, links)
|
||||
}
|
||||
}
|
||||
|
||||
class SpecialStringResourceProcessor implements ResourceProcessor<StringResource> {
|
||||
@Override StringResource process(StringResource resource) {
|
||||
resource.add(new Link("http://localhost:8080/special-string-resource", "special-string-resource"))
|
||||
resource
|
||||
}
|
||||
}
|
||||
|
||||
class LongResource extends Resource<Long> {
|
||||
LongResource(Long content, Link... links) {
|
||||
super(content, links)
|
||||
}
|
||||
}
|
||||
|
||||
class SpecialLongResourceProcessor implements ResourceProcessor<LongResource> {
|
||||
@Override LongResource process(LongResource resource) {
|
||||
resource.add(new Link("http://localhost:8080/special-long-resource", "special-long-resource"))
|
||||
resource
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc.spec
|
||||
|
||||
import org.springframework.data.rest.test.webmvc.Person
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
class TopLevelEntitySpec extends BaseSpec {
|
||||
|
||||
@Transactional
|
||||
def "saves top-level entity"() {
|
||||
|
||||
given:
|
||||
def person = new Person(name: "John Doe")
|
||||
def request = createJsonRequest("POST", "people/1", null, person)
|
||||
|
||||
when:
|
||||
def response = controller.createOrUpdate(request, baseUri, "people", "1")
|
||||
|
||||
then:
|
||||
// Second status given in gradle build but doesn't happen in IDE for some reason
|
||||
response.statusCode == HttpStatus.CREATED || response.statusCode == HttpStatus.NO_CONTENT
|
||||
|
||||
}
|
||||
|
||||
@Transactional
|
||||
def "retrieves top-level entity"() {
|
||||
|
||||
given:
|
||||
def person = newPerson()
|
||||
def request = createRequest("GET", "people/${person.id}", null)
|
||||
|
||||
when:
|
||||
def response = controller.entity(request, baseUri, "people", "${person.id}")
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.OK
|
||||
|
||||
}
|
||||
|
||||
@Transactional
|
||||
def "updates top-level entity"() {
|
||||
|
||||
given:
|
||||
def person = newPerson()
|
||||
person.name = "Johnnie Doe"
|
||||
person = people.save(person)
|
||||
def persId = person.id
|
||||
def request = createJsonRequest("PUT", "people/$persId", null, ["name": "Johnnie Doe"])
|
||||
def retrReq = createRequest("GET", "people/$persId", null)
|
||||
|
||||
when:
|
||||
def response = controller.createOrUpdate(request, baseUri, "people", "$persId")
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.NO_CONTENT
|
||||
|
||||
when:
|
||||
response = controller.entity(retrReq, baseUri, "people", "$persId")
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.OK
|
||||
|
||||
when:
|
||||
def pers = readJson(response)
|
||||
|
||||
then:
|
||||
pers.name == "Johnnie Doe"
|
||||
|
||||
}
|
||||
|
||||
@Transactional
|
||||
def "won't delete entities whose delete methods are not exported"() {
|
||||
|
||||
given:
|
||||
def person = people.save(new Person(name: "John Doe"))
|
||||
def persId = person.id
|
||||
def request = createRequest("DELETE", "people/$persId", null)
|
||||
|
||||
when:
|
||||
def response = controller.deleteEntity(request, "people", "$persId")
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.METHOD_NOT_ALLOWED
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package org.springframework.data.rest.test;
|
||||
|
||||
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.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
|
||||
import org.springframework.orm.jpa.JpaDialect;
|
||||
import org.springframework.orm.jpa.JpaTransactionManager;
|
||||
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
|
||||
import org.springframework.orm.jpa.vendor.Database;
|
||||
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
|
||||
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@Configuration
|
||||
@ComponentScan(basePackages = "org.springframework.data.rest.test.webmvc")
|
||||
@EnableJpaRepositories
|
||||
@EnableTransactionManagement
|
||||
public class ApplicationConfig {
|
||||
|
||||
@Bean public MessageSource messageSource() {
|
||||
ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
|
||||
ms.setBasename("org.springframework.data.rest.test.ValidationErrors");
|
||||
return ms;
|
||||
}
|
||||
|
||||
@Bean public DataSource dataSource() {
|
||||
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
|
||||
return builder.setType(EmbeddedDatabaseType.HSQL).build();
|
||||
}
|
||||
|
||||
@Bean public EntityManagerFactory entityManagerFactory() {
|
||||
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
|
||||
vendorAdapter.setDatabase(Database.HSQL);
|
||||
vendorAdapter.setGenerateDdl(true);
|
||||
|
||||
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
|
||||
factory.setJpaVendorAdapter(vendorAdapter);
|
||||
factory.setPackagesToScan(getClass().getPackage().getName());
|
||||
factory.setDataSource(dataSource());
|
||||
|
||||
factory.afterPropertiesSet();
|
||||
|
||||
return factory.getObject();
|
||||
}
|
||||
|
||||
@Bean public JpaDialect jpaDialect() {
|
||||
return new HibernateJpaDialect();
|
||||
}
|
||||
|
||||
@Bean public PlatformTransactionManager transactionManager() {
|
||||
JpaTransactionManager txManager = new JpaTransactionManager();
|
||||
txManager.setEntityManagerFactory(entityManagerFactory());
|
||||
return txManager;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package org.springframework.data.rest.test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.Timestamp;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.codehaus.jackson.JsonGenerationException;
|
||||
import org.codehaus.jackson.JsonGenerator;
|
||||
import org.codehaus.jackson.Version;
|
||||
import org.codehaus.jackson.map.Module;
|
||||
import org.codehaus.jackson.map.SerializerProvider;
|
||||
import org.codehaus.jackson.map.module.SimpleSerializers;
|
||||
import org.codehaus.jackson.map.ser.std.SerializerBase;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.rest.test.webmvc.Person;
|
||||
import org.springframework.data.rest.test.webmvc.PersonValidator;
|
||||
import org.springframework.data.rest.test.webmvc.TestRepositoryEventListener;
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.ResourceProcessor;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@Configuration
|
||||
@Import(RepositoryRestMvcConfiguration.class)
|
||||
public class ApplicationRestConfig {
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Bean public ConversionService customConversionService() {
|
||||
DefaultFormattingConversionService cs = new DefaultFormattingConversionService();
|
||||
cs.addConverter(new Converter<String[], List<Long>>() {
|
||||
@Override public List<Long> convert(String[] source) {
|
||||
List<Long> longs = new ArrayList<Long>(source.length);
|
||||
for(String s : source) {
|
||||
longs.add(Long.parseLong(s));
|
||||
}
|
||||
return longs;
|
||||
}
|
||||
});
|
||||
// cs.addConverter(new Converter<Person, Resource>() {
|
||||
// @Override public Resource convert(Person person) {
|
||||
// Map<String, Object> m = new HashMap<String, Object>();
|
||||
// m.put("name", person.getName());
|
||||
// CustomResource r = new CustomResource(m);
|
||||
// r.add(new Link("http://localhost:8080/people/1", "self"));
|
||||
// return r;
|
||||
// }
|
||||
// });
|
||||
return cs;
|
||||
}
|
||||
|
||||
@Bean public ResourceProcessor<Resource<Person>> personProcessor() {
|
||||
return new ResourceProcessor<Resource<Person>>() {
|
||||
@Override public Resource<Person> process(Resource<Person> resource) {
|
||||
System.out.println("\t***** ResourceProcessor for Person: " + resource);
|
||||
resource.add(new Link("http://localhost:8080/people", "added-link"));
|
||||
return resource;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean public TestRepositoryEventListener testRepositoryEventListener() {
|
||||
return new TestRepositoryEventListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* This validator will be picked up automatically. The default configuration is to look at the bean name
|
||||
* and figure out what event you're interested in. This validator is interested in 'beforeSave' events
|
||||
* because the word 'beforeSave' appears in the first part of the bean name. It recognizes:
|
||||
* <p/>
|
||||
* - beforeSave
|
||||
* - afterSave
|
||||
* - beforeDelete
|
||||
* - afterDelete
|
||||
* - beforeLinkSave
|
||||
* - afterLinkSave
|
||||
* <p/>
|
||||
* What you put after that doesn't matter, you just need to make the bean name unique, of course.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean public PersonValidator beforeSavePersonValidator() {
|
||||
return new PersonValidator();
|
||||
}
|
||||
|
||||
@Bean public Module customModule() {
|
||||
return new Module() {
|
||||
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
|
||||
|
||||
@Override public String getModuleName() {
|
||||
return "custom";
|
||||
}
|
||||
|
||||
@Override public Version version() {
|
||||
return Version.unknownVersion();
|
||||
}
|
||||
|
||||
@Override public void setupModule(SetupContext context) {
|
||||
context.getDeserializationConfig().withDateFormat(dateFormat);
|
||||
|
||||
SimpleSerializers sers = new SimpleSerializers();
|
||||
sers.addSerializer(Timestamp.class, new SerializerBase<Timestamp>(Timestamp.class) {
|
||||
@Override public void serialize(Timestamp value, JsonGenerator jgen, SerializerProvider provider)
|
||||
throws IOException, JsonGenerationException {
|
||||
synchronized(dateFormat) {
|
||||
jgen.writeString(dateFormat.format(value));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
context.addSerializers(sers);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
package org.springframework.data.rest.test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import groovy.lang.Closure;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.client.DefaultResponseErrorHandler;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jbrisbin@vmware.com>
|
||||
*/
|
||||
public class RestBuilder {
|
||||
|
||||
private static final String[] DATE_FORMATS = new String[]{
|
||||
"EEE, dd MMM yyyy HH:mm:ss z",
|
||||
"yyyy-MM-dd'T'HH:mm:ss.SSSZ",
|
||||
"yyyy-MM-dd HH:mm:ss"
|
||||
};
|
||||
|
||||
private ConversionService conversionService = new DefaultConversionService();
|
||||
private ClientHttpRequestFactory requestFactory;
|
||||
private RestTemplate restTemplate;
|
||||
private HttpHeaders headers = new HttpHeaders();
|
||||
private MediaType contentType;
|
||||
private Class<?> responseType = byte[].class;
|
||||
private Map uriParams;
|
||||
private Object body;
|
||||
private Closure errorHandler;
|
||||
|
||||
public RestBuilder() {
|
||||
this.restTemplate = new RestTemplate();
|
||||
}
|
||||
|
||||
public RestBuilder(ClientHttpRequestFactory requestFactory) {
|
||||
this.requestFactory = requestFactory;
|
||||
this.restTemplate = new RestTemplate(requestFactory);
|
||||
}
|
||||
|
||||
public Object call(Closure cl) {
|
||||
RestBuilder b = null != requestFactory ? new RestBuilder(requestFactory) : new RestBuilder();
|
||||
if(null != errorHandler) {
|
||||
b.setErrorHandler(errorHandler);
|
||||
}
|
||||
b.conversionService = conversionService;
|
||||
cl.setDelegate(b);
|
||||
|
||||
return cl.call();
|
||||
}
|
||||
|
||||
public Object delete(String url) {
|
||||
restTemplate.delete(url);
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
public Object get(String url) {
|
||||
return restTemplate.getForEntity(maybeAddParams(url), responseType);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
public Object post(String url) {
|
||||
if(responseType == URI.class) {
|
||||
return restTemplate.postForLocation(maybeAddParams(url), new HttpEntity(body, headers));
|
||||
} else {
|
||||
return restTemplate.postForEntity(maybeAddParams(url), new HttpEntity(body, headers), responseType);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
public Object put(String url) {
|
||||
if(null != uriParams) {
|
||||
restTemplate.put(maybeAddParams(url), new HttpEntity(body, headers), uriParams);
|
||||
} else {
|
||||
restTemplate.put(maybeAddParams(url), new HttpEntity(body, headers));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Object accept(String accept) {
|
||||
headers.setAccept(MediaType.parseMediaTypes(accept));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Object body(Object body) {
|
||||
this.body = body;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Object contentType(String contentType) {
|
||||
this.contentType = MediaType.parseMediaType(contentType);
|
||||
headers.setContentType(this.contentType);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Object date(Date date) {
|
||||
headers.setDate(date.getTime());
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
public Object date(String date) {
|
||||
for(String fmt : DATE_FORMATS) {
|
||||
try {
|
||||
Date dte = new SimpleDateFormat(fmt).parse(date);
|
||||
headers.setDate(dte.getTime());
|
||||
break;
|
||||
} catch(ParseException e) {
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
public Object header(String key, Object val) {
|
||||
if(null != val) {
|
||||
if(val instanceof List) {
|
||||
headers.put(key, (List)val);
|
||||
} else if(ClassUtils.isAssignable(val.getClass(), String.class)) {
|
||||
headers.set(key, (String)val);
|
||||
} else {
|
||||
headers.set(key, conversionService.convert(val, String.class));
|
||||
}
|
||||
} else {
|
||||
headers.remove(key);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
public Object headers(Map headers) {
|
||||
this.headers.putAll(headers);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Date now() {
|
||||
return Calendar.getInstance().getTime();
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
public Object param(String key, String value) {
|
||||
if(null == uriParams) {
|
||||
uriParams = new HashMap();
|
||||
}
|
||||
uriParams.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Object params(Map params) {
|
||||
this.uriParams = params;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Object responseType(Class<?> responseType) {
|
||||
this.responseType = responseType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Object setConversionService(ConversionService conversionService) {
|
||||
this.conversionService = conversionService;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Object setErrorHandler(Closure errorHandler) {
|
||||
this.errorHandler = errorHandler;
|
||||
if(null != errorHandler) {
|
||||
this.restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
|
||||
@Override public void handleError(ClientHttpResponse response)
|
||||
throws IOException {
|
||||
RestBuilder.this.errorHandler.call(response);
|
||||
}
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Object setMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||
restTemplate.setMessageConverters(converters);
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
private String maybeAddParams(String url) {
|
||||
StringBuffer buff = new StringBuffer(url);
|
||||
if(null != uriParams) {
|
||||
buff.append("?");
|
||||
for(Map.Entry<String, String> entry : ((Map<String, String>)uriParams).entrySet()) {
|
||||
try {
|
||||
buff.append(entry.getKey()).append("=").append(URLEncoder.encode(entry.getValue(), "UTF-8"));
|
||||
} catch(UnsupportedEncodingException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return buff.toString();
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return "RestBuilder{" +
|
||||
"requestFactory=" + requestFactory +
|
||||
", restTemplate=" + restTemplate +
|
||||
", headers=" + headers +
|
||||
", params=" + uriParams +
|
||||
", contentType=" + contentType +
|
||||
", errorHandler=" + errorHandler +
|
||||
'}';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package org.springframework.data.rest.test;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRegistration;
|
||||
|
||||
import org.springframework.web.WebApplicationInitializer;
|
||||
import org.springframework.web.context.ContextLoaderListener;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class RestExporterWebInitializer implements WebApplicationInitializer {
|
||||
|
||||
@Override public void onStartup(ServletContext servletContext) throws ServletException {
|
||||
// Create the 'root' Spring application context
|
||||
AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
|
||||
rootContext.register(ApplicationConfig.class);
|
||||
|
||||
// Manage the lifecycle of the root application context
|
||||
servletContext.addListener(new ContextLoaderListener(rootContext));
|
||||
|
||||
// Register and map the dispatcher servlet
|
||||
DispatcherServlet servlet = new DispatcherServlet();
|
||||
servlet.setContextClass(AnnotationConfigWebApplicationContext.class);
|
||||
servlet.setContextConfigLocation(ApplicationRestConfig.class.getName());
|
||||
ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", servlet);
|
||||
dispatcher.setLoadOnStartup(1);
|
||||
dispatcher.addMapping("/*");
|
||||
|
||||
//new DefaultServletHandlerConfigurer(servletContext).enable();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
field.name.required = Field {0}.{1} is required.
|
||||
no.userid = {0}s must be assigned initial userids.
|
||||
@@ -1,89 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import javax.persistence.CascadeType;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.OneToOne;
|
||||
|
||||
import org.codehaus.jackson.annotate.JsonBackReference;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jbrisbin@vmware.com>
|
||||
*/
|
||||
@Entity
|
||||
public class Address {
|
||||
|
||||
@Id @GeneratedValue private Long id;
|
||||
private String[] lines;
|
||||
private String city;
|
||||
private String province;
|
||||
private String postalCode;
|
||||
@JsonBackReference
|
||||
@OneToOne(cascade = CascadeType.REMOVE)
|
||||
private Person person;
|
||||
|
||||
public Address() {
|
||||
}
|
||||
|
||||
public Address(String[] lines, String city, String province, String postalCode) {
|
||||
this.lines = lines;
|
||||
this.city = city;
|
||||
this.province = province;
|
||||
this.postalCode = postalCode;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String[] getLines() {
|
||||
return lines;
|
||||
}
|
||||
|
||||
public void setLines(String[] lines) {
|
||||
this.lines = lines;
|
||||
}
|
||||
|
||||
public String getCity() {
|
||||
return city;
|
||||
}
|
||||
|
||||
public void setCity(String city) {
|
||||
this.city = city;
|
||||
}
|
||||
|
||||
public String getProvince() {
|
||||
return province;
|
||||
}
|
||||
|
||||
public void setProvince(String province) {
|
||||
this.province = province;
|
||||
}
|
||||
|
||||
public String getPostalCode() {
|
||||
return postalCode;
|
||||
}
|
||||
|
||||
public void setPostalCode(String postalCode) {
|
||||
this.postalCode = postalCode;
|
||||
}
|
||||
|
||||
public Person getPerson() {
|
||||
return person;
|
||||
}
|
||||
|
||||
public void setPerson(Person person) {
|
||||
this.person = person;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object o) {
|
||||
if(!(o instanceof Address)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Address address2 = (Address)o;
|
||||
return (address2.id == id || (id != null && id.equals(address2.id)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jbrisbin@vmware.com>
|
||||
*/
|
||||
public interface AddressRepository extends CrudRepository<Address, Long> {
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.rest.repository.annotation.HandleAfterSave;
|
||||
import org.springframework.data.rest.repository.annotation.RepositoryEventHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@Component
|
||||
@RepositoryEventHandler(Person.class)
|
||||
public class AfterSavePersonHandler {
|
||||
|
||||
private final static Logger LOG = LoggerFactory.getLogger(AfterSavePersonHandler.class);
|
||||
|
||||
@HandleAfterSave
|
||||
public void handleAfterSave(Person person) {
|
||||
LOG.info("saved person: " + person);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@Entity
|
||||
public class Child extends Parent {
|
||||
|
||||
private String occupation;
|
||||
|
||||
public Child() {
|
||||
}
|
||||
|
||||
public Child(String name, String occupation) {
|
||||
super(name);
|
||||
this.occupation = occupation;
|
||||
}
|
||||
|
||||
public String getOccupation() {
|
||||
return occupation;
|
||||
}
|
||||
|
||||
public void setOccupation(String occupation) {
|
||||
this.occupation = occupation;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.rest.repository.annotation.RestResource;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@RestResource(exported = false)
|
||||
public interface ChildRepository extends JpaRepository<Child, Long> {
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.codehaus.jackson.annotate.JsonAnyGetter;
|
||||
import org.codehaus.jackson.annotate.JsonProperty;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.Resource;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class CustomResource extends Resource<Map<String, Object>> {
|
||||
|
||||
public CustomResource(Map<String, Object> properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@JsonProperty("@id")
|
||||
public String getSelfLink() {
|
||||
return super.getId().getHref();
|
||||
}
|
||||
|
||||
@JsonProperty("_links")
|
||||
@Override public List<Link> getLinks() {
|
||||
return super.getLinks();
|
||||
}
|
||||
|
||||
@JsonAnyGetter
|
||||
@Override public Map<String, Object> getContent() {
|
||||
return super.getContent();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@Entity
|
||||
public class Customer {
|
||||
|
||||
@Id @GeneratedValue private Long id;
|
||||
@NotNull(message = "no.userid")
|
||||
private String userid;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getUserid() {
|
||||
return userid;
|
||||
}
|
||||
|
||||
public Customer setUserid(String userid) {
|
||||
this.userid = userid;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public interface CustomerRepository extends CrudRepository<Customer, Long> {
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.persistence.CascadeType;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.OneToMany;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@Entity
|
||||
public class CustomerTracker {
|
||||
|
||||
@Id @GeneratedValue private Long id;
|
||||
@OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true)
|
||||
private List<Customer> customers = Collections.emptyList();
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public List<Customer> getCustomers() {
|
||||
return customers;
|
||||
}
|
||||
|
||||
public CustomerTracker setCustomers(List<Customer> customers) {
|
||||
this.customers = customers;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public interface CustomerTrackerRepository extends CrudRepository<CustomerTracker, Long> {
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import java.util.List;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.OneToMany;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jon@jbrisbin.com>
|
||||
*/
|
||||
@Entity
|
||||
public class Family {
|
||||
|
||||
@Id @GeneratedValue private Long id;
|
||||
private String surname;
|
||||
@OneToMany
|
||||
private List<Person> members;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jon@jbrisbin.com>
|
||||
*/
|
||||
public interface FamilyRepository
|
||||
extends CrudRepository<Family, Long> {
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Inheritance;
|
||||
import javax.persistence.InheritanceType;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@Entity
|
||||
@Inheritance(strategy = InheritanceType.JOINED)
|
||||
public class Parent {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
private String name;
|
||||
|
||||
public Parent() {
|
||||
}
|
||||
|
||||
public Parent(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.rest.repository.annotation.RestResource;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@RestResource(exported = false)
|
||||
public interface ParentRepository extends JpaRepository<Parent, Long> {
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.persistence.CascadeType;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.MapKey;
|
||||
import javax.persistence.OneToMany;
|
||||
import javax.persistence.PrePersist;
|
||||
import javax.persistence.Version;
|
||||
|
||||
import org.codehaus.jackson.annotate.JsonManagedReference;
|
||||
import org.springframework.data.rest.repository.annotation.RestResource;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jbrisbin@vmware.com>
|
||||
*/
|
||||
@Entity
|
||||
public class Person {
|
||||
|
||||
@Id @GeneratedValue private Long id;
|
||||
private String name;
|
||||
@RestResource(path = "version")
|
||||
@Version
|
||||
private Long version;
|
||||
@JsonManagedReference
|
||||
@OneToMany(cascade = CascadeType.REMOVE)
|
||||
private List<Address> addresses;
|
||||
@OneToMany(cascade = CascadeType.REMOVE)
|
||||
@MapKey(name = "type")
|
||||
private Map<String, Profile> profiles;
|
||||
private Date created;
|
||||
|
||||
public Person() {
|
||||
}
|
||||
|
||||
public Person(Long id, String name, List<Address> addresses, Map<String, Profile> profiles) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.addresses = addresses;
|
||||
this.profiles = profiles;
|
||||
}
|
||||
|
||||
public Person(String name, List<Address> addresses, Map<String, Profile> profiles) {
|
||||
this.name = name;
|
||||
this.addresses = addresses;
|
||||
this.profiles = profiles;
|
||||
}
|
||||
|
||||
public Person(String name, Map<String, Profile> profiles) {
|
||||
this.name = name;
|
||||
this.profiles = profiles;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public Long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(Long version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public List<Address> getAddresses() {
|
||||
return addresses;
|
||||
}
|
||||
|
||||
public void setAddresses(List<Address> addresses) {
|
||||
this.addresses = addresses;
|
||||
}
|
||||
|
||||
public Map<String, Profile> getProfiles() {
|
||||
return profiles;
|
||||
}
|
||||
|
||||
public void setProfiles(Map<String, Profile> profiles) {
|
||||
this.profiles = profiles;
|
||||
}
|
||||
|
||||
public Date getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
private void setCreated() {
|
||||
this.created = Calendar.getInstance().getTime();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jbrisbin@vmware.com>
|
||||
*/
|
||||
@Component
|
||||
public class PersonLoader implements InitializingBean {
|
||||
|
||||
@Autowired
|
||||
private PersonRepository personRepository;
|
||||
@Autowired
|
||||
private ProfileRepository profileRepository;
|
||||
@Autowired
|
||||
private AddressRepository addressRepository;
|
||||
|
||||
@Transactional
|
||||
@Override public void afterPropertiesSet()
|
||||
throws Exception {
|
||||
|
||||
Person p1 = personRepository.save(new Person("John Doe"));
|
||||
|
||||
Map<String, Profile> pers1profiles = new HashMap<String, Profile>();
|
||||
Profile twitter = profileRepository.save(new Profile("twitter", "#!/johndoe", p1));
|
||||
Profile fb = profileRepository.save(new Profile("facebook", "/johndoe", p1));
|
||||
pers1profiles.put("twitter", twitter);
|
||||
pers1profiles.put("facebook", fb);
|
||||
p1.setProfiles(pers1profiles);
|
||||
|
||||
Address pers1addr = addressRepository.save(new Address(new String[]{"1234 W. 1st St."},
|
||||
"Univille",
|
||||
"ST",
|
||||
"12345"));
|
||||
p1.setAddresses(Arrays.asList(pers1addr));
|
||||
|
||||
personRepository.save(p1);
|
||||
|
||||
|
||||
Person p2 = personRepository.save(new Person("Jane Doe"));
|
||||
|
||||
Map<String, Profile> pers2profiles = new HashMap<String, Profile>();
|
||||
Profile twitter2 = profileRepository.save(new Profile("twitter", "#!/janedoe", p2));
|
||||
Profile fb2 = profileRepository.save(new Profile("facebook", "/janedoe", p2));
|
||||
pers2profiles.put("twitter", twitter2);
|
||||
pers2profiles.put("facebook", fb2);
|
||||
p2.setProfiles(pers2profiles);
|
||||
|
||||
Address pers2addr = addressRepository.save(new Address(new String[]{"1234 E. 2nd St."},
|
||||
"Univille",
|
||||
"ST",
|
||||
"12345"));
|
||||
p2.setAddresses(Arrays.asList(pers2addr));
|
||||
|
||||
personRepository.save(p2);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
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.repository.annotation.ConvertWith;
|
||||
import org.springframework.data.rest.repository.annotation.RestResource;
|
||||
|
||||
/**
|
||||
* Example {@link org.springframework.data.repository.CrudRepository} for dealing with a {@link Person}. Also uses the
|
||||
* {@link RestResource} annotation to turn off the delete methods.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@RestResource(path = "people", rel = "people")
|
||||
public interface PersonRepository extends PagingAndSortingRepository<Person, Long> {
|
||||
|
||||
@Override
|
||||
@RestResource(exported = false) void delete(Long id);
|
||||
|
||||
@Override
|
||||
@RestResource(exported = false) void delete(Person entity);
|
||||
|
||||
@RestResource(path = "name", rel = "names") List<Person> findByName(@Param("name") String name);
|
||||
|
||||
@RestResource(path = "nameStartsWith", rel = "nameStartsWith")
|
||||
Page findByNameStartsWith(@Param("name") String name, Pageable p);
|
||||
|
||||
@Query("select count(p) from Person p")
|
||||
@RestResource(path = "count") Long personCount();
|
||||
|
||||
@Query("select p from Person p where p.id in(:ids)")
|
||||
@RestResource(path = "id") Page<Person> findById(@Param("ids") List<Long> ids, Pageable pageable);
|
||||
|
||||
@RestResource(path = "created") List<Person> findByCreatedGreaterThan(
|
||||
@Param("startDate") @ConvertWith(StringToISODateConverter.class) Date startDate
|
||||
);
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.ValidationUtils;
|
||||
import org.springframework.validation.Validator;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jbrisbin@vmware.com>
|
||||
*/
|
||||
public class PersonValidator implements Validator {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PersonValidator.class);
|
||||
|
||||
@Override public boolean supports(Class<?> clazz) {
|
||||
return ClassUtils.isAssignable(clazz, Person.class);
|
||||
}
|
||||
|
||||
@Override public void validate(Object target, Errors errors) {
|
||||
Person p = (Person)target;
|
||||
LOG.debug(" ***** Validating Person " + p);
|
||||
ValidationUtils.rejectIfEmpty(errors, "name", "field.name.required");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.ManyToOne;
|
||||
|
||||
import org.codehaus.jackson.annotate.JsonBackReference;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jbrisbin@vmware.com>
|
||||
*/
|
||||
@Entity
|
||||
public class Profile {
|
||||
|
||||
@Id @GeneratedValue private Long id;
|
||||
private String type;
|
||||
private String url;
|
||||
@JsonBackReference
|
||||
@ManyToOne(optional = false)
|
||||
private Person person;
|
||||
|
||||
public Profile() {
|
||||
}
|
||||
|
||||
public Profile(String type, String url) {
|
||||
this.type = type;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public Profile(String type, String url, Person person) {
|
||||
this.type = type;
|
||||
this.url = url;
|
||||
this.person = person;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public Person getPerson() {
|
||||
return person;
|
||||
}
|
||||
|
||||
public void setPerson(Person person) {
|
||||
this.person = person;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object o) {
|
||||
if(!(o instanceof Profile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Profile p2 = (Profile)o;
|
||||
|
||||
boolean idEq;
|
||||
if(null != id) {
|
||||
idEq = id.equals(p2.id);
|
||||
} else {
|
||||
idEq = p2.id == null;
|
||||
}
|
||||
|
||||
boolean typeEq;
|
||||
if(null != type) {
|
||||
typeEq = type.equals(p2.type);
|
||||
} else {
|
||||
typeEq = p2.type == null;
|
||||
}
|
||||
|
||||
boolean urlEq;
|
||||
if(null != url) {
|
||||
urlEq = url.equals(p2.url);
|
||||
} else {
|
||||
urlEq = p2.url == null;
|
||||
}
|
||||
|
||||
return idEq && typeEq && urlEq;
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return "Profile{" +
|
||||
"id=" + id +
|
||||
", type='" + type + '\'' +
|
||||
", url='" + url + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jbrisbin@vmware.com>
|
||||
*/
|
||||
public interface ProfileRepository extends CrudRepository<Profile, Long> {
|
||||
|
||||
public Address findByPerson(@Param("person") Person person);
|
||||
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Matchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.internal.matchers.Equals;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.ResourceProcessor;
|
||||
import org.springframework.hateoas.Resources;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ResourceProcessorHandlerMethodReturnValueHandler}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ResourceProcessorHandlerMethodReturnValueHandlerUnitTests {
|
||||
|
||||
@Mock
|
||||
HandlerMethodReturnValueHandler delegate;
|
||||
|
||||
@Mock
|
||||
MethodParameter parameter;
|
||||
|
||||
List<ResourceProcessor<?>> resourceProcessors;
|
||||
|
||||
Resource<String> source = new Resource<String>("foo");
|
||||
Resource<String> result = StringResourceProcessor.RESULT;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
resourceProcessors = new ArrayList<ResourceProcessor<?>>();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsIfDelegateSupports() {
|
||||
assertSupport(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doesNotSupportIfDelegateDoesNot() {
|
||||
assertSupport(false);
|
||||
}
|
||||
|
||||
private void assertSupport(boolean value) {
|
||||
|
||||
when(delegate.supportsReturnType(Mockito.any(MethodParameter.class))).thenReturn(value);
|
||||
HandlerMethodReturnValueHandler handler = new ResourceProcessorHandlerMethodReturnValueHandler(delegate,
|
||||
resourceProcessors);
|
||||
|
||||
assertThat(handler.supportsReturnType(parameter), is(value));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokesStringPostProcessorForSimpleStringResource() throws Exception {
|
||||
|
||||
resourceProcessors.add(new StringResourceProcessor());
|
||||
resourceProcessors.add(new LongResourceProcessor());
|
||||
|
||||
HttpEntity<Resource<String>> input = new HttpEntity<Resource<String>>(source);
|
||||
HttpEntity<Resource<String>> output = new HttpEntity<Resource<String>>(result);
|
||||
|
||||
assertProcessorInvokedForMethod("stringResourceEntity", input, output);
|
||||
assertProcessorInvokedForMethod("resourceEntity", input, output);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokesStringPostProcessorForSimpleStringResourceInResponseEntity() throws Exception {
|
||||
|
||||
resourceProcessors.add(new StringResourceProcessor());
|
||||
resourceProcessors.add(new LongResourceProcessor());
|
||||
|
||||
ResponseEntity<Resource<String>> input = new ResponseEntity<Resource<String>>(source, HttpStatus.OK);
|
||||
ResponseEntity<Resource<String>> output = new ResponseEntity<Resource<String>>(result, HttpStatus.OK);
|
||||
|
||||
assertProcessorInvokedForMethod("stringResourceEntity", input, output);
|
||||
assertProcessorInvokedForMethod("resourceEntity", input, output);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokesStringPostProcessorForSimpleStringResources() throws Exception {
|
||||
|
||||
resourceProcessors.add(new StringResourceProcessor());
|
||||
resourceProcessors.add(new LongResourceProcessor());
|
||||
resourceProcessors.add(new StringResourcesProcessor());
|
||||
|
||||
Resources<Resource<String>> sources = new Resources<Resource<String>>(Collections.singleton(source));
|
||||
|
||||
HttpEntity<Resources<Resource<String>>> input = new HttpEntity<Resources<Resource<String>>>(sources);
|
||||
HttpEntity<Resources<Resource<String>>> output = new HttpEntity<Resources<Resource<String>>>(
|
||||
StringResourcesProcessor.RESULT);
|
||||
|
||||
assertProcessorInvokedForMethod("stringResourceEntity", input, output);
|
||||
assertProcessorInvokedForMethod("resourceEntity", input, output);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokesStringPostProcessorForSpecializedStringResource() throws Exception {
|
||||
|
||||
resourceProcessors.add(new StringResourceProcessor());
|
||||
resourceProcessors.add(new LongResourceProcessor());
|
||||
|
||||
HttpEntity<Resource<String>> stringOutput = new HttpEntity<Resource<String>>(result);
|
||||
HttpEntity<StringResource> specializedInput = new HttpEntity<StringResource>(new StringResource("foo"));
|
||||
|
||||
assertProcessorInvokedForMethod("stringResourceEntity", specializedInput, stringOutput);
|
||||
assertProcessorInvokedForMethod("resourceEntity", specializedInput, stringOutput);
|
||||
assertProcessorInvokedForMethod("specializedStringResourceEntity", specializedInput, stringOutput);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doesNotInvokeSpecializedStringPostProcessorForSimpleStringResource() throws Exception {
|
||||
|
||||
resourceProcessors.add(new SpecializedStringResourceProcessor());
|
||||
resourceProcessors.add(new LongResourceProcessor());
|
||||
|
||||
HttpEntity<Resource<String>> input = new HttpEntity<Resource<String>>(source);
|
||||
|
||||
assertProcessorInvokedForMethod("stringResourceEntity", input, input);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokesSpecializedStringPostProcessor() throws Exception {
|
||||
|
||||
resourceProcessors.add(new SpecializedStringResourceProcessor());
|
||||
resourceProcessors.add(new LongResourceProcessor());
|
||||
|
||||
HttpEntity<StringResource> input = new HttpEntity<StringResource>(new StringResource("foo"));
|
||||
HttpEntity<StringResource> output = new HttpEntity<StringResource>(SpecializedStringResourceProcessor.RESULT);
|
||||
|
||||
assertProcessorInvokedForMethod("specializedStringResourceEntity", input, output);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokesLongPostProcessorForLongResource() throws Exception {
|
||||
|
||||
resourceProcessors.add(new StringResourceProcessor());
|
||||
resourceProcessors.add(new LongResourceProcessor());
|
||||
|
||||
HttpEntity<Resource<Long>> input = new HttpEntity<Resource<Long>>(new Resource<Long>(50L));
|
||||
HttpEntity<LongResource> specializedInput = new HttpEntity<LongResource>(new LongResource(50L));
|
||||
HttpEntity<Resource<Long>> output = new HttpEntity<Resource<Long>>(LongResourceProcessor.RESULT);
|
||||
|
||||
assertProcessorInvokedForMethod("resourceEntity", specializedInput, output);
|
||||
assertProcessorInvokedForMethod("numberResourceEntity", input, output);
|
||||
}
|
||||
|
||||
private void assertProcessorInvokedForMethod(String methodName, Object returnValue, Object processedValue)
|
||||
throws Exception {
|
||||
|
||||
HandlerMethodReturnValueHandler handler = new ResourceProcessorHandlerMethodReturnValueHandler(delegate,
|
||||
resourceProcessors);
|
||||
|
||||
Method method = Controller.class.getMethod(methodName);
|
||||
MethodParameter returnType = new MethodParameter(method, -1);
|
||||
|
||||
handler.handleReturnValue(returnValue, returnType, null, null);
|
||||
|
||||
verify(delegate, times(1)).handleReturnValue(argThat(new HttpEntityMatcher(processedValue)), eq(returnType),
|
||||
eq((ModelAndViewContainer) null), eq((NativeWebRequest) null));
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
static class HttpEntityMatcher extends Equals {
|
||||
|
||||
public HttpEntityMatcher(Object wanted) {
|
||||
super(wanted);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.mockito.internal.matchers.Equals#matches(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean matches(Object actual) {
|
||||
|
||||
Object wanted = getWanted();
|
||||
|
||||
if (actual instanceof ResponseEntity && wanted instanceof ResponseEntity) {
|
||||
|
||||
ResponseEntity<?> left = (ResponseEntity<?>) wanted;
|
||||
ResponseEntity<?> right = (ResponseEntity<?>) actual;
|
||||
|
||||
if (!left.getStatusCode().equals(right.getStatusCode())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (actual instanceof HttpEntity && wanted instanceof HttpEntity) {
|
||||
|
||||
HttpEntity<?> left = (HttpEntity<?>) wanted;
|
||||
HttpEntity<?> right = (HttpEntity<?>) actual;
|
||||
|
||||
if (!left.getBody().equals(right.getBody())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!left.getHeaders().equals(right.getHeaders())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.matches(actual);
|
||||
}
|
||||
}
|
||||
|
||||
interface Controller {
|
||||
|
||||
Resources<Resource<String>> resources();
|
||||
|
||||
Resource<String> resource();
|
||||
|
||||
StringResource specializedResource();
|
||||
|
||||
Object object();
|
||||
|
||||
HttpEntity<Resource<?>> resourceEntity();
|
||||
|
||||
HttpEntity<Resources<?>> resourcesEntity();
|
||||
|
||||
HttpEntity<Object> objectEntity();
|
||||
|
||||
HttpEntity<Resource<String>> stringResourceEntity();
|
||||
|
||||
HttpEntity<Resource<? extends Number>> numberResourceEntity();
|
||||
|
||||
HttpEntity<StringResource> specializedStringResourceEntity();
|
||||
|
||||
ResponseEntity<Resource<?>> resourceResponseEntity();
|
||||
|
||||
ResponseEntity<Resources<?>> resourcesResponseEntity();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ResourceProcessor} to process {@link String}s.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
static class StringResourceProcessor implements ResourceProcessor<Resource<String>> {
|
||||
|
||||
static final Resource<String> RESULT = new Resource<String>("bar");
|
||||
|
||||
@Override
|
||||
public Resource<String> process(Resource<String> resource) {
|
||||
return RESULT;
|
||||
}
|
||||
}
|
||||
|
||||
static class StringResourcesProcessor implements ResourceProcessor<Resources<Resource<String>>> {
|
||||
|
||||
static final Resources<Resource<String>> RESULT = new Resources<Resource<String>>(
|
||||
Collections.singleton(StringResourceProcessor.RESULT));
|
||||
|
||||
@Override
|
||||
public Resources<Resource<String>> process(Resources<Resource<String>> resources) {
|
||||
return RESULT;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ResourceProcessor} to process {@link Long} values.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
static class LongResourceProcessor implements ResourceProcessor<Resource<Long>> {
|
||||
|
||||
static final Resource<Long> RESULT = new Resource<Long>(10L);
|
||||
|
||||
@Override
|
||||
public Resource<Long> process(Resource<Long> resource) {
|
||||
return RESULT;
|
||||
}
|
||||
}
|
||||
|
||||
static class StringResource extends Resource<String> {
|
||||
|
||||
public StringResource(String value) {
|
||||
super(value);
|
||||
}
|
||||
}
|
||||
|
||||
static class LongResource extends Resource<Long> {
|
||||
|
||||
public LongResource(Long value) {
|
||||
super(value);
|
||||
}
|
||||
}
|
||||
|
||||
static class SpecializedStringResourceProcessor implements ResourceProcessor<StringResource> {
|
||||
|
||||
static final StringResource RESULT = new StringResource("foobar");
|
||||
|
||||
@Override
|
||||
public StringResource process(StringResource resource) {
|
||||
return RESULT;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class StringToISODateConverter implements Converter<String[], Date> {
|
||||
@Override public Date convert(String[] s) {
|
||||
if(s.length == 1) {
|
||||
try {
|
||||
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS").parse(s[0]);
|
||||
} catch(ParseException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Can only parse a single date in the parameter.");
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class StringToListOfLongsConverter implements Converter<String[], List<Long>> {
|
||||
|
||||
@Override public List<Long> convert(String[] source) {
|
||||
List<Long> longs = new ArrayList<Long>();
|
||||
String strings;
|
||||
if(source.length == 1) {
|
||||
strings = source[0];
|
||||
} else {
|
||||
strings = StringUtils.arrayToCommaDelimitedString(source);
|
||||
}
|
||||
for(String s : StringUtils.commaDelimitedListToStringArray(strings)) {
|
||||
longs.add(Long.parseLong(s));
|
||||
}
|
||||
return longs;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import groovy.lang.Closure;
|
||||
import org.springframework.data.rest.repository.context.AbstractRepositoryEventListener;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class TestRepositoryEventListener extends AbstractRepositoryEventListener<TestRepositoryEventListener> {
|
||||
|
||||
private List<Closure> handlers = new ArrayList<Closure>();
|
||||
|
||||
public List<Closure> getHandlers() {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
@Override protected void onBeforeSave(Object entity) {
|
||||
for(Closure cl : handlers) {
|
||||
cl.call("beforeSave", entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected void onAfterSave(Object entity) {
|
||||
for(Closure cl : handlers) {
|
||||
cl.call("afterSave", entity);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import java.util.UUID;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jbrisbin@vmware.com>
|
||||
*/
|
||||
@Entity
|
||||
public class UuidTest {
|
||||
|
||||
@Id UUID id = UUID.randomUUID();
|
||||
String name;
|
||||
|
||||
public UuidTest() {
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.rest.repository.annotation.RestResource;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jbrisbin@vmware.com>
|
||||
*/
|
||||
@RestResource(exported = false)
|
||||
public interface UuidTestRepository
|
||||
extends CrudRepository<UuidTest, UUID> {
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public abstract class AbstractServerEnabledTest {
|
||||
|
||||
private Server server;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
if(null == server) {
|
||||
server = new Server(0);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +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.test.webmvc.Address</class>
|
||||
<class>org.springframework.data.rest.test.webmvc.Child</class>
|
||||
<class>org.springframework.data.rest.test.webmvc.Customer</class>
|
||||
<class>org.springframework.data.rest.test.webmvc.CustomerTracker</class>
|
||||
<class>org.springframework.data.rest.test.webmvc.Family</class>
|
||||
<class>org.springframework.data.rest.test.webmvc.Person</class>
|
||||
<class>org.springframework.data.rest.test.webmvc.Profile</class>
|
||||
<class>org.springframework.data.rest.test.webmvc.UuidTest</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>
|
||||
@@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:p="http://www.springframework.org/schema/p"
|
||||
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 id="baseUri" class="java.net.URI">
|
||||
<constructor-arg value="http://localhost:8080/data"/>
|
||||
</bean>
|
||||
|
||||
<bean id="config" class="org.springframework.data.rest.webmvc.RepositoryRestConfiguration"
|
||||
p:jsonpParamName="callback"
|
||||
p:jsonpOnErrParamName="errback"
|
||||
p:baseUri-ref="baseUri">
|
||||
<property name="domainTypeToRepositoryMappings">
|
||||
<map key-type="java.lang.Class" value-type="java.lang.Class">
|
||||
<entry key="org.springframework.data.rest.test.webmvc.Person"
|
||||
value="org.springframework.data.rest.test.webmvc.PersonRepository"/>
|
||||
</map>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<!--
|
||||
If you need to add Converters to the REST exporter to handle the property types you're using
|
||||
in your entities, then just configure a ConversionServiceFactoryBean here, add the Converters
|
||||
you need, and they will, in turn, be added to the FormattingConversionService the REST exporter
|
||||
uses internally.
|
||||
|
||||
Uncomment this block to add the included UUID <-> String converters, which are not included by default.
|
||||
-->
|
||||
<bean class="org.springframework.context.support.ConversionServiceFactoryBean">
|
||||
<property name="converters">
|
||||
<set>
|
||||
<bean class="org.springframework.data.rest.core.convert.StringToUUIDConverter"/>
|
||||
<bean class="org.springframework.data.rest.core.convert.UUIDToStringConverter"/>
|
||||
</set>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<!--
|
||||
The manual configuration (which doesn't look at the bean name) can be done by declaring the
|
||||
event listener instance yourself. It's significantly more XML, but if you need more control:
|
||||
-->
|
||||
<!--
|
||||
<bean class="org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener">
|
||||
<property name="validators">
|
||||
<map>
|
||||
<entry key="beforeSave">
|
||||
<list>
|
||||
<bean class="org.springframework.data.rest.test.webmvc.PersonValidator"/>
|
||||
</list>
|
||||
</entry>
|
||||
</map>
|
||||
</property>
|
||||
</bean>
|
||||
-->
|
||||
|
||||
</beans>
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/sh
|
||||
curl -v -d '{"surname" : "Doe"}' -H "Content-Type: application/json" http://localhost:8080/family
|
||||
curl -v -d '{"name" : "John Doe"}' -H "Content-Type: application/json" http://localhost:8080/people
|
||||
curl -v -d '{"name" : "Jane Doe"}' -H "Content-Type: application/json" http://localhost:8080/people
|
||||
curl -v -d 'http://localhost:8080/people/1
|
||||
http://localhost:8080/people/2' -H "Content-Type: text/uri-list" http://localhost:8080/family/1/members
|
||||
curl -v -d '{"postalCode":"12345","province":"MO","lines":["1 W 1st St."],"city":"Univille","person": {"href":"http://localhost:8080/people/1"}}' -H "Content-Type: application/json" http://localhost:8080/address
|
||||
curl -v -d "http://localhost:8080/address/1" -H "Content-Type: text/uri-list" http://localhost:8080/people/1/addresses
|
||||
curl -v -d "http://localhost:8080/people/1" -X PUT -H "Content-Type: text/uri-list" http://localhost:8080/address/1/person
|
||||
curl -v -d '{"postalCode":"54321","province":"MO","lines":["2 W 1st St."],"city":"Univille","person": {"href":"http://localhost:8080/people/2"}}' -H "Content-Type: application/json" http://localhost:8080/address
|
||||
curl -v -d "http://localhost:8080/address/2" -H "Content-Type: text/uri-list" http://localhost:8080/people/2/addresses
|
||||
curl -v -d '{"type" : "twitter", "url": "#!/johndoe", "person": {"href": "http://localhost:8080/people/1"}}' -H "Content-Type: application/json" http://localhost:8080/profile
|
||||
#curl -v -d "http://localhost:8080/profile/1" -H "Content-Type: text/uri-list" http://localhost:8080/people/1/profiles
|
||||
curl -v -d '{"type" : "facebook", "url": "/janedoe", "person": {"href": "http://localhost:8080/people/2"}}' -H "Content-Type: application/json" http://localhost:8080/profile
|
||||
#curl -v -d '{"links": [{"rel":"facebook", "href": "http://localhost:8080/profile/2"}]}' -H "Content-Type: application/json" http://localhost:8080/people/2/profiles
|
||||
@@ -1,8 +0,0 @@
|
||||
require "json"
|
||||
require "net/http"
|
||||
|
||||
client = Net::HTTP.new("localhost", 8080)
|
||||
|
||||
File.open("names.txt").each_line do |name|
|
||||
client.post("/people", JSON.dump({"name" => name.chomp}), {"Content-Type"=>"application/json"})
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
<configuration>
|
||||
|
||||
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>
|
||||
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
|
||||
</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="org.springframework.data.rest.auditlog" level="INFO"/>
|
||||
<logger name="org.springframework.data.rest" level="DEBUG"/>
|
||||
<logger name="org.springframework.data" level="INFO"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="stdout"/>
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
@@ -1,100 +0,0 @@
|
||||
Adalberto Raymos
|
||||
Adrien Maytubby
|
||||
Alonzo Schroyer
|
||||
Alva Sauvageau
|
||||
Amalia Velie
|
||||
Amber Pay
|
||||
Annabell Zozaya
|
||||
Antone Ryan
|
||||
Antonia Maslanka
|
||||
Art Esperanza
|
||||
Ashlee Mittan
|
||||
Audrie Smid
|
||||
Augustine Crosswell
|
||||
Benny Graden
|
||||
Billye Bornmann
|
||||
Blythe Milby
|
||||
Bret Pistole
|
||||
Briana Angry
|
||||
Bruno Feeley
|
||||
Carol Cruikshank
|
||||
Chanell Neidlinger
|
||||
Cher Griswould
|
||||
Cheri Batson
|
||||
Claud Bardon
|
||||
Crysta Kooker
|
||||
Cyrus Balius
|
||||
Daniel Vangieson
|
||||
Daron Gardocki
|
||||
Delana Rowley
|
||||
Devon Osei
|
||||
Diego Schaefer
|
||||
Dionna Chavers
|
||||
Doretha Folden
|
||||
Edna Codner
|
||||
Elfreda Capron
|
||||
Eli Ekhoff
|
||||
Elijah Canard
|
||||
Emile Steenburg
|
||||
Erline Santiago
|
||||
Ervin Kennemore
|
||||
Ezekiel Clinkenbeard
|
||||
Felisa Burmeister
|
||||
Fleta Mckiney
|
||||
Frankie Mires
|
||||
Gaston Spille
|
||||
Gerardo Mandiola
|
||||
Gilda Wilbers
|
||||
Guadalupe Boutiette
|
||||
Hannah Perloff
|
||||
Jeanna Rundstrom
|
||||
Kandis Netherland
|
||||
Keneth Sigg
|
||||
Kenton Layssard
|
||||
Kimberlee Turlington
|
||||
Kimiko Corlew
|
||||
Latricia Fickas
|
||||
Leanna Wedel
|
||||
Lindsey Mccalister
|
||||
Lucas Trischitta
|
||||
Marhta Genther
|
||||
Mathew Garramone
|
||||
Maxie Coke
|
||||
Micheal Veronesi
|
||||
Miguel Eveland
|
||||
Ona Hardrick
|
||||
Orville Mccarson
|
||||
Raguel Moscowitz
|
||||
Rana Bussmann
|
||||
Rashad Deuser
|
||||
Renata Labate
|
||||
Reva Larger
|
||||
Rey Durtschi
|
||||
Rosalina Merthie
|
||||
Rusty Biafore
|
||||
Samual Moree
|
||||
Samual Plattsmier
|
||||
Scot Cheely
|
||||
Seymour Kohls
|
||||
Shaniqua Khan
|
||||
Shanon Kueny
|
||||
Sharmaine Musel
|
||||
Shelby Prator
|
||||
Sheldon Loiselle
|
||||
Shenita Broxterman
|
||||
Silas Scarth
|
||||
Sol Stockfisch
|
||||
Sonia Otsuka
|
||||
Stephine Daum
|
||||
Stewart Lenzo
|
||||
Theron Carmell
|
||||
Titus Streck
|
||||
Tomi Minasian
|
||||
Tyrone Hopton
|
||||
Usha Horsely
|
||||
Val Hoffmeyer
|
||||
Vicente Perko
|
||||
Virgil Cousin
|
||||
Walton Svoboda
|
||||
Winford Hagwood
|
||||
Yoshiko Dekany
|
||||
Reference in New Issue
Block a user