Re-org submodules, add debug compiler options

This commit is contained in:
Jon Brisbin
2012-04-11 15:44:20 -05:00
parent 492519f150
commit e49014542c
46 changed files with 6 additions and 14 deletions

View File

@@ -0,0 +1,84 @@
package org.springframework.data.rest.webmvc;
import java.io.ByteArrayOutputStream;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.CustomSerializerFactory;
import org.springframework.data.rest.core.SimpleLink;
import org.springframework.data.rest.core.util.FluentBeanSerializer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.view.AbstractView;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
@SuppressWarnings({"unchecked"})
public class JsonView extends AbstractView {
private ObjectMapper mapper = new ObjectMapper();
{
CustomSerializerFactory customSerializerFactory = new CustomSerializerFactory();
customSerializerFactory.addSpecificMapping(SimpleLink.class, new FluentBeanSerializer(SimpleLink.class));
mapper.setSerializerFactory(customSerializerFactory);
}
public JsonView(String mediaType) {
setContentType(mediaType);
}
@Override
protected void renderMergedOutputModel(Map<String, Object> model,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
HttpStatus status = status(model);
response.setStatus(status.value());
String contentType = getContentType();
HttpHeaders headers = headers(model);
if (null != headers) {
for (Map.Entry<String, String> entry : headers.toSingleValueMap().entrySet()) {
response.setHeader(entry.getKey(), entry.getValue());
}
if (null != headers.getContentType()) {
contentType = headers.getContentType().toString();
}
}
response.setContentType(contentType);
Object resource = model.get("resource");
if (null != resource) {
if (resource instanceof Throwable) {
resource = ((Throwable) resource).getMessage();
}
ByteArrayOutputStream bout = new ByteArrayOutputStream();
mapper.writerWithDefaultPrettyPrinter().writeValue(bout, resource);
response.getOutputStream().write(bout.toByteArray());
} else {
response.setContentLength(0);
}
}
private HttpStatus status(Map<String, Object> model) {
Object o = model.get("status");
if (null != o && o instanceof HttpStatus) {
return (HttpStatus) o;
}
throw new IllegalArgumentException("No status is set in the model.");
}
private HttpHeaders headers(Map<String, Object> model) {
Object o = model.get("headers");
if (null != o && o instanceof HttpHeaders) {
return (HttpHeaders) o;
}
return null;
}
}

View File

@@ -0,0 +1,28 @@
package org.springframework.data.rest.webmvc;
import java.util.ArrayList;
import java.util.List;
import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.map.annotate.JsonDeserialize;
import org.springframework.data.rest.core.Link;
import org.springframework.data.rest.core.SimpleLink;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class Links {
private List<SimpleLink> links = new ArrayList<SimpleLink>();
public Links add(SimpleLink link) {
links.add(link);
return this;
}
@JsonProperty("_links")
public List<SimpleLink> getLinks() {
return this.links;
}
}

View File

@@ -0,0 +1,67 @@
package org.springframework.data.rest.webmvc;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.persistence.EntityManagerFactory;
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.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.rest.repository.JpaRepositoryMetadata;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
@Configuration
@ImportResource("classpath*:META-INF/spring-data-rest/**/*-export.xml")
public class RepositoryRestConfiguration {
@Autowired
EntityManagerFactory entityManagerFactory;
@Autowired(required = false)
JpaRepositoryMetadata jpaRepositoryMetadata;
@Autowired(required = false)
ConversionService customConversionService;
ConversionService defaultConversionService = new DefaultConversionService();
@Autowired(required = false)
List<HttpMessageConverter<?>> httpMessageConverters = new ArrayList<HttpMessageConverter<?>>();
@Bean ConversionService conversionService() {
if (null != customConversionService) {
return customConversionService;
} else {
return defaultConversionService;
}
}
@Bean List<HttpMessageConverter<?>> httpMessageConverters() {
if (httpMessageConverters.isEmpty()) {
MappingJacksonHttpMessageConverter json = new MappingJacksonHttpMessageConverter();
json.setSupportedMediaTypes(
Arrays.asList(MediaType.APPLICATION_JSON, MediaType.valueOf("application/x-spring-data+json"))
);
httpMessageConverters.add(json);
}
return httpMessageConverters;
}
@Bean JpaRepositoryMetadata jpaRepositoryMetadata() throws Exception {
if (null == jpaRepositoryMetadata) {
jpaRepositoryMetadata = new JpaRepositoryMetadata();
}
return jpaRepositoryMetadata;
}
@Bean PersistenceAnnotationBeanPostProcessor persistenceAnnotationBeanPostProcessor() {
return new PersistenceAnnotationBeanPostProcessor();
}
}

View File

