Skip to content

Instantly share code, notes, and snippets.

@jbrisbin
Created December 11, 2012 14:31
Show Gist options
  • Save jbrisbin/4258957 to your computer and use it in GitHub Desktop.
Save jbrisbin/4258957 to your computer and use it in GitHub Desktop.
package org.springframework.data.rest.webmvc;
import static java.lang.String.*;
import static java.util.Collections.*;
import static org.springframework.data.rest.core.util.UriUtils.*;
import static org.springframework.data.rest.repository.support.RepositoryUtils.*;
import static org.springframework.util.ReflectionUtils.*;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.common.base.Function;
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.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
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.repository.core.RepositoryInformation;
import org.springframework.data.repository.support.DomainClassConverter;
import org.springframework.data.rest.config.ResourceMapping;
import org.springframework.data.rest.repository.PersistentEntityResource;
import org.springframework.data.rest.repository.invoke.CrudMethod;
import org.springframework.data.rest.repository.invoke.RepositoryMethod;
import org.springframework.data.rest.repository.support.RepositoryInformationSupport;
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.web.bind.annotation.ExceptionHandler;
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
*/
public class RepositoryRestController extends RepositoryInformationSupport implements ApplicationContextAware {
private static final TypeDescriptor STRING_TYPE = TypeDescriptor.valueOf(String.class);
private ApplicationContext applicationContext;
@Autowired
private DomainClassConverter domainClassConverter;
@Autowired
private ConversionService conversionService;
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* List available {@link org.springframework.data.repository.CrudRepository}s that are being exported.
*
* @param baseUri
* The URI under which all URLs are considered relative.
*
* @return {@link Resources} with links to the available repositories.
*
* @throws IOException
*/
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/",
method = RequestMethod.GET,
produces = {
"application/json",
"application/x-spring-data-compact+json"
}
)
@ResponseBody
public Resources<?> listRepositories(URI baseUri) throws IOException {
return new Resources(emptyList(), getRepositoryLinks(baseUri));
}
/**
* List entities in a compact style by providing only links to the entities rather than inling them into the
* response.
*
* @param repoRequest
* The incoming request.
*
* @return A {@link Resources} of the entity self links.
*
* @throws IOException
*/
@RequestMapping(
value = "/{repository}",
method = RequestMethod.GET,
produces = {
"application/x-spring-data-compact+json"
}
)
@ResponseBody
public Resource<?> listEntitiesCompact(final RepositoryRestRequest repoRequest) throws IOException {
final Resource<?> entityLinks = new Resource<Object>(Collections.emptyList());
doWithEntities(repoRequest,
CrudMethod.FIND_ALL.toMethodName(),
Collections.<String, Object>emptyMap(),
new Function<Resource<?>, Object>() {
@Override public Object apply(Resource<?> resource) {
String entityRel = repoRequest.getPersistentEntityResourceMapping().getRel();
Link l = new Link(resource.getId().getHref(), entityRel);
entityLinks.add(l);
return null;
}
});
List<Link> methodLinks = new ArrayList<Link>();
maybeAddSearchLinks(repoRequest,
getQueryMethodLinks(repoRequest),
methodLinks);
entityLinks.add(methodLinks);
return entityLinks;
}
/**
* List entities of a {@link org.springframework.data.repository.CrudRepository} by invoking
* {@link org.springframework.data.repository.CrudRepository#findAll()} and applying any available paging parameters.
*
* @param repoRequest
* The incoming request.
*
* @return A {@link Resources} of the inlined entities.
*
* @throws IOException
*/
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/{repository}",
method = RequestMethod.GET,
produces = {
"*/*",
"application/json",
"application/x-spring-data-verbose+json"
}
)
@ResponseBody
public Resources<?> listEntities(RepositoryRestRequest repoRequest) throws IOException {
final List<Resource<?>> resources = new ArrayList<Resource<?>>();
List<Link> links = doWithEntities(repoRequest,
CrudMethod.FIND_ALL.toMethodName(),
Collections.<String, Object>emptyMap(),
new Function<Resource<?>, Object>() {
@Override public Object apply(Resource<?> resource) {
resources.add(resource);
return null;
}
});
maybeAddSearchLinks(repoRequest,
getQueryMethodLinks(repoRequest),
links);
return new Resources<Resource<?>>(resources, links);
}
/**
* JSONP version of {@link #listEntities(RepositoryRestRequest)}.
*
* @param repoRequest
* The incoming request.
*
* @return The JSON result wrapped in a call to the requested Javascript function.
*
* @throws IOException
*/
@RequestMapping(
value = "/{repository}",
method = RequestMethod.GET,
produces = {
"application/javascript"
}
)
@ResponseBody
public JsonpResponse<Resources<?>> listEntitiesJsonp(RepositoryRestRequest repoRequest) throws IOException {
String callback = repoRequest.getRequest().getParameter(config.getJsonpParamName());
String errback = repoRequest.getRequest().getParameter(config.getJsonpOnErrParamName());
return new JsonpResponse<Resources<?>>(new ResponseEntity<Resources<?>>(listEntities(repoRequest), HttpStatus.OK),
(null != callback ? callback : config.getJsonpParamName()),
(null != errback ? errback : config.getJsonpOnErrParamName()));
}
@RequestMapping(
value = "/{repository}/search",
method = RequestMethod.GET,
produces = {
"application/json"
}
)
@ResponseBody
public Resource<?> listSearchMethods(RepositoryRestRequest repoRequest) {
final Resource<?> searchMethods = new Resource<Object>(Collections.emptyList());
List<Link> methodLinks = new ArrayList<Link>();
maybeAddSearchLinks(repoRequest,
getQueryMethodLinks(repoRequest),
methodLinks);
searchMethods.add(methodLinks);
return searchMethods;
}
@RequestMapping(
value = "/{repository}/search",
method = RequestMethod.GET,
produces = {
"application/x-spring-data-compact+json"
}
)
@ResponseBody
public Resource<?> listSearchMethodsCompact(RepositoryRestRequest repoRequest) {
return listSearchMethods(repoRequest);
}
/**
* Retrieve a specific entity.
*
* @param repoRequest
* The incoming request.
* @param id
* The ID of the entity to load.
*
* @return A {@link Resource} of the entity.
*/
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/{repository}/{id}",
method = RequestMethod.GET,
produces = {
"application/json"
}
)
@ResponseBody
public Resource<?> getEntity(RepositoryRestRequest repoRequest,
@PathVariable String id) throws EntityNotFoundException {
TypeDescriptor domainType = TypeDescriptor.valueOf(repoRequest.getPersistentEntity().getType());
Object entity = domainClassConverter.convert(id, STRING_TYPE, domainType);
if(null == entity) {
throw new EntityNotFoundException();
}
Link selfLink = repoRequest.buildEntitySelfLink(entity, conversionService);
return new PersistentEntityResource<Object>(repoRequest.getBaseUri(),
repoRequest.getPersistentEntity(),
entity,
selfLink);
}
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/{repository}/{id}",
method = RequestMethod.GET,
produces = {
"application/javascript"
}
)
@ResponseBody
public JsonpResponse<Resource<?>> getEntityJsonp(RepositoryRestRequest repoRequest,
@PathVariable String id) throws EntityNotFoundException {
String callback = repoRequest.getRequest().getParameter(config.getJsonpParamName());
String errback = repoRequest.getRequest().getParameter(config.getJsonpOnErrParamName());
return new JsonpResponse<Resource<?>>(new ResponseEntity<Resource<?>>(getEntity(repoRequest, id), HttpStatus.OK),
callback,
errback);
}
@ExceptionHandler({RepositoryNotFoundException.class, EntityNotFoundException.class})
public ResponseEntity<?> handleNotFound(Throwable t) {
return notFound();
}
private <T> ResponseEntity<T> notFound() {
return notFound(null, null);
}
private <T> ResponseEntity<T> notFound(HttpHeaders headers, T body) {
HttpHeaders hdrs = new HttpHeaders();
if(null != headers) {
hdrs.putAll(headers);
}
return new ResponseEntity<T>(body, headers, HttpStatus.NOT_FOUND);
}
private void maybeAddSearchLinks(RepositoryRestRequest repoRequest,
List<Link> methodLinks,
List<Link> resourceLinks) {
if(methodLinks.size() > 0) {
resourceLinks.add(new Link(buildUri(repoRequest.getBaseUri(),
repoRequest.getRepositoryResourceMapping().getPath(),
"search").toString(),
format("%s.search",
repoRequest.getRepositoryResourceMapping().getRel())));
resourceLinks.addAll(methodLinks);
}
}
private List<Link> getRepositoryLinks(URI baseUri) {
List<Link> links = new ArrayList<Link>();
for(Class<?> domainType : repositories) {
RepositoryInformation repoInfo = findRepositoryInfoFor(domainType);
ResourceMapping mapping = getResourceMapping(config, repoInfo);
if(!mapping.isExported()) {
return null;
}
String href = buildUri(baseUri, mapping.getPath()).toString();
links.add(new Link(href, mapping.getRel()));
}
return links;
}
private List<Link> getQueryMethodLinks(RepositoryRestRequest req) {
List<RepositoryMethod> methods = repositoryMethods.get(req.getRepositoryInformation().getRepositoryInterface());
if(null == methods) {
return Collections.emptyList();
}
List<Link> links = new ArrayList<Link>();
for(RepositoryMethod repoMethod : methods) {
if(!req.getRepositoryInformation().isQueryMethod(repoMethod.getMethod())) {
continue;
}
String name = repoMethod.getMethod().getName();
ResourceMapping methodMapping = req.getRepositoryResourceMapping().getResourceMappingFor(name);
String path = findPath(repoMethod.getMethod());
String rel = findRel(repoMethod.getMethod());
if(null != methodMapping) {
if(null != methodMapping.getPath()) {
path = methodMapping.getPath();
}
if(null != methodMapping.getRel()) {
rel = methodMapping.getRel();
}
}
links.add(new Link(buildUri(req.getBaseUri(),
req.getRepositoryResourceMapping().getPath(),
"search",
path).toString(),
rel));
}
return links;
}
@SuppressWarnings({"unchecked"})
private List<Link> doWithEntities(final RepositoryRestRequest req,
String path,
final Map<String, Object> params,
Function<Resource<?>, ?> fn) {
final List<Link> links = new ArrayList<Link>();
Iterable<?> results = doWithRepositoryMethod(
req,
path,
new Function<RepositoryMethod, Iterable<?>>() {
@Override public Iterable<?> apply(RepositoryMethod repoMethod) {
Iterable<?> results;
// Build up method parameters
List<Object> methodParams = new ArrayList<Object>();
int i = 0;
for(Class<?> paramType : repoMethod.getParamTypes()) {
if(Pageable.class.isAssignableFrom(paramType)) {
methodParams.add(new PageRequest(req.getPagingAndSorting().getPageNumber(),
req.getPagingAndSorting().getPageSize(),
req.getPagingAndSorting().getSort()));
continue;
}
if(Sort.class.isAssignableFrom(paramType)) {
methodParams.add(req.getPagingAndSorting().getSort());
continue;
}
String name = repoMethod.getParamNames()[i++];
Object paramValue = params.get(name);
if(null != paramValue && !paramType.isAssignableFrom(paramValue.getClass())) {
paramValue = conversionService.convert(paramValue, paramType);
}
methodParams.add(paramValue);
}
// Invoke method
Object result = invokeMethod(repoMethod.getMethod(), req.getRepository(), methodParams.toArray());
// Interrogate result
if(result instanceof Page) {
Page page = (Page)result;
if(page.hasPreviousPage()) {
req.addPrevLink(page, links);
}
if(page.hasNextPage()) {
req.addNextLink(page, links);
}
results = (page.hasContent() ? page : Collections.emptyList());
} else if(result instanceof Iterable) {
results = (Iterable)result;
} else {
results = Collections.singletonList(result);
}
return results;
}
}
);
for(Object o : results) {
fn.apply(new PersistentEntityResource<Object>(req.getBaseUri(),
req.getPersistentEntity(),
o,
req.buildEntitySelfLink(o, conversionService)));
}
return links;
}
private <T> T doWithRepositoryMethod(RepositoryRestRequest req,
String path,
Function<RepositoryMethod, T> fn) {
List<RepositoryMethod> repoMethods = repositoryMethods.get(req.getRepositoryInformation().getRepositoryInterface());
for(RepositoryMethod repoMethod : repoMethods) {
String repoMethodName = repoMethod.getMethod().getName();
if(repoMethodName.equals(path)) {
T t = fn.apply(repoMethod);
if(null != t) {
return t;
}
}
if(req.getRepositoryResourceMapping().hasResourceMappingFor(repoMethodName)) {
ResourceMapping methodMapping = req.getRepositoryResourceMapping().getResourceMappingFor(repoMethodName);
if(!path.equals(methodMapping.getPath()) || !methodMapping.isExported()) {
continue;
}
T t = fn.apply(repoMethod);
if(null != t) {
return t;
}
}
}
throw new RepositoryNotFoundException();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment