This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class DiscoverySpec extends BaseSpec {
|
||||
def links = readJson(response).links
|
||||
|
||||
then:
|
||||
links.size() == 5
|
||||
links.size() == 6
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user