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

@@ -1,10 +1,12 @@
package org.springframework.data.rest.repository;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Map;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.Repository;
import org.springframework.data.rest.repository.invoke.CrudMethod;
import org.springframework.data.rest.repository.invoke.RepositoryQueryMethod;
/**
@@ -74,4 +76,13 @@ public interface RepositoryMetadata<E extends EntityMetadata<? extends Attribute
*/
Map<String, RepositoryQueryMethod> queryMethods();
/**
* Does this Repository all this method to be exported?
*
* @param method
*
* @return
*/
Boolean exportsMethod(CrudMethod method);
}

View File

@@ -0,0 +1,19 @@
package org.springframework.data.rest.repository.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleAfterLinkDelete {
Class<?>[] value() default {};
}

View File

@@ -0,0 +1,19 @@
package org.springframework.data.rest.repository.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleBeforeLinkDelete {
Class<?>[] value() default {};
}

View File

@@ -42,6 +42,10 @@ public abstract class AbstractRepositoryEventListener<T extends AbstractReposito
onBeforeLinkSave(event.getSource(), ((BeforeLinkSaveEvent)event).getLinked());
} else if(event instanceof AfterLinkSaveEvent) {
onAfterLinkSave(event.getSource(), ((AfterLinkSaveEvent)event).getLinked());
} else if(event instanceof BeforeLinkDeleteEvent) {
onBeforeLinkDelete(event.getSource(), ((BeforeLinkDeleteEvent)event).getLinked());
} else if(event instanceof AfterLinkDeleteEvent) {
onAfterLinkDelete(event.getSource(), ((BeforeLinkDeleteEvent)event).getLinked());
} else if(event instanceof BeforeDeleteEvent) {
onBeforeDelete(event.getSource());
} else if(event instanceof AfterDeleteEvent) {
@@ -83,6 +87,24 @@ public abstract class AbstractRepositoryEventListener<T extends AbstractReposito
protected void onAfterLinkSave(Object parent, Object linked) {
}
/**
* Override this method if you are interested in {@literal beforeLinkDelete} events.
*
* @param parent
* @param linked
*/
protected void onBeforeLinkDelete(Object parent, Object linked) {
}
/**
* Override this method if you are interested in {@literal afterLinkDelete} events.
*
* @param parent
* @param linked
*/
protected void onAfterLinkDelete(Object parent, Object linked) {
}
/**
* Override this method if you are interested in {@literal beforeDelete} events.
*

View File

@@ -0,0 +1,10 @@
package org.springframework.data.rest.repository.context;
/**
* @author Jon Brisbin
*/
public class AfterLinkDeleteEvent extends LinkSaveEvent {
public AfterLinkDeleteEvent(Object source, Object linked) {
super(source, linked);
}
}

View File

@@ -5,8 +5,7 @@ package org.springframework.data.rest.repository.context;
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class BeforeDeleteEvent
extends RepositoryEvent {
public class BeforeDeleteEvent extends RepositoryEvent {
public BeforeDeleteEvent(Object source) {
super(source);
}

View File

@@ -0,0 +1,10 @@
package org.springframework.data.rest.repository.context;
/**
* @author Jon Brisbin
*/
public class BeforeLinkDeleteEvent extends LinkSaveEvent{
public BeforeLinkDeleteEvent(Object source, Object linked) {
super(source, linked);
}
}

View File

@@ -0,0 +1,63 @@
package org.springframework.data.rest.repository.invoke;
import java.lang.reflect.Method;
/**
* @author Jon Brisbin
*/
public enum CrudMethod {
COUNT,
DELETE_ALL,
DELETE_ONE,
DELETE_SOME,
FIND_ALL,
FIND_ONE,
FIND_SOME,
SAVE_ONE,
SAVE_SOME;
public static CrudMethod fromMethod(Method m) {
String s = m.getName();
Class<?>[] paramTypes = m.getParameterTypes();
boolean some = (paramTypes.length > 0 && Iterable.class.isAssignableFrom(paramTypes[0]));
if("count".equals(s)) {
return COUNT;
} else if("delete".equals(s)) {
return (some ? DELETE_SOME : DELETE_ONE);
} else if("deleteAll".equals(s)) {
return DELETE_ALL;
} else if("findAll".equals(s)) {
return (some ? FIND_SOME : FIND_ALL);
} else if("findOne".equals(s)) {
return FIND_ONE;
} else if("save".equals(s)) {
return (some ? SAVE_SOME : SAVE_ONE);
} else {
return null;
}
}
public String toMethodName() {
switch(this) {
case COUNT:
return "count";
case DELETE_ALL:
return "deleteAll";
case DELETE_ONE:
case DELETE_SOME:
return "delete";
case FIND_ALL:
case FIND_SOME:
return "findAll";
case FIND_ONE:
return "findOne";
case SAVE_ONE:
case SAVE_SOME:
return "save";
default:
return null;
}
}
}

View File

@@ -3,70 +3,16 @@ package org.springframework.data.rest.repository.invoke;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.query.Param;
import org.springframework.util.ReflectionUtils;
import org.springframework.data.rest.repository.support.Methods;
/**
* @author Jon Brisbin
*/
public class RepositoryMethod {
public enum Type {
COUNT,
CUSTOM,
DELETE,
FIND_ALL,
FIND_ONE,
SAVE;
public static Type fromMethodName(String s) {
if("count".equals(s)) {
return COUNT;
} else if("delete".equals(s)) {
return DELETE;
} else if("findAll".equals(s)) {
return FIND_ALL;
} else if("findOne".equals(s)) {
return FIND_ONE;
} else if("save".equals(s)) {
return SAVE;
} else {
return CUSTOM;
}
}
public String toMethodName() {
switch(this) {
case COUNT:
return "count";
case DELETE:
return "delete";
case FIND_ALL:
return "findAll";
case FIND_ONE:
return "findOne";
case SAVE:
return "save";
default:
return null;
}
}
}
public static final ReflectionUtils.MethodFilter USER_METHODS = new ReflectionUtils.MethodFilter() {
@Override public boolean matches(Method method) {
return (!method.isSynthetic()
&& !method.isBridge()
&& method.getDeclaringClass() != Object.class
&& !method.getName().contains("$"));
}
};
public static final LocalVariableTableParameterNameDiscoverer NAME_DISCOVERER = new LocalVariableTableParameterNameDiscoverer();
private Method method;
private Class<?>[] paramTypes;
private String[] paramNames;
@@ -84,7 +30,7 @@ public class RepositoryMethod {
sortable = true;
}
}
paramNames = NAME_DISCOVERER.getParameterNames(method);
paramNames = Methods.NAME_DISCOVERER.getParameterNames(method);
if(null == paramNames) {
paramNames = new String[paramTypes.length];
}

View File

@@ -17,8 +17,7 @@ import org.springframework.util.ReflectionUtils;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class JpaAttributeMetadata
implements AttributeMetadata {
public class JpaAttributeMetadata implements AttributeMetadata {
private String name;
private Attribute attribute;

View File

@@ -12,6 +12,7 @@ import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.repository.EntityMetadata;
import org.springframework.data.rest.repository.annotation.RestResource;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
@@ -36,22 +37,28 @@ public class JpaEntityMetadata
for(Attribute attr : entityType.getAttributes()) {
boolean exported = true;
Field field = ReflectionUtils.findField(type, attr.getJavaMember().getName());
if(null != field) {
RestResource fieldResourceAnno = field.getAnnotation(RestResource.class);
if(null != fieldResourceAnno) {
exported = fieldResourceAnno.exported();
}
if(null == field) {
continue;
}
RestResource fieldResourceAnno = field.getAnnotation(RestResource.class);
if(null != fieldResourceAnno) {
exported = fieldResourceAnno.exported();
}
if(exported) {
String name = attr.getName();
if(null != fieldResourceAnno && StringUtils.hasText(fieldResourceAnno.path())) {
name = fieldResourceAnno.path();
}
Class<?> attrType = (attr instanceof PluralAttribute
? ((PluralAttribute)attr).getElementType().getJavaType()
: attr.getJavaType());
if(repositories.hasRepositoryFor(attrType)) {
linkedAttributes.put(attr.getName(), new JpaAttributeMetadata(entityType, attr));
linkedAttributes.put(name, new JpaAttributeMetadata(entityType, attr));
} else {
if(!(attr instanceof SingularAttribute && ((SingularAttribute)attr).isId())
&& !(attr instanceof SingularAttribute && ((SingularAttribute)attr).isVersion())) {
embeddedAttributes.put(attr.getName(), new JpaAttributeMetadata(entityType, attr));
embeddedAttributes.put(name, new JpaAttributeMetadata(entityType, attr));
}
}
}

View File

@@ -13,21 +13,23 @@ import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.data.rest.repository.annotation.RestResource;
import org.springframework.data.rest.repository.invoke.CrudMethod;
import org.springframework.data.rest.repository.invoke.RepositoryQueryMethod;
import org.springframework.data.rest.repository.support.Methods;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class JpaRepositoryMetadata
implements RepositoryMetadata<JpaEntityMetadata> {
public class JpaRepositoryMetadata implements RepositoryMetadata<JpaEntityMetadata> {
private final String name;
private final Class<?> repoClass;
private final CrudRepository<Object, Serializable> repository;
private final EntityInformation entityInfo;
private final Map<String, RepositoryQueryMethod> queryMethods = new HashMap<String, RepositoryQueryMethod>();
private final Map<CrudMethod, Boolean> crudMethodExposed = new HashMap<CrudMethod, Boolean>();
private final Map<String, RepositoryQueryMethod> queryMethods = new HashMap<String, RepositoryQueryMethod>();
private String rel;
private JpaEntityMetadata entityMetadata;
@@ -66,6 +68,24 @@ public class JpaRepositoryMetadata
}
}
ReflectionUtils.doWithMethods(
repoClass,
new ReflectionUtils.MethodCallback() {
@Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
CrudMethod cr = CrudMethod.fromMethod(method);
RestResource rr = method.getAnnotation(RestResource.class);
if(null != rr) {
crudMethodExposed.put(cr, rr.exported());
}
}
},
new ReflectionUtils.MethodFilter() {
@Override public boolean matches(Method method) {
return (null != CrudMethod.fromMethod(method) && Methods.USER_METHODS.matches(method));
}
}
);
Metamodel metamodel = entityManager.getMetamodel();
entityMetadata = new JpaEntityMetadata(repositories, metamodel.entity(entityInfo.getJavaType()));
}
@@ -102,6 +122,15 @@ public class JpaRepositoryMetadata
return Collections.unmodifiableMap(queryMethods);
}
@Override public Boolean exportsMethod(CrudMethod method) {
Boolean b = crudMethodExposed.get(method);
if(null == b) {
return true;
} else {
return b;
}
}
@Override public String toString() {
return "JpaRepositoryMetadata{" +
"name='" + name + '\'' +

View File

@@ -0,0 +1,28 @@
package org.springframework.data.rest.repository.support;
import java.lang.reflect.Method;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.util.ReflectionUtils;
/**
* @author Jon Brisbin
*/
public abstract class Methods {
private Methods() {
}
public static final ReflectionUtils.MethodFilter USER_METHODS =
new ReflectionUtils.MethodFilter() {
@Override public boolean matches(Method method) {
return (!method.isSynthetic()
&& !method.isBridge()
&& method.getDeclaringClass() != Object.class
&& !method.getName().contains("$"));
}
};
public static final LocalVariableTableParameterNameDiscoverer NAME_DISCOVERER =
new LocalVariableTableParameterNameDiscoverer();
}

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);
}