@@ -0,0 +1,968 @@
package org.springframework.data.rest.webmvc;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.PluralAttribute;
import javax.persistence.metamodel.SingularAttribute;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.rest.core.Handler;
import org.springframework.data.rest.core.Link;
import org.springframework.data.rest.core.SimpleLink;
import org.springframework.data.rest.core.util.UriUtils;
import org.springframework.data.rest.repository.JpaEntityMetadata;
import org.springframework.data.rest.repository.JpaRepositoryMetadata;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
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.util.UriComponentsBuilder;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
@Controller
public class RepositoryRestController implements InitializingBean {
public static final String STATUS = "status";
public static final String HEADERS = "headers";
public static final String LOCATION = "Location";
public static final String RESOURCE = "resource";
public static final String SELF = "self";
public static final String LINKS = "_links";
public static final int HAS_RESOURCE = 1;
public static final int HAS_RESOURCE_ID = 2;
public static final int HAS_SECOND_LEVEL_RESOURCE = 3;
public static final int HAS_SECOND_LEVEL_ID = 4;
private static final Logger LOG = LoggerFactory.getLogger(RepositoryRestController.class);
private MediaType uriListMediaType = MediaType.parseMediaType("text/uri-list");
private MediaType jsonMediaType = MediaType.parseMediaType("application/x-spring-data+json");
private JpaRepositoryMetadata repositoryMetadata;
private Map<CrudRepository, TypeMetaCacheEntry> typeMetaCache = new ConcurrentHashMap<CrudRepository, TypeMetaCacheEntry>();
private ConversionService conversionService = new DefaultConversionService();
private List<HttpMessageConverter<?>> httpMessageConverters;
public JpaRepositoryMetadata getRepositoryMetadata() {
return repositoryMetadata;
}
public void setRepositoryMetadata(JpaRepositoryMetadata repositoryMetadata) {
this.repositoryMetadata = repositoryMetadata;
}
public JpaRepositoryMetadata repositoryMetadata() {
return repositoryMetadata;
}
public RepositoryRestController repositoryMetadata(JpaRepositoryMetadata repositoryMetadata) {
this.repositoryMetadata = repositoryMetadata;
return this;
}
public ConversionService getConversionService() {
return conversionService;
}
public void setConversionService(ConversionService conversionService) {
this.conversionService = conversionService;
}
public ConversionService conversionService() {
return conversionService;
}
public RepositoryRestController conversionService(ConversionService conversionService) {
this.conversionService = conversionService;
return this;
}
public List<HttpMessageConverter<?>> getHttpMessageConverters() {
return httpMessageConverters;
}
public void setHttpMessageConverters(List<HttpMessageConverter<?>> httpMessageConverters) {
this.httpMessageConverters = httpMessageConverters;
}
public List<HttpMessageConverter<?>> httpMessageConverters() {
return httpMessageConverters;
}
public RepositoryRestController httpMessageConverters(List<HttpMessageConverter<?>> httpMessageConverters) {
this.httpMessageConverters = httpMessageConverters;
return this;
}
public MediaType getUriListMediaType() {
return uriListMediaType;
}
public void setUriListMediaType(MediaType uriListMediaType) {
this.uriListMediaType = uriListMediaType;
}
public void setUriListMediaType(String uriListMediaType) {
this.uriListMediaType = MediaType.valueOf(uriListMediaType);
}
public MediaType uriListMediaType() {
return uriListMediaType;
}
public RepositoryRestController uriListMediaType(MediaType uriListMediaType) {
setUriListMediaType(uriListMediaType);
return this;
}
public RepositoryRestController uriListMediaType(String uriListMediaType) {
setUriListMediaType(uriListMediaType);
return this;
}
public MediaType getJsonMediaType() {
return jsonMediaType;
}
public void setJsonMediaType(MediaType jsonMediaType) {
this.jsonMediaType = jsonMediaType;
}
public void setJsonMediaType(String jsonMediaType) {
this.jsonMediaType = MediaType.valueOf(jsonMediaType);
}
public MediaType jsonMediaType() {
return jsonMediaType;
}
public RepositoryRestController jsonMediaType(MediaType jsonMediaType) {
setJsonMediaType(jsonMediaType);
return this;
}
public RepositoryRestController jsonMediaType(String jsonMediaType) {
setJsonMediaType(jsonMediaType);
return this;
}
@Override public void afterPropertiesSet() throws Exception {
Assert.notNull(httpMessageConverters, "HttpMessageConverters cannot be null");
}
@RequestMapping(
value = "/",
method = RequestMethod.GET,
produces = {
"application/json"
}
)
public void listRepositories(UriComponentsBuilder uriBuilder,
Model model) {
URI baseUri = uriBuilder.build().toUri();
Links links = new Links();
for (String name : repositoryMetadata.repositoryNames()) {
links.add(new SimpleLink(name, buildUri(baseUri, name)));
}
model.addAttribute(STATUS, HttpStatus.OK);
model.addAttribute(RESOURCE, links);
}
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/{repository}",
method = RequestMethod.GET,
produces = {
"application/json"
}
)
public void listEntities(UriComponentsBuilder uriBuilder,
@PathVariable String repository,
Model model) {
URI baseUri = uriBuilder.build().toUri();
final CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
return;
}
final TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
Links links = new Links();
Iterator iter = repo.findAll().iterator();
while (iter.hasNext()) {
Object o = iter.next();
Serializable id = typeMeta.entityInfo.getId(o);
links.add(new SimpleLink(o.getClass().getSimpleName(), buildUri(baseUri, repository, id.toString())));
}
model.addAttribute(STATUS, HttpStatus.OK);
model.addAttribute(RESOURCE, links);
}
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/{repository}",
method = RequestMethod.POST,
produces = {
"application/json"
}
)
public void create(ServerHttpRequest request,
UriComponentsBuilder uriBuilder,
@PathVariable String repository,
Model model) {
URI baseUri = uriBuilder.build().toUri();
CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_IMPLEMENTED);
return;
}
final TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
MediaType incomingMediaType = request.getHeaders().getContentType();
try {
final Object incoming = readIncoming(request, incomingMediaType, typeMeta.domainClass);
if (null == incoming) {
model.addAttribute(STATUS, HttpStatus.NOT_ACCEPTABLE);
} else {
Object savedEntity = repo.save(incoming);
String sId = typeMeta.entityInfo.getId(savedEntity).toString();
URI selfUri = buildUri(baseUri, repository, sId);
HttpHeaders headers = new HttpHeaders();
headers.set(LOCATION, selfUri.toString());
model.addAttribute(HEADERS, headers);
model.addAttribute(STATUS, HttpStatus.CREATED);
}
} catch (IOException e) {
model.addAttribute(STATUS, HttpStatus.BAD_REQUEST);
LOG.error(e.getMessage(), e);
}
}
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/{repository}/{id}",
method = RequestMethod.GET,
produces = {
"application/json"
}
)
public void entity(ServerHttpRequest request,
UriComponentsBuilder uriBuilder,
@PathVariable String repository,
@PathVariable String id,
Model model) {
URI baseUri = uriBuilder.build().toUri();
CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
return;
}
TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
Serializable serId = stringToSerializable(id, typeMeta.idType);
Object entity = repo.findOne(serId);
if (null == entity) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
} else {
HttpHeaders headers = new HttpHeaders();
Object version = typeMeta.entityMetadata.version(entity);
if (null != version) {
List<String> etags = request.getHeaders().getIfNoneMatch();
for (String etag : etags) {
if (("\"" + version.toString() + "\"").equals(etag)) {
model.addAttribute(STATUS, HttpStatus.NOT_MODIFIED);
return;
}
}
headers.set("ETag", "\"" + version.toString() + "\"");
}
Map<String, Object> entityDto = extractPropertiesLinkAware(entity,
typeMeta.entityMetadata,
UriComponentsBuilder.fromUri(baseUri)
.pathSegment(repository, id)
.build()
.toUri());
addSelfLink(baseUri, entityDto, repository, id);
model.addAttribute(HEADERS, headers);
model.addAttribute(STATUS, HttpStatus.OK);
model.addAttribute(RESOURCE, entityDto);
}
}
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/{repository}/{id}",
method = {
RequestMethod.PUT,
RequestMethod.POST
},
consumes = {
"application/json"
},
produces = {
"application/json"
}
)
public void createOrUpdate(ServerHttpRequest request,
UriComponentsBuilder uriBuilder,
@PathVariable String repository,
@PathVariable String id,
Model model) {
URI baseUri = uriBuilder.build().toUri();
CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_IMPLEMENTED);
return;
}
final TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
Serializable serId = stringToSerializable(id, typeMeta.idType);
Object entity = null;
switch (request.getMethod()) {
case POST:
try {
entity = typeMeta.domainClass.newInstance();
} catch (InstantiationException e) {
model.addAttribute(STATUS, HttpStatus.INTERNAL_SERVER_ERROR);
LOG.error(e.getMessage(), e);
return;
} catch (IllegalAccessException e) {
model.addAttribute(STATUS, HttpStatus.INTERNAL_SERVER_ERROR);
LOG.error(e.getMessage(), e);
return;
}
break;
case PUT:
entity = repo.findOne(serId);
break;
}
if (null == entity) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
} else {
final MediaType incomingMediaType = request.getHeaders().getContentType();
try {
final Object incoming = readIncoming(request, incomingMediaType, typeMeta.domainClass);
if (null == incoming) {
model.addAttribute(STATUS, HttpStatus.BAD_REQUEST);
} else {
typeMeta.entityMetadata.id(serId, incoming);
Object savedEntity = repo.save(entity);
String savedId = typeMeta.entityInfo.getId(savedEntity).toString();
if (request.getMethod() == HttpMethod.POST) {
URI selfUri = buildUri(baseUri, repository, savedId);
HttpHeaders headers = new HttpHeaders();
headers.set(LOCATION, selfUri.toString());
model.addAttribute(HEADERS, headers);
model.addAttribute(STATUS, HttpStatus.CREATED);
} else {
model.addAttribute(STATUS, HttpStatus.NO_CONTENT);
}
}
} catch (IOException e) {
model.addAttribute(STATUS, HttpStatus.BAD_REQUEST);
LOG.error(e.getMessage(), e);
}
}
}
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/{repository}/{id}",
method = RequestMethod.DELETE
)
public void deleteEntity(@PathVariable String repository,
@PathVariable String id,
Model model) {
CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
return;
}
TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
Serializable serId = stringToSerializable(id, typeMeta.idType);
repo.delete(serId);
model.addAttribute(STATUS, HttpStatus.NO_CONTENT);
}
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/{repository}/{id}/{property}",
method = RequestMethod.GET,
produces = {
"application/json",
"text/uri-list"
}
)
public void propertyOfEntity(UriComponentsBuilder uriBuilder,
@PathVariable String repository,
@PathVariable String id,
@PathVariable String property,
Model model) {
URI baseUri = uriBuilder.build().toUri();
CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
return;
}
TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
Serializable serId = stringToSerializable(id, typeMeta.idType);
Object entity = repo.findOne(serId);
if (null == entity) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
} else {
Attribute attr = typeMeta.entityType.getAttribute(property);
if (null != attr) {
Class<?> childType;
if (attr instanceof PluralAttribute) {
childType = ((PluralAttribute) attr).getElementType().getJavaType();
} else {
childType = attr.getJavaType();
}
CrudRepository childRepo = repositoryMetadata.repositoryFor(childType);
if (null == childRepo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
return;
}
model.addAttribute(STATUS, HttpStatus.OK);
TypeMetaCacheEntry childTypeMeta = typeMetaEntry(childRepo);
Object child = typeMeta.entityMetadata.get(property, entity);
if (null != child) {
Links links = new Links();
if (child instanceof Collection) {
for (Object o : (Collection) child) {
String childId = childTypeMeta.entityInfo.getId(o).toString();
URI uri = buildUri(baseUri, repository, id, property, childId);
links.add(new SimpleLink(childType.getSimpleName(), uri));
}
} else if (child instanceof Map) {
for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) child).entrySet()) {
String childId = childTypeMeta.entityInfo.getId(entry.getValue()).toString();
URI uri = buildUri(baseUri, repository, id, property, childId);
Object oKey = entry.getKey();
String sKey;
if (ClassUtils.isAssignable(oKey.getClass(), String.class)) {
sKey = (String) oKey;
} else {
sKey = conversionService.convert(oKey, String.class);
}
links.add(new SimpleLink(sKey, uri));
}
} else {
String childId = childTypeMeta.entityInfo.getId(child).toString();
URI uri = buildUri(baseUri, repository, id, property, childId);
links.add(new SimpleLink(property, uri));
}
model.addAttribute(RESOURCE, links);
} else {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
}
} else {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
}
}
}
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/{repository}/{id}/{property}",
method = {
RequestMethod.PUT,
RequestMethod.POST
},
consumes = {
"application/json",
"text/uri-list"
},
produces = {
"application/json",
"text/uri-list"
}
)
public void updateLinks(final ServerHttpRequest request,
UriComponentsBuilder uriBuilder,
@PathVariable String repository,
@PathVariable String id,
final @PathVariable String property,
final Model model) {
URI baseUri = uriBuilder.build().toUri();
CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
return;
}
final TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
Serializable serId = stringToSerializable(id, typeMeta.idType);
final Object entity = repo.findOne(serId);
if (null == entity) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
} else {
final Attribute attr = typeMeta.entityMetadata.linkedAttributes().get(property);
if (null != attr) {
final AtomicReference<String> rel = new AtomicReference<String>();
Handler<Object, Void> entityHandler = new Handler<Object, Void>() {
@Override public Void handle(Object childEntity) {
if (attr instanceof PluralAttribute) {
PluralAttribute plAttr = (PluralAttribute) attr;
switch (plAttr.getCollectionType()) {
case COLLECTION:
case LIST: {
Collection c = new ArrayList();
Collection current = (Collection) typeMeta.entityMetadata.get(property, entity);
if (request.getMethod() == HttpMethod.POST && null != current) {
c.addAll(current);
}
c.add(childEntity);
typeMeta.entityMetadata.set(property, c, entity);
}
break;
case SET: {
Set s = new HashSet();
Set current = (Set) typeMeta.entityMetadata.get(property, entity);
if (request.getMethod() == HttpMethod.POST && null != current) {
s.addAll(current);
}
s.add(childEntity);
typeMeta.entityMetadata.set(property, s, entity);
}
break;
case MAP: {
Map m = new HashMap();
Map current = (Map) typeMeta.entityMetadata.get(property, entity);
if (request.getMethod() == HttpMethod.POST && null != current) {
m.putAll(current);
}
String key = rel.get();
if (null == key) {
model.addAttribute(STATUS, HttpStatus.NOT_ACCEPTABLE);
return null;
} else {
m.put(rel.get(), childEntity);
typeMeta.entityMetadata.set(property, m, entity);
}
}
break;
}
} else if (attr instanceof SingularAttribute) {
typeMeta.entityMetadata.set(property, childEntity, entity);
}
return null;
}
};
MediaType incomingMediaType = request.getHeaders().getContentType();
try {
if (uriListMediaType.equals(incomingMediaType)) {
BufferedReader in = new BufferedReader(new InputStreamReader(request.getBody()));
String line;
while (null != (line = in.readLine())) {
String sLinkUri = line.trim();
Object o = resolveTopLevelResource(baseUri, sLinkUri);
if (null != o) {
entityHandler.handle(o);
}
}
} else if (jsonMediaType.equals(incomingMediaType)) {
final Map<String, List<Map<String, String>>> incoming = readIncoming(request, incomingMediaType, Map.class);
for (Map<String, String> link : incoming.get(LINKS)) {
String sLinkUri = link.get("href");
Object o = resolveTopLevelResource(baseUri, sLinkUri);
rel.set(link.get("rel"));
if (null != o) {
entityHandler.handle(o);
}
}
}
repo.save(entity);
if (request.getMethod() == HttpMethod.PUT) {
model.addAttribute(STATUS, HttpStatus.NO_CONTENT);
} else {
model.addAttribute(STATUS, HttpStatus.CREATED);
}
} catch (IOException e) {
model.addAttribute(STATUS, HttpStatus.INTERNAL_SERVER_ERROR);
LOG.error(e.getMessage(), e);
}
} else {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
}
}
}
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/{repository}/{id}/{property}",
method = {
RequestMethod.DELETE
}
)
public void clearLinks(@PathVariable String repository,
@PathVariable String id,
@PathVariable String property,
Model model) {
CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
return;
}
final TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
Serializable serId = stringToSerializable(id, typeMeta.idType);
final Object entity = repo.findOne(serId);
if (null == entity) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
} else {
final Attribute attr = typeMeta.entityMetadata.linkedAttributes().get(property);
if (null != attr) {
typeMeta.entityMetadata.set(property, null, entity);
repo.save(entity);
model.addAttribute(STATUS, HttpStatus.NO_CONTENT);
} else {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
}
}
}
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/{repository}/{id}/{property}/{childId}",
method = {
RequestMethod.GET
},
produces = {
"application/json"
}
)
public void childEntity(UriComponentsBuilder uriBuilder,
@PathVariable String repository,
@PathVariable String id,
@PathVariable String property,
@PathVariable String childId,
Model model) {
URI baseUri = uriBuilder.build().toUri();
CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
return;
}
final TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
Serializable serId = stringToSerializable(id, typeMeta.idType);
final Object entity = repo.findOne(serId);
if (null != entity) {
final Attribute attr = typeMeta.entityMetadata.linkedAttributes().get(property);
if (null != attr) {
// Find child entity
CrudRepository childRepo = repositoryFromAttribute(attr);
if (null != childRepo) {
TypeMetaCacheEntry childTypeMeta = typeMetaEntry(childRepo);
Serializable sChildId = stringToSerializable(childId, childTypeMeta.idType);
Object childEntity = childRepo.findOne(sChildId);
if (null != childEntity) {
Map<String, Object> entityDto = extractPropertiesLinkAware(childEntity,
childTypeMeta.entityMetadata,
baseUri);
URI selfUri = addSelfLink(baseUri, entityDto, repository, id);
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Location", selfUri.toString());
model.addAttribute(HEADERS, headers);
model.addAttribute(STATUS, HttpStatus.OK);
model.addAttribute(RESOURCE, entityDto);
return;
}
}
}
}
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
}
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/{repository}/{id}/{property}/{childId}",
method = {
RequestMethod.DELETE
}
)
public void deleteLink(@PathVariable String repository,
@PathVariable String id,
@PathVariable String property,
@PathVariable String childId,
Model model) {
CrudRepository repo = repositoryMetadata.repositoryFor(repository);
if (null == repo) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
return;
}
final TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
Serializable serId = stringToSerializable(id, typeMeta.idType);
final Object entity = repo.findOne(serId);
if (null == entity) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
} else {
final Attribute attr = typeMeta.entityMetadata.linkedAttributes().get(property);
if (null != attr) {
// Find child entity
CrudRepository childRepo = repositoryFromAttribute(attr);
if (null != childRepo) {
TypeMetaCacheEntry childTypeMeta = typeMetaEntry(childRepo);
Serializable sChildId = stringToSerializable(childId, childTypeMeta.idType);
Object childEntity = childRepo.findOne(sChildId);
if (null != childEntity) {
// Remove child entity from relationship based on property type
if (attr instanceof PluralAttribute) {
PluralAttribute plAttr = (PluralAttribute) attr;
switch (plAttr.getCollectionType()) {
case COLLECTION:
case LIST:
Collection c = (Collection) typeMeta.entityMetadata.get(property, entity);
if (null != c) {
c.remove(childEntity);
}
break;
case SET:
Set s = (Set) typeMeta.entityMetadata.get(property, entity);
if (null != s) {
s.remove(childEntity);
}
break;
case MAP:
Object keyToRemove = null;
Map<Object, Object> m = (Map) typeMeta.entityMetadata.get(property, entity);
if (null != m) {
for (Map.Entry<Object, Object> entry : m.entrySet()) {
Object val = entry.getValue();
if (null != val && val.equals(childEntity)) {
keyToRemove = entry.getKey();
break;
}
}
if (null != keyToRemove) {
m.remove(keyToRemove);
}
}
break;
}
} else if (attr instanceof SingularAttribute) {
typeMeta.entityMetadata.set(property, childEntity, entity);
}
model.addAttribute(STATUS, HttpStatus.NO_CONTENT);
}
} else {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
}
}
}
}
private static URI buildUri(URI baseUri, String... pathSegments) {
return UriComponentsBuilder.fromUri(baseUri).pathSegment(pathSegments).build().toUri();
}
private TypeMetaCacheEntry typeMetaEntry(CrudRepository repo) {
TypeMetaCacheEntry entry = typeMetaCache.get(repo);
if (null == entry) {
entry = new TypeMetaCacheEntry(repo);
typeMetaCache.put(repo, entry);
}
return entry;
}
@SuppressWarnings({"unchecked"})
private CrudRepository repositoryFromAttribute(Attribute attr) {
CrudRepository repo = null;
if (attr instanceof PluralAttribute) {
repo = repositoryMetadata.repositoryFor(((PluralAttribute) attr).getElementType().getJavaType());
} else {
repo = repositoryMetadata.repositoryFor(attr.getJavaType());
}
return repo;
}
@SuppressWarnings({"unchecked"})
private URI addSelfLink(URI baseUri, Map<String, Object> model, String... pathComponents) {
List<Link> links = (List<Link>) model.get(LINKS);
if (null == links) {
links = new ArrayList<Link>();
model.put(LINKS, links);
}
URI selfUri = buildUri(baseUri, pathComponents);
links.add(new SimpleLink(SELF, selfUri));
return selfUri;
}
@SuppressWarnings({"unchecked"})
private <V extends Serializable> V stringToSerializable(String s, Class<V> targetType) {
if (ClassUtils.isAssignable(targetType, String.class)) {
return (V) s;
} else {
return conversionService.convert(s, targetType);
}
}
@SuppressWarnings({"unchecked"})
private Object resolveTopLevelResource(URI baseUri, String uri) {
URI href = URI.create(uri);
URI relativeUri = baseUri.relativize(href);
Stack<URI> uris = UriUtils.explode(baseUri, relativeUri);
if (uris.size() > 1) {
String repoName = UriUtils.path(uris.get(0));
String sId = UriUtils.path(uris.get(1));
CrudRepository repo = repositoryMetadata.repositoryFor(repoName);
EntityInformation entityInfo = repositoryMetadata.entityInfoFor(repo);
Class<? extends Serializable> idType = entityInfo.getIdType();
Serializable serId = stringToSerializable(sId, idType);
return repo.findOne(serId);
}
return null;
}
@SuppressWarnings({"unchecked"})
private <V> V readIncoming(HttpInputMessage request, MediaType incomingMediaType, Class<V> targetType) throws IOException {
for (HttpMessageConverter converter : httpMessageConverters) {
if (converter.canRead(targetType, incomingMediaType)) {
return (V) converter.read(targetType, request);
}
}
return null;
}
@SuppressWarnings({"unchecked"})
private Map<String, Object> extractPropertiesLinkAware(final Object entity,
final JpaEntityMetadata entityMetadata,
final URI baseUri) {
final Map<String, Object> entityDto = new HashMap<String, Object>();
entityMetadata.doWithEmbedded(new Handler<Attribute, Void>() {
@Override public Void handle(Attribute attr) {
String name = attr.getName();
Object val = entityMetadata.get(name, entity);
if (null != val) {
entityDto.put(name, val);
}
return null;
}
});
entityMetadata.doWithLinked(new Handler<Attribute, Void>() {
@Override public Void handle(Attribute attr) {
String name = attr.getName();
URI uri = UriComponentsBuilder.fromUri(baseUri)
.pathSegment(name)
.build()
.toUri();
Link l = new SimpleLink(name, uri);
List<Link> links = (List<Link>) entityDto.get(LINKS);
if (null == links) {
links = new ArrayList<Link>();
entityDto.put(LINKS, links);
}
links.add(l);
return null;
}
});
return entityDto;
}
private class TypeMetaCacheEntry {
EntityInformation entityInfo;
Class<?> domainClass;
Class<? extends Serializable> idType;
EntityType entityType;
JpaEntityMetadata entityMetadata;
@SuppressWarnings({"unchecked"})
private TypeMetaCacheEntry(CrudRepository repo) {
entityInfo = repositoryMetadata.entityInfoFor(repo);
domainClass = entityInfo.getJavaType();
idType = entityInfo.getIdType();
entityType = repositoryMetadata.entityTypeFor(domainClass);
entityMetadata = repositoryMetadata.entityMetadataFor(domainClass);
}
}
}

