Created
December 11, 2012 14:31
-
-
Save jbrisbin/4258957 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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