DATAREST-39: Modified the DELETEing of links so that an attempt to delete a required relationship will result in a 405 Method Not Allowed. Had to change the entities and tests to accommodate the new required relationship on the Address entity, which pingponged into a number of changes.

This commit is contained in:
Jon Brisbin
2012-08-17 13:37:26 -05:00
committed by Jon Brisbin
parent 2dd0511f8f
commit b9b6a8fe92
11 changed files with 130 additions and 35 deletions

View File

@@ -1,5 +1,6 @@
package org.springframework.data.rest.repository;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
@@ -90,6 +91,27 @@ public interface AttributeMetadata {
*/
Map asMap(Object target);
/**
* Does this attribute have the given annotation on it?
*
* @param annoType
* The type of annotation to search for.
*
* @return {@literal true} if this annotation exists on this attribute, {@literal false} otherwise.
*/
boolean hasAnnotation(Class<? extends Annotation> annoType);
/**
* Get the given annotation.
*
* @param annoType
* The type of annotation to get.
* @param <A>
*
* @return The annotation, or {@literal null} if it doesn't exist.
*/
<A extends Annotation> A annotation(Class<A> annoType);
/**
* Get the path of this attribute.
*

View File

@@ -1,6 +1,7 @@
package org.springframework.data.rest.repository.jpa;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
@@ -126,6 +127,14 @@ public class JpaAttributeMetadata implements AttributeMetadata {
return (Map)get(target);
}
@Override public boolean hasAnnotation(Class<? extends Annotation> annoType) {
return field.isAnnotationPresent(annoType);
}
@Override public <A extends Annotation> A annotation(Class<A> annoType) {
return field.getAnnotation(annoType);
}
@Override public Object get(Object target) {
try {
if(null != getter) {

View File

@@ -74,13 +74,11 @@ public class RepositoryAwareMappingHttpMessageConverter
MediaTypes.COMPACT_JSON,
MediaTypes.VERBOSE_JSON
));
mapper.registerModule(new RepositoryAwareModule());
setObjectMapper(mapper);
}
@Override public void afterPropertiesSet() throws Exception {
mapper.registerModule(new RepositoryAwareModule());
for(Module m : modules) {
mapper.registerModule(m);
}
@@ -194,6 +192,12 @@ public class RepositoryAwareMappingHttpMessageConverter
}
private class RepositoryAwareModule extends SimpleModule {
SimpleSerializers sers = new SimpleSerializers();
SimpleDeserializers dsers = new SimpleDeserializers();
SimpleSerializers keySers = new SimpleSerializers();
SimpleKeyDeserializers keyDsers = new SimpleKeyDeserializers();
private RepositoryAwareModule() {
super("RepositoryAwareModule", new Version(1, 0, 0, "SNAPSHOT"));
}
@@ -204,10 +208,6 @@ public class RepositoryAwareMappingHttpMessageConverter
new SimpleAbstractTypeResolver()
.addMapping(Link.class, ResourceLink.class)
);
SimpleSerializers sers = new SimpleSerializers();
SimpleDeserializers dsers = new SimpleDeserializers();
SimpleSerializers keySers = new SimpleSerializers();
SimpleKeyDeserializers keyDsers = new SimpleKeyDeserializers();
for(RepositoryExporter repoExp : repositoryExporters) {
for(String repoName : new ArrayList<String>(repoExp.repositoryNames())) {

View File

@@ -20,6 +20,8 @@ import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;
import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;
import javax.servlet.http.HttpServletRequest;
import org.codehaus.jackson.map.ObjectMapper;
@@ -1065,9 +1067,9 @@ public class RepositoryRestController
AttributeMetadata idAttr = propRepoMeta.entityMetadata().idAttribute();
String propertyRel = repository +
"." + entity.getClass().getSimpleName() +
"." + property +
"." + propRepoMeta.entityMetadata().type().getSimpleName();
"." + property;
if(propVal instanceof Collection) {
propertyRel += "." + propRepoMeta.entityMetadata().type().getSimpleName();
ResourceSet resources = new ResourceSet();
for(Object o : (Collection)propVal) {
String propValId = idAttr.get(o).toString();
@@ -1088,6 +1090,7 @@ public class RepositoryRestController
}
body = resources;
} else if(propVal instanceof Map) {
propertyRel += "." + propRepoMeta.entityMetadata().type().getSimpleName();
Map resource = new HashMap();
for(Map.Entry<Object, Object> entry : ((Map<Object, Object>)propVal).entrySet()) {
String propValId = idAttr.get(entry.getValue()).toString();
@@ -1427,6 +1430,7 @@ public class RepositoryRestController
@PathVariable String linkedId) throws IOException {
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
CrudRepository repo = repoMeta.repository();
// If I can't load the parent entity, then this method isn't allowed.
if(!repoMeta.exportsMethod(CrudMethod.FIND_ONE) || !repoMeta.exportsMethod(CrudMethod.SAVE_ONE)) {
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
}
@@ -1440,6 +1444,12 @@ public class RepositoryRestController
return notFoundResponse(request);
}
// Check if this @*ToOne relationship is optional and if not, fail with a 405 Method Not Allowed
if((attrMeta.hasAnnotation(ManyToOne.class) && !attrMeta.annotation(ManyToOne.class).optional())
|| (attrMeta.hasAnnotation(OneToOne.class) && !attrMeta.annotation(OneToOne.class).optional())) {
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
}
// Find linked entity
RepositoryMetadata linkedRepoMeta;
if(null == (linkedRepoMeta = repositoryMetadataFor(attrMeta))) {
@@ -1460,18 +1470,18 @@ public class RepositoryRestController
// Remove linked entity from relationship based on property type
if(attrMeta.isCollectionLike()) {
Collection c = attrMeta.asCollection(entity);
if(null != c) {
if(null != c && c != Collections.emptyList()) {
c.remove(linkedEntity);
}
} else if(attrMeta.isSetLike()) {
Set s = attrMeta.asSet(entity);
if(null != s) {
if(null != s && s != Collections.emptySet()) {
s.remove(linkedEntity);
}
} else if(attrMeta.isMapLike()) {
Object keyToRemove = null;
Map<Object, Object> m = attrMeta.asMap(entity);
if(null != m) {
if(null != m && m != Collections.emptyMap()) {
for(Map.Entry<Object, Object> entry : m.entrySet()) {
Object val = entry.getValue();
if(null != val && val.equals(linkedEntity)) {
@@ -1484,7 +1494,7 @@ public class RepositoryRestController
}
}
} else {
attrMeta.set(linkedEntity, entity);
attrMeta.set(null, entity);
}
publishEvent(new BeforeLinkDeleteEvent(entity, linkedEntity));
@@ -1698,7 +1708,8 @@ public class RepositoryRestController
for(String attrName : entityMetadata.linkedAttributes().keySet()) {
URI uri = buildUri(baseUri, attrName);
resource.addLink(new ResourceLink(repoRel + "." + entity.getClass().getSimpleName() + "." + attrName, uri));
String rel = repoRel + "." + entity.getClass().getSimpleName() + "." + attrName;
resource.addLink(new ResourceLink(rel, uri));
}
return resource;

View File

@@ -39,7 +39,8 @@ abstract class BaseSpec extends Specification {
@Autowired AddressRepository addresses
@Autowired CustomerRepository customers
UriComponentsBuilder baseUri
ObjectMapper mapper = new ObjectMapper()
def mapper = new ObjectMapper()
@Transactional
def setup() {
@@ -49,12 +50,12 @@ abstract class BaseSpec extends Specification {
bindResource(emf, new EntityManagerHolder(emf.createEntityManager()))
}
for (Person p : people.findAll()) {
people.delete(p)
}
for (Address a : addresses.findAll()) {
addresses.delete(a)
}
for (Person p : people.findAll()) {
people.delete(p)
}
}
def readJson(ResponseEntity entity) {
@@ -93,15 +94,19 @@ abstract class BaseSpec extends Specification {
}
def newPerson() {
people.save(new Person(name: "John Doe", addresses: [newAddress("Univille")]))
def p = people.save(new Person(name: "John Doe"))
def a = newAddress("Univille", p)
p.addresses = [a]
people.save(p)
}
def newAddress(city) {
def newAddress(city, person) {
addresses.save(new Address(
["1234 W. 1st St."] as String[],
city,
"ST",
"12345"
"12345",
person
))
}

View File

@@ -2,20 +2,28 @@ package org.springframework.data.rest.webmvc.spec
import org.springframework.http.HttpStatus
import org.springframework.transaction.annotation.Transactional
import spock.lang.Shared
/**
* @author Jon Brisbin
*/
class RelationshipsSpec extends BaseSpec {
@Shared
Long persId
@Shared
Long addrId
def setup() {
def person = newPerson()
persId = person.id
addrId = person.addresses[0].id
}
@Transactional
def "saves entity relationship"() {
given:
def person = newPerson()
def persId = person.id
def addr = newAddress("Smallville")
def addrId = addr.id
def request = createUriListRequest(
"POST",
"people/$persId/addresses",
@@ -35,7 +43,19 @@ class RelationshipsSpec extends BaseSpec {
then:
response.statusCode == HttpStatus.OK
readJson(response).city == "Smallville"
readJson(response).city == "Univille"
}
@Transactional
def "cannot delete a required relationship"() {
when:
def request = createRequest("DELETE", "address/$addrId/person/$persId", null)
def response = controller.deleteLink(request, "address", "$addrId", "person", "$persId")
then:
response.statusCode == HttpStatus.METHOD_NOT_ALLOWED
}

View File

@@ -47,7 +47,7 @@ class TopLevelEntitySpec extends BaseSpec {
person.name = "Johnnie Doe"
person = people.save(person)
def persId = person.id
def request = createJsonRequest("PUT", "people/$persId", null, person)
def request = createJsonRequest("PUT", "people/$persId", null, ["name": "Johnnie Doe"])
def retrReq = createRequest("GET", "people/$persId", null)
when:

View File

@@ -1,5 +1,6 @@
package org.springframework.data.rest.test.webmvc;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@@ -16,17 +17,18 @@ public class Address {
private String city;
private String province;
private String postalCode;
@ManyToOne
@ManyToOne(optional = false, cascade = CascadeType.REMOVE)
private Person person;
public Address() {
}
public Address(String[] lines, String city, String province, String postalCode) {
public Address(String[] lines, String city, String province, String postalCode, Person person) {
this.lines = lines;
this.city = city;
this.province = province;
this.postalCode = postalCode;
this.person = person;
}
public Long getId() {
@@ -73,4 +75,13 @@ public class Address {
this.person = person;
}
@Override public boolean equals(Object o) {
if(!(o instanceof Address)) {
return false;
}
Address address2 = (Address)o;
return (address2.id == id || (id != null && id.equals(address2.id)));
}
}

View File

@@ -42,6 +42,11 @@ public class Person {
this.profiles = profiles;
}
public Person(String name, Map<String, Profile> profiles) {
this.name = name;
this.profiles = profiles;
}
public Long getId() {
return id;
}

View File

@@ -42,7 +42,6 @@ public class PersonLoader
@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"));
@@ -53,19 +52,32 @@ public class PersonLoader
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"));
Address pers1addr = addressRepository.save(new Address(new String[]{"1234 W. 1st St."},
"Univille",
"ST",
"12345",
p1));
p1.setAddresses(Arrays.asList(pers1addr));
personRepository.save(p1);
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));
Person p2 = personRepository.save(new Person("Jane Doe", pers2profiles));
Address pers2addr = addressRepository.save(new Address(new String[]{"1234 E. 2nd St."},
"Univille",
"ST",
"12345",
p2));
p2.setAddresses(Arrays.asList(pers2addr));
personRepository.save(p2);
}

View File

@@ -4,10 +4,10 @@ curl -v -d '{"name" : "John Doe"}' -H "Content-Type: application/json" http://lo
curl -v -d '{"name" : "Jane Doe"}' -H "Content-Type: application/json" http://localhost:8080/people
curl -v -d 'http://localhost:8080/people/1
http://localhost:8080/people/2' -H "Content-Type: text/uri-list" http://localhost:8080/family/1/members
curl -v -d '{"postalCode":"12345","province":"MO","lines":["1 W 1st St."],"city":"Univille"}' -H "Content-Type: application/json" http://localhost:8080/address
curl -v -d '{"postalCode":"12345","province":"MO","lines":["1 W 1st St."],"city":"Univille","person": {"href":"http://localhost:8080/people/1"}}' -H "Content-Type: application/json" http://localhost:8080/address
curl -v -d "http://localhost:8080/address/1" -H "Content-Type: text/uri-list" http://localhost:8080/people/1/addresses
curl -v -d "http://localhost:8080/people/1" -X PUT -H "Content-Type: text/uri-list" http://localhost:8080/address/1/person
curl -v -d '{"postalCode":"54321","province":"MO","lines":["2 W 1st St."],"city":"Univille"}' -H "Content-Type: application/json" http://localhost:8080/address
curl -v -d '{"postalCode":"54321","province":"MO","lines":["2 W 1st St."],"city":"Univille","person": {"href":"http://localhost:8080/people/2"}}' -H "Content-Type: application/json" http://localhost:8080/address
curl -v -d "http://localhost:8080/address/2" -H "Content-Type: text/uri-list" http://localhost:8080/people/2/addresses
curl -v -d '{"type" : "twitter", "url": "#!/johndoe"}' -H "Content-Type: application/json" http://localhost:8080/profile
curl -v -d "http://localhost:8080/profile/1" -H "Content-Type: text/uri-list" http://localhost:8080/people/1/profiles