View File

@@ -0,0 +1,93 @@
package org.springframework.data.rest.webmvc;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.persistence.EntityManagerFactory;
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.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.rest.repository.JpaRepositoryMetadata;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
@Configuration
public class RepositoryRestMvcConfiguration {
@Autowired
RepositoryRestConfiguration parentConfig;
RepositoryRestController repositoryRestController;
@Bean ContentNegotiatingViewResolver contentNegotiatingViewResolver() {
ContentNegotiatingViewResolver viewResolver = new ContentNegotiatingViewResolver();
Map<String, String> jsonTypes = new HashMap<String, String>() {{
put("json", "application/json");
put("sdjson", "application/x-spring-data+json");
put("urilist", "text/uri-list");
}};
viewResolver.setMediaTypes(jsonTypes);
viewResolver.setDefaultViews(
Arrays.asList((View) new JsonView("application/json"),
(View) new JsonView("application/x-spring-data+json"),
(View) new UriListView())
);
return viewResolver;
}
@Bean RepositoryRestController repositoryRestController() throws Exception {
if (null == repositoryRestController) {
this.repositoryRestController = new RepositoryRestController()
.repositoryMetadata(parentConfig.jpaRepositoryMetadata())
.conversionService(parentConfig.conversionService())
.httpMessageConverters(parentConfig.httpMessageConverters())
.jsonMediaType("application/json");
}
return repositoryRestController;
}
@Bean RequestMappingHandlerMapping handlerMapping() {
return new RequestMappingHandlerMapping();
}
@Bean RequestMappingHandlerAdapter handlerAdapter() {
RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
handlerAdapter.setCustomArgumentResolvers(
Arrays.asList((HandlerMethodArgumentResolver) new ServerHttpRequestMethodArgumentResolver())
);
return handlerAdapter;
}
@Bean ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver() {
return new ExceptionHandlerExceptionResolver();
}
@Bean DefaultHandlerExceptionResolver handlerExceptionResolver() {
return new DefaultHandlerExceptionResolver();
}
@Bean ResponseStatusExceptionResolver responseStatusExceptionResolver() {
return new ResponseStatusExceptionResolver();
}
}

