#28, #29: Support for nested objects as links in the JSON representation.

This commit is contained in:
Jon Brisbin
2012-08-17 08:46:25 -05:00
committed by Jon Brisbin
parent 3da1a241e0
commit 2dd0511f8f
12 changed files with 236 additions and 113 deletions

View File

@@ -26,8 +26,9 @@ public abstract class RepositoryExporter<M extends RepositoryMetadata<E>, E exte
protected ApplicationContext applicationContext;
protected Repositories repositories;
protected List<String> exportOnlyTheseClasses = Collections.emptyList();
protected Map<String, M> repositoryMetadata;
protected Map<String, M> repositoryMetadata;
protected List<String> exportOnlyTheseClasses = Collections.emptyList();
protected Map<Class<?>, Class<?>> domainTypeMappings = new HashMap<Class<?>, Class<?>>();
/**
* Get the list of class names of Repositories to export.
@@ -53,6 +54,16 @@ public abstract class RepositoryExporter<M extends RepositoryMetadata<E>, E exte
return (M)this;
}
public Map<Class<?>, Class<?>> getDomainTypeMappings() {
return domainTypeMappings;
}
@SuppressWarnings({"unchecked"})
public M setDomainTypeMappings(Map<Class<?>, Class<?>> domainTypeMappings) {
this.domainTypeMappings = domainTypeMappings;
return (M)this;
}
@Override public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
@@ -100,6 +111,22 @@ public abstract class RepositoryExporter<M extends RepositoryMetadata<E>, E exte
*/
public M repositoryMetadataFor(Class<?> domainType) {
refresh();
// Look for an exact match
for(M repoMeta : repositoryMetadata.values()) {
if(repoMeta.domainType() == domainType) {
return repoMeta;
}
}
// Didn't find an exact match, look for domain type mapping
Class<?> repoClass = domainTypeMappings.get(domainType);
if(null != repoClass) {
for(M repoMeta : repositoryMetadata.values()) {
if(repoMeta.repositoryClass() == repoClass) {
return repoMeta;
}
}
}
// Didn't find a mapping, look for a superclass
for(M repoMeta : repositoryMetadata.values()) {
if(repoMeta.domainType().isAssignableFrom(domainType)) {
return repoMeta;

View File

@@ -42,8 +42,10 @@ import org.springframework.data.rest.repository.RepositoryExporter;
import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.data.rest.repository.UriToDomainObjectResolver;
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;
import org.springframework.web.util.UriComponentsBuilder;
@@ -101,14 +103,7 @@ public class RepositoryAwareMappingHttpMessageConverter
@SuppressWarnings({"unchecked"})
public RepositoryAwareMappingHttpMessageConverter setRepositoryExporters(List<RepositoryExporter> repositoryExporters) {
this.repositoryExporters = repositoryExporters;
for(RepositoryExporter repoExp : repositoryExporters) {
for(String repoName : new ArrayList<String>(repoExp.repositoryNames())) {
RepositoryMetadata repoMeta = repoExp.repositoryMetadataFor(repoName);
Class domainType = repoMeta.entityMetadata().type();
}
}
this.mapper.registerModule(new RepositoryAwareModule());
return this;
}
@@ -174,6 +169,16 @@ public class RepositoryAwareMappingHttpMessageConverter
}
}
@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);
}
}
@SuppressWarnings({"unchecked"})
private RepositoryMetadata repositoryMetadataFor(Class<?> domainType) {
for(RepositoryExporter repoExp : repositoryExporters) {
@@ -259,9 +264,9 @@ public class RepositoryAwareMappingHttpMessageConverter
}
String rel = repoMeta.rel() + "." + repoMeta.domainType().getSimpleName();
URI href = buildUri(RepositoryRestController.BASE_URI.get(), repoMeta.name(), sId);
URI selfUri = buildUri(RepositoryRestController.BASE_URI.get(), repoMeta.name(), sId);
jgen.writeObject(new ResourceLink(rel, href));
jgen.writeObject(new ResourceLink(rel, selfUri));
}
}
@@ -320,89 +325,120 @@ public class RepositoryAwareMappingHttpMessageConverter
throw ctxt.instantiationException(getValueClass(), e);
}
for(JsonToken tok = jp.getCurrentToken(); tok != JsonToken.END_OBJECT; tok = jp.nextToken()) {
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(
RepositoryRestController.BASE_URI.get(),
URI.create(name.substring(1))
);
} else if("href".equals(name)) {
continue;
}
if("href".equals(name)) {
entity = domainObjectResolver.resolve(
RepositoryRestController.BASE_URI.get(),
URI.create(jp.nextTextValue())
);
} else if("rel".equals(name)) {
// rel is currently ignored
} else {
// Read the attribute metadata
AttributeMetadata attrMeta = repoMeta.entityMetadata().attribute(name);
if(null == attrMeta) {
continue;
}
// Try and read the value of this attribute.
// The method of doing that varies based on the type of the property.
Object val;
if(attrMeta.isCollectionLike()) {
Collection c = attrMeta.asCollection(entity);
if(null == c) {
c = new ArrayList();
}
continue;
}
if(jp.getCurrentToken() == JsonToken.START_ARRAY) {
while((tok = jp.nextToken()) != JsonToken.END_ARRAY) {
Object cval = jp.readValueAs(attrMeta.elementType());
c.add(cval);
}
}
if("rel".equals(name)) {
// rel is currently ignored
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) {
do {
Object cval = jp.readValueAs(attrMeta.elementType());
c.add(cval);
} while((tok = jp.nextToken()) != JsonToken.END_ARRAY);
val = c;
} else if(attrMeta.isSetLike()) {
Set s = attrMeta.asSet(entity);
if(null == s) {
s = new HashSet();
}
if(jp.getCurrentToken() == JsonToken.START_ARRAY) {
while((tok = jp.nextToken()) != JsonToken.END_ARRAY) {
Object sval = jp.readValueAs(attrMeta.elementType());
s.add(sval);
}
}
} 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) {
do {
Object sval = jp.readValueAs(attrMeta.elementType());
s.add(sval);
} while((tok = jp.nextToken()) != JsonToken.END_ARRAY);
val = s;
} else if(attrMeta.isMapLike()) {
Map m = attrMeta.asMap(entity);
if(null == m) {
m = new HashMap();
}
if(jp.getCurrentToken() == JsonToken.START_OBJECT) {
while((tok = jp.nextToken()) != JsonToken.END_OBJECT) {
name = jp.getCurrentName();
Object mkey = (
name.startsWith("@http")
? domainObjectResolver.resolve(
RepositoryRestController.BASE_URI.get(),
URI.create(name.substring(1))
)
: name
);
tok = jp.nextToken();
Object mval = jp.readValueAs(attrMeta.elementType());
} 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();
}
m.put(mkey, mval);
}
}
if((tok = jp.nextToken()) == JsonToken.START_OBJECT) {
do {
name = jp.getCurrentName();
Object mkey = (
name.startsWith("@http")
? domainObjectResolver.resolve(
RepositoryRestController.BASE_URI.get(),
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;
}
}

View File

@@ -1112,7 +1112,7 @@ public class RepositoryRestController
body = new MapResource(resource);
} else {
String propValId = idAttr.get(propVal).toString();
URI path = buildUri(baseUri, repository, id, property, propValId);
URI path = buildUri(baseUri, repository, id, property);
URI selfUri = buildUri(baseUri, propRepoMeta.name(), propValId);
if(shouldReturnLinks(accept)) {
Resource<?> resource = new Resource<Object>();
@@ -1122,7 +1122,7 @@ public class RepositoryRestController
MapResource res = createResource(propRepoMeta.rel(),
propVal,
propRepoMeta.entityMetadata(),
buildUri(baseUri, propRepoMeta.name(), propValId));
selfUri);
res.addLink(new ResourceLink(propertyRel, path));
res.addLink(new ResourceLink(SELF, selfUri));
body = res;
@@ -1214,6 +1214,14 @@ public class RepositoryRestController
m.put(rel.get(), linkedEntity);
attrMeta.set(m, entity);
} else {
// Don't support POST when it's a single value
if(request.getMethod() == HttpMethod.POST) {
try {
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
} catch(IOException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
attrMeta.set(linkedEntity, entity);
}

View File

@@ -6,6 +6,7 @@ import org.springframework.context.ApplicationContext
import org.springframework.data.rest.test.webmvc.Address
import org.springframework.data.rest.test.webmvc.AddressRepository
import org.springframework.data.rest.test.webmvc.ApplicationConfig
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.TestRepositoryEventListener
@@ -36,6 +37,7 @@ abstract class BaseSpec extends Specification {
@Autowired EntityManagerFactory emf
@Autowired PersonRepository people
@Autowired AddressRepository addresses
@Autowired CustomerRepository customers
UriComponentsBuilder baseUri
ObjectMapper mapper = new ObjectMapper()

View File

@@ -25,7 +25,7 @@ class DiscoverySpec extends BaseSpec {
def links = readJson(response).links
then:
links.size() == 5
links.size() == 6
}

View File

@@ -0,0 +1,39 @@
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,29 +1,29 @@
package org.springframework.data.rest.test.webmvc;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.OneToOne;
/**
* @author Jon Brisbin
*/
@MappedSuperclass
@Entity
public class Customer {
@Id @GeneratedValue private Long id;
@OneToOne private Person person;
private String userid;
public Long getId() {
return id;
}
public Person getPerson() {
return person;
public String getUserid() {
return userid;
}
public void setPerson(Person person) {
this.person = person;
public Customer setUserid(String userid) {
this.userid = userid;
return this;
}
}

View File

@@ -5,5 +5,5 @@ import org.springframework.data.repository.CrudRepository;
/**
* @author Jon Brisbin
*/
public interface WebCustomerRepository extends CrudRepository<WebCustomer, Long> {
public interface CustomerRepository extends CrudRepository<Customer, Long> {
}

View File

@@ -0,0 +1,34 @@
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

@@ -0,0 +1,9 @@
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

@@ -6,6 +6,7 @@ import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.MapKey;
import javax.persistence.MappedSuperclass;
import javax.persistence.OneToMany;
import javax.persistence.Version;

View File

@@ -1,33 +0,0 @@
package org.springframework.data.rest.test.webmvc;
import java.util.Map;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
/**
* @author Jon Brisbin
*/
@Entity
public class WebCustomer extends Customer {
private String username;
@OneToMany
private Map<Profile, Person> people;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Map<Profile, Person> getPeople() {
return people;
}
public void setPeople(Map<Profile, Person> people) {
this.people = people;
}
}