Added before/after link delete events and fixed a bug with deleting links where the updated entity was never saved back to the DB.

This commit is contained in:
Jon Brisbin
2012-07-30 13:53:13 -05:00
parent 1cb9307b1c
commit 2f6650c846
22 changed files with 497 additions and 85 deletions

View File

@@ -22,6 +22,7 @@ public class RepositoryRestConfiguration {
private String jsonpOnErrParamName = null;
private List<HttpMessageConverter<?>> customConverters = Collections.emptyList();
private MediaType defaultMediaType = MediaType.APPLICATION_JSON;
private boolean dumpErrors = false;
public int getDefaultPageSize() {
return defaultPageSize;
@@ -100,4 +101,13 @@ public class RepositoryRestConfiguration {
return this;
}
public boolean isDumpErrors() {
return dumpErrors;
}
public RepositoryRestConfiguration setDumpErrors(boolean dumpErrors) {
this.dumpErrors = dumpErrors;
return this;
}
}

View File

@@ -31,6 +31,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.convert.ConversionService;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -53,12 +54,15 @@ import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.data.rest.repository.RepositoryNotFoundException;
import org.springframework.data.rest.repository.annotation.RestResource;
import org.springframework.data.rest.repository.context.AfterDeleteEvent;
import org.springframework.data.rest.repository.context.AfterLinkDeleteEvent;
import org.springframework.data.rest.repository.context.AfterLinkSaveEvent;
import org.springframework.data.rest.repository.context.AfterSaveEvent;
import org.springframework.data.rest.repository.context.BeforeDeleteEvent;
import org.springframework.data.rest.repository.context.BeforeLinkDeleteEvent;
import org.springframework.data.rest.repository.context.BeforeLinkSaveEvent;
import org.springframework.data.rest.repository.context.BeforeSaveEvent;
import org.springframework.data.rest.repository.context.RepositoryEvent;
import org.springframework.data.rest.repository.invoke.CrudMethod;
import org.springframework.data.rest.repository.invoke.RepositoryMethodResponse;
import org.springframework.data.rest.repository.invoke.RepositoryQueryMethod;
import org.springframework.format.support.DefaultFormattingConversionService;
@@ -348,6 +352,10 @@ public class RepositoryRestController
URI baseUri = uriBuilder.build().toUri();
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
if(!repoMeta.exportsMethod(CrudMethod.FIND_ALL)) {
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
}
RepositoryMethodResponse response = new RepositoryMethodResponse();
Iterator allEntities = Collections.emptyList().iterator();
@@ -515,8 +523,18 @@ public class RepositoryRestController
String[] paramNames = queryMethod.paramNames();
Object[] paramVals = new Object[paramTypes.length];
for(int i = 0; i < paramVals.length; i++) {
String queryVal = request.getServletRequest().getParameter(paramNames[i]);
if(null == queryVal) {
if(Pageable.class.isAssignableFrom(paramTypes[i])) {
// Handle paging
paramVals[i] = pageSort;
continue;
} else if(Sort.class.isAssignableFrom(paramTypes[i])) {
// Handle sorting
paramVals[i] = (null != pageSort ? pageSort.getSort() : null);
continue;
}
String queryVal;
if(null == (queryVal = request.getServletRequest().getParameter(paramNames[i]))) {
continue;
}
@@ -524,12 +542,6 @@ public class RepositoryRestController
if(String.class.isAssignableFrom(paramTypes[i])) {
// Param type is a String
paramVals[i] = queryVal;
} else if(Pageable.class.isAssignableFrom(paramTypes[i])) {
// Handle paging
paramVals[i] = pageSort;
} else if(Sort.class.isAssignableFrom(paramTypes[i])) {
// Handle sorting
paramVals[i] = (null != pageSort ? pageSort.getSort() : null);
} else if(null != (paramRepoMeta = repositoryMetadataFor(paramTypes[i]))) {
// Complex parameter is a managed type
Serializable id = stringToSerializable(queryVal,
@@ -667,6 +679,9 @@ public class RepositoryRestController
URI baseUri = uriBuilder.build().toUri();
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
if(!repoMeta.exportsMethod(CrudMethod.SAVE_ONE)) {
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
}
CrudRepository repo = repoMeta.repository();
MediaType incomingMediaType = request.getHeaders().getContentType();
@@ -726,6 +741,9 @@ public class RepositoryRestController
URI baseUri = uriBuilder.build().toUri();
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
if(!repoMeta.exportsMethod(CrudMethod.FIND_ONE)) {
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
}
Serializable serId = stringToSerializable(id,
(Class<? extends Serializable>)repoMeta.entityMetadata()
.idAttribute()
@@ -790,6 +808,9 @@ public class RepositoryRestController
URI baseUri = uriBuilder.build().toUri();
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
if(!repoMeta.exportsMethod(CrudMethod.SAVE_ONE) || !repoMeta.exportsMethod(CrudMethod.FIND_ONE)) {
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
}
Serializable serId = stringToSerializable(id,
(Class<? extends Serializable>)repoMeta.entityMetadata()
.idAttribute()
@@ -865,6 +886,9 @@ public class RepositoryRestController
@PathVariable String repository,
@PathVariable String id) throws IOException {
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
if(!repoMeta.exportsMethod(CrudMethod.DELETE_ONE)) {
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
}
Serializable serId = stringToSerializable(id,
(Class<? extends Serializable>)repoMeta.entityMetadata()
.idAttribute()
@@ -909,6 +933,9 @@ public class RepositoryRestController
URI baseUri = uriBuilder.build().toUri();
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
if(!repoMeta.exportsMethod(CrudMethod.FIND_ONE)) {
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
}
Serializable serId = stringToSerializable(id,
(Class<? extends Serializable>)repoMeta.entityMetadata()
.idAttribute()
@@ -931,6 +958,10 @@ public class RepositoryRestController
}
RepositoryMetadata propRepoMeta = repositoryMetadataFor(attrType);
if(!propRepoMeta.exportsMethod(CrudMethod.FIND_ONE)) {
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
}
Object propVal;
if(null == (propVal = attrMeta.get(entity))) {
@@ -1001,6 +1032,9 @@ public class RepositoryRestController
URI baseUri = uriBuilder.build().toUri();
final RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
if(!repoMeta.exportsMethod(CrudMethod.SAVE_ONE)) {
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
}
Serializable serId = stringToSerializable(id,
(Class<? extends Serializable>)repoMeta.entityMetadata()
.idAttribute()
@@ -1109,6 +1143,9 @@ public class RepositoryRestController
@PathVariable String id,
@PathVariable String property) throws IOException {
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
if(!repoMeta.exportsMethod(CrudMethod.SAVE_ONE)) {
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
}
CrudRepository repo = repoMeta.repository();
Serializable serId = stringToSerializable(id,
(Class<? extends Serializable>)repoMeta.entityMetadata()
@@ -1162,6 +1199,9 @@ public class RepositoryRestController
URI baseUri = uriBuilder.build().toUri();
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
if(!repoMeta.exportsMethod(CrudMethod.FIND_ONE)) {
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
}
AttributeMetadata attrMeta;
if(null == (attrMeta = repoMeta.entityMetadata().attribute(property))) {
@@ -1226,6 +1266,9 @@ public class RepositoryRestController
@PathVariable String linkedId) throws IOException {
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
CrudRepository repo = repoMeta.repository();
if(!repoMeta.exportsMethod(CrudMethod.FIND_ONE) || !repoMeta.exportsMethod(CrudMethod.SAVE_ONE)) {
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
}
Serializable serId = stringToSerializable(id,
(Class<? extends Serializable>)repoMeta.entityMetadata()
.idAttribute()
@@ -1283,6 +1326,10 @@ public class RepositoryRestController
attrMeta.set(linkedEntity, entity);
}
publishEvent(new BeforeLinkDeleteEvent(entity, linkedEntity));
Object savedEntity = repo.save(entity);
publishEvent(new AfterLinkDeleteEvent(savedEntity, linkedEntity));
return negotiateResponse(request, HttpStatus.NO_CONTENT, new HttpHeaders(), null);
}
@@ -1306,11 +1353,67 @@ public class RepositoryRestController
return notFoundResponse(request);
}
/**
* Handle NPEs as a regular 500 error.
*
* @param e
* @param request
*
* @return
*
* @throws IOException
*/
@ExceptionHandler(NullPointerException.class)
@ResponseBody
public ResponseEntity handleNPE(NullPointerException e,
ServletServerHttpRequest request) throws IOException {
if(LOG.isErrorEnabled()) {
LOG.error(e.getMessage(), e);
}
return negotiateResponse(request, HttpStatus.INTERNAL_SERVER_ERROR, new HttpHeaders(), null);
}
/**
* Handle {@link InvocationTargetException}s as a 400 Bad Request because these are likely to occur if, e.g. the user
* does not provide a value for a query parameter.
*
* @param e
* @param request
*
* @return
*
* @throws IOException
*/
@ExceptionHandler(InvocationTargetException.class)
@ResponseBody
public ResponseEntity handleInvocationTargetException(InvocationTargetException e,
ServletServerHttpRequest request) throws IOException {
if(LOG.isErrorEnabled()) {
LOG.error(e.getMessage(), e);
}
for(Throwable cause = e.getCause(); (null != cause && cause != e.getCause()); cause = cause.getCause()) {
if(cause instanceof InvalidDataAccessApiUsageException || cause instanceof IllegalArgumentException) {
return negotiateResponse(request, HttpStatus.BAD_REQUEST, new HttpHeaders(), null);
}
}
return negotiateResponse(request, HttpStatus.INTERNAL_SERVER_ERROR, new HttpHeaders(), e);
}
/**
* Handle failures commonly thrown from code tries to read incoming data and convert or cast it to the right type.
*
* @param t
* @param request
*
* @return
*
* @throws IOException
*/
@ExceptionHandler(
{
NullPointerException.class,
IllegalArgumentException.class,
IllegalStateException.class,
ClassCastException.class
}
)
@@ -1320,7 +1423,7 @@ public class RepositoryRestController
if(LOG.isErrorEnabled()) {
LOG.error(t.getMessage(), t);
}
return negotiateResponse(request, HttpStatus.BAD_REQUEST, new HttpHeaders(), null);
return negotiateResponse(request, HttpStatus.BAD_REQUEST, new HttpHeaders(), t);
}
/**

View File

@@ -7,6 +7,7 @@ import org.springframework.context.annotation.ImportResource;
import org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener;
import org.springframework.data.rest.repository.jpa.JpaRepositoryExporter;
import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor;
import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>

View File

@@ -0,0 +1,48 @@
package org.springframework.data.rest.webmvc;
import java.io.IOException;
import java.io.PrintWriter;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
/**
* @author Jon Brisbin
*/
public class ThrowableHttpMessageConverter extends AbstractHttpMessageConverter<Throwable> {
private final ObjectMapper mapper = new ObjectMapper();
@Override protected boolean supports(Class<?> clazz) {
throw new IllegalStateException("supports(Class<?> clazz) not used in " + getClass().getName());
}
@Override public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}
@Override public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return (Throwable.class.isAssignableFrom(clazz)
&& (mediaType.getSubtype().contains("json") || mediaType.getSubtype().contains("text")));
}
@Override protected Throwable readInternal(Class<? extends Throwable> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
throw new HttpMessageNotReadableException("Cannot read Throwables from input.");
}
@Override protected void writeInternal(Throwable throwable, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
if(outputMessage.getHeaders().getContentType().getSubtype().contains("json")) {
outputMessage.getBody().write(mapper.writeValueAsBytes(throwable));
} else {
throwable.printStackTrace(new PrintWriter(outputMessage.getBody()));
}
}
}

View File

@@ -76,7 +76,7 @@ abstract class BaseSpec extends Specification {
method: method
)
if (query) {
query.collect {k, v -> req.addParameter(k, v)}
query.collect { String k, String v -> req.addParameter(k, v)}
}
if (contentType) {
req.contentType = contentType

View File

@@ -0,0 +1,65 @@
package org.springframework.data.rest.webmvc.spec
import org.springframework.data.domain.PageRequest
import org.springframework.data.rest.test.webmvc.Person
import org.springframework.data.rest.webmvc.PagingAndSorting
import org.springframework.data.rest.webmvc.RepositoryRestConfiguration
import org.springframework.http.HttpStatus
import spock.lang.Shared
import java.lang.reflect.InvocationTargetException
/**
* @author Jon Brisbin
*/
class QueryMethodsSpec extends BaseSpec {
@Shared
def pageSort = new PagingAndSorting(RepositoryRestConfiguration.DEFAULT, new PageRequest(0, 10))
def "exposes query method links to discovery"() {
given:
def request = createRequest("GET", "people/search", null)
when:
def response = controller.listQueryMethods(request, baseUri, "people")
def body = readJson(response)
then:
response.statusCode == HttpStatus.OK
body["_links"].size() == 2
}
def "invokes query methods"() {
given:
people.save(new Person(name: "John Doe"))
people.save(new Person(name: "Bill Doe"))
def request = createRequest("GET", "people/search/nameStartsWith", ["name": "John"])
when:
def response = controller.query(request, pageSort, baseUri, "people", "nameStartsWith")
def body = readJson(response)
then:
response.statusCode == HttpStatus.OK
body["results"].size() == 1
}
def "blows up on empty query parameters"() {
given:
def request = createRequest("GET", "people/search/nameStartsWith", null)
when:
controller.query(request, pageSort, baseUri, "people", "nameStartsWith")
then:
thrown(InvocationTargetException)
}
}

View File

@@ -66,4 +66,19 @@ class TopLevelEntitySpec extends BaseSpec {
}
def "won't delete entities whose delete methods are not exported"() {
given:
def person = people.save(new Person(name: "John Doe"))
def persId = person.id
def request = createRequest("DELETE", "people/$persId", null)
when:
def response = controller.deleteEntity(request, "people", "$persId")
then:
response.statusCode == HttpStatus.METHOD_NOT_ALLOWED
}
}

View File

@@ -6,8 +6,10 @@ import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.rest.webmvc.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaDialect;
@@ -23,6 +25,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
* @author Jon Brisbin
*/
@Configuration
@Import(RepositoryRestMvcConfiguration.class)
@ComponentScan(basePackageClasses = ApplicationConfig.class)
@EnableJpaRepositories
@EnableTransactionManagement

View File

@@ -14,10 +14,15 @@ import org.springframework.data.rest.repository.annotation.RestResource;
@RestResource(path = "people", rel = "peeps")
public interface PersonRepository extends PagingAndSortingRepository<Person, Long> {
@RestResource(path = "name", rel = "names")
public List<Person> findByName(@Param("name") String name);
@Override
@RestResource(exported = false) void delete(Long id);
@Override
@RestResource(exported = false) void delete(Person entity);
@RestResource(path = "name", rel = "names") List<Person> findByName(@Param("name") String name);
@RestResource(path = "nameStartsWith", rel = "nameStartsWith")
public Page findByNameStartsWith(@Param("name") String name, Pageable p);
Page findByNameStartsWith(@Param("name") String name, Pageable p);
}