View File

@@ -0,0 +1,31 @@
package org.springframework.data.rest.webmvc;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.ClassUtils;
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 <jon@jbrisbin.com>
*/
public class ServerHttpRequestMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override public boolean supportsParameter(MethodParameter parameter) {
return ClassUtils.isAssignable(parameter.getParameterType(), ServerHttpRequest.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
return new ServletServerHttpRequest((HttpServletRequest) webRequest.getNativeRequest());
}
}

View File

@@ -0,0 +1,71 @@
package org.springframework.data.rest.webmvc;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.data.rest.core.Link;
import org.springframework.data.rest.core.SimpleLink;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.view.AbstractView;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class UriListView extends AbstractView {
public UriListView() {
setContentType("text/uri-list");
}
@SuppressWarnings({"unchecked"})
@Override
protected void renderMergedOutputModel(Map<String, Object> model,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
Object resource = model.get("resource");
response.setContentType(getContentType());
HttpStatus status = (HttpStatus) model.get("status");
HttpHeaders headers = (HttpHeaders) model.get("headers");
List<SimpleLink> links = null;
if (resource instanceof List) {
links = (List<SimpleLink>) resource;
} else if (resource instanceof Map) {
Map m = (Map) resource;
Object o = m.get("_links");
if (null != o && o instanceof List) {
links = (List<SimpleLink>) o;
} else {
response.setStatus(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE);
return;
}
} else if (resource instanceof Links) {
links = ((Links) resource).getLinks();
}
if (null != status) {
response.setStatus(status.value());
}
if (null != headers) {
for (Map.Entry<String, String> entry : headers.toSingleValueMap().entrySet()) {
response.setHeader(entry.getKey(), entry.getValue());
}
}
PrintWriter out = response.getWriter();
if (null != links) {
for (Link l : links) {
out.println(l.href().toString());
}
}
out.flush();
}
}

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>org.springframework.data.rest.webmvc.RepositoryRestConfiguration</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>exporter</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<filter>
<filter-name>entityManagerInViewFilter</filter-name>
<filter-class>org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>entityManagerInViewFilter</filter-name>
<servlet-name>exporter</servlet-name>
</filter-mapping>
<servlet-mapping>
<servlet-name>exporter</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>

