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

Additional changes include:

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

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

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

View File

@@ -0,0 +1,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);
}
}

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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]);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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 {
}

View File

@@ -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) {
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
/**

View File

@@ -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;
}
}

View File

@@ -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"
))
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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")
}
}

View File

@@ -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"
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
};
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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();
}
}

View File

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

View File

@@ -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)));
}
}

View File

@@ -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> {
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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> {
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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> {
}

View File

@@ -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;
}
}

View File

@@ -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> {
}

View File

@@ -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;
}
}

View File

@@ -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> {
}

View File

@@ -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;
}
}

View File

@@ -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> {
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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
);
}

View File

@@ -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");
}
}

View File

@@ -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 + '\'' +
'}';
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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.");
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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> {
}

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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