View File

@@ -0,0 +1,124 @@
package org.springframework.data.rest.webmvc.spec
import org.codehaus.jackson.map.ObjectMapper
import org.codehaus.jackson.map.ser.CustomSerializerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.rest.core.SimpleLink
import org.springframework.data.rest.core.util.FluentBeanSerializer
import org.springframework.data.rest.test.webmvc.Address
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.test.context.ContextConfiguration
import org.springframework.transaction.annotation.Transactional
import org.springframework.ui.ExtendedModelMap
import org.springframework.web.util.UriComponentsBuilder
import spock.lang.Shared
import spock.lang.Specification
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
@ContextConfiguration(classes = [RepositoryRestConfiguration, RepositoryRestMvcConfiguration])
class RepositoryRestControllerSpec extends Specification {
@Shared
UriComponentsBuilder uriBuilder
@Shared
ObjectMapper mapper = new ObjectMapper()
@Autowired
URI baseUri
@Autowired
RepositoryRestController controller
MockHttpServletRequest createRequest(String method, String path) {
return new MockHttpServletRequest(
serverPort: 8080,
requestURI: "/data/$path",
method: method
)
}
def setupSpec() {
uriBuilder = UriComponentsBuilder.fromUriString("http://localhost:8080/data")
def customSerializerFactory = new CustomSerializerFactory()
customSerializerFactory.addSpecificMapping(SimpleLink, new FluentBeanSerializer(SimpleLink))
mapper.setSerializerFactory(customSerializerFactory)
}
@Transactional
def "API Test"() {
given:
def model = new ExtendedModelMap()
when: "listing available repositories"
controller.listRepositories(uriBuilder, model)
def reposLinks = model.resource?.links
then:
model.status == HttpStatus.OK
reposLinks?.size() == 3
when: "adding an entity"
model.clear()
def req = createRequest("POST", "person")
def data = mapper.writeValueAsBytes([name: "John Doe"])
req.content = data
controller.create(new ServletServerHttpRequest(req), uriBuilder, "person", model)
then:
model.status == HttpStatus.CREATED
when: "listing available entities"
model.clear()
controller.listEntities(uriBuilder, "person", model)
def personsLinks = model.resource?.links
then:
model.status == HttpStatus.OK
personsLinks[0].href().toString() == "http://localhost:8080/data/person/1"
when: "getting specific entity"
model.clear()
req = createRequest("GET", "person/1")
controller.entity(new ServletServerHttpRequest(req), uriBuilder, "person", "1", model)
then:
model.resource?.name == "John Doe"
when: "creating child entity"
model.clear()
req = createRequest("POST", "address")
data = mapper.writeValueAsBytes(new Address(["1 W. 1st St."] as String[], "Univille", "ST", "12345"))
req.content = data
controller.create(new ServletServerHttpRequest(req), uriBuilder, "address", model)
then:
model.status == HttpStatus.CREATED
when: "linking child to parent entity"
model.clear()
req = createRequest("POST", "person/1/addresses")
req.contentType = "text/uri-list"
data = "http://localhost:8080/data/address/1".bytes
req.content = data
controller.updateLinks(new ServletServerHttpRequest(req), uriBuilder, "person", "1", "addresses", model)
then:
model.status == HttpStatus.CREATED
when: "getting property of entity"
model.clear()
controller.propertyOfEntity(uriBuilder, "person", "1", "addresses", model)
def addrLinks = model.resource?.links
then:
addrLinks.size() == 1
}
}

View File

@@ -0,0 +1,225 @@
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 <jon@jbrisbin.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

@@ -0,0 +1,65 @@
package org.springframework.data.rest.test.webmvc;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
@Entity
public class Address {
@Id @GeneratedValue private Long id;
private String[] lines;
private String city;
private String province;
private String postalCode;
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;
}
}

View File

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

View File

@@ -0,0 +1,78 @@
package org.springframework.data.rest.test.webmvc;
import java.util.List;
import java.util.Map;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Version;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
@Entity
public class Person {
@Id @GeneratedValue private Long id;
private String name;
@Version
private Long version;
@OneToMany
private List<Address> addresses;
@OneToMany
private Map<String, Profile> profiles;
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 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;
}
}

View File

@@ -0,0 +1,70 @@
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;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class PersonLoader implements InitializingBean {
private PersonRepository personRepository;
private ProfileRepository profileRepository;
private AddressRepository addressRepository;
public PersonRepository getPersonRepository() {
return personRepository;
}
public void setPersonRepository(PersonRepository personRepository) {
this.personRepository = personRepository;
}
public ProfileRepository getProfileRepository() {
return profileRepository;
}
public void setProfileRepository(ProfileRepository profileRepository) {
this.profileRepository = profileRepository;
}
public AddressRepository getAddressRepository() {
return addressRepository;
}
public void setAddressRepository(AddressRepository addressRepository) {
this.addressRepository = addressRepository;
}
@Override public void afterPropertiesSet() throws Exception {
Address pers1addr = addressRepository.save(new Address(new String[]{"1234 W. 1st St."}, "Univille", "ST", "12345"));
Map<String, Profile> pers1profiles = new HashMap<String, Profile>();
Profile twitter = profileRepository.save(new Profile("twitter", "#!/johndoe"));
Profile fb = profileRepository.save(new Profile("facebook", "/johndoe"));
pers1profiles.put("twitter", twitter);
pers1profiles.put("facebook", fb);
Person p1 = personRepository.save(
new Person(
"John Doe",
Arrays.asList(addressRepository.findOne(pers1addr.getId())),
pers1profiles
)
);
Address pers2addr = addressRepository.save(new Address(new String[]{"1234 E. 2nd St."}, "Univille", "ST", "12345"));
Map<String, Profile> pers2profiles = new HashMap<String, Profile>();
Profile twitter2 = profileRepository.save(new Profile("twitter", "#!/janedoe"));
Profile fb2 = profileRepository.save(new Profile("facebook", "/janedoe"));
pers2profiles.put("facebook", fb2);
Person p2 = personRepository.save(new Person("Jane Doe", Arrays.asList(pers2addr), pers2profiles));
}
}

View File

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

View File

@@ -0,0 +1,80 @@
package org.springframework.data.rest.test.webmvc;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
@Entity
public class Profile {
@Id @GeneratedValue private Long id;
private String type;
private String url;
public Profile() {
}
public Profile(String type, String url) {
this.type = type;
this.url = url;
}
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;
}
@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

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

View File

@@ -0,0 +1,16 @@
<?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.Person</class>
<class>org.springframework.data.rest.test.webmvc.Profile</class>
<class>org.springframework.data.rest.test.webmvc.Address</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

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<import resource="shared.xml"/>
<jpa:repositories base-package="org.springframework.data.rest.test.webmvc"/>
<!--
<bean class="org.springframework.data.rest.test.webmvc.PersonLoader">
<property name="personRepository" ref="personRepository"/>
<property name="profileRepository" ref="profileRepository"/>
<property name="addressRepository" ref="addressRepository"/>
</bean>
-->
</beans>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">
<bean id="baseUri" class="java.net.URI">
<constructor-arg value="http://localhost:8080/data"/>
</bean>
<jdbc:embedded-database id="dataSource" type="HSQL"/>
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="generateDdl" value="true"/>
<property name="database" value="HSQL"/>
</bean>
</property>
<property name="persistenceUnitName" value="jpa.sample"/>
</bean>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
</beans>

View File

@@ -0,0 +1,11 @@
#!/bin/sh
curl -d '{"name" : "John Doe"}' -H "Content-Type: application/json" http://localhost:8080/person
curl -d '{"name" : "Jane Doe"}' -H "Content-Type: application/json" http://localhost:8080/person
curl -d '{"postalCode":"12345","province":"MO","lines":["1 W 1st St."],"city":"Univille"}' -H "Content-Type: application/json" http://localhost:8080/address
curl -d "http://localhost:8080/address/1" -H "Content-Type: text/uri-list" http://localhost:8080/person/1/addresses
curl -d '{"postalCode":"54321","province":"MO","lines":["2 W 1st St."],"city":"Univille"}' -H "Content-Type: application/json" http://localhost:8080/address
curl -d "http://localhost:8080/address/2" -H "Content-Type: text/uri-list" http://localhost:8080/person/2/addresses
curl -d '{"type" : "twitter", "url": "#!/johndoe"}' -H "Content-Type: application/json" http://localhost:8080/profile
curl -d "http://localhost:8080/profile/1" -H "Content-Type: text/uri-list" http://localhost:8080/person/1/profiles
curl -d '{"type" : "facebook", "url": "/janedoe"}' -H "Content-Type: application/json" http://localhost:8080/profile
curl -d '{"_links": [{"rel":"facebook", "href": "http://localhost:8080/profile/2"}]}' -H "Content-Type: application/json" http://localhost:8080/person/2/profiles

View File

@@ -0,0 +1,18 @@
<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" level="DEBUG"/>
<logger name="org.springframework.data" level="INFO"/>
<root level="INFO">
<appender-ref ref="stdout"/>
</root>
</configuration>