Added ApplicationEvent handling and annotation-based extension mechanism.

This commit is contained in:
Jon Brisbin
2012-04-26 11:40:14 -05:00
parent 57caa05823
commit fe9e485d81
31 changed files with 987 additions and 41 deletions

View File

@@ -35,7 +35,7 @@ configure(subprojects) { subproject ->
javadoc {
options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED
options.author = true
options.header = project.name
options.header = subproject.name
//options.overview = "${projectDir}/src/main/java/overview.html"
}
@@ -90,7 +90,7 @@ idea {
inheritOutputDirs = false
outputDir = file("build/classes/main")
testOutputDir = file("build/classes/test")
downloadJavadoc = true
downloadJavadoc = false
downloadSources = true
}
project {

View File

@@ -1,6 +1,6 @@
dependencies {
// Google Guava
compile "com.google.guava:guava:11.0.1"
compile "com.google.guava:guava:11.0.2"
}

View File

@@ -0,0 +1,43 @@
package org.springframework.data.rest.core.util;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class RestHelper<T> {
public HttpStatus status;
public HttpHeaders headers = new HttpHeaders();
public T body;
private RestHelper(T body) {
this.body = body;
}
public static <T> RestHelper<T> resource(T body) {
return new RestHelper<T>(body);
}
public RestHelper<T> header(String key, String value) {
headers.add(key, value);
return this;
}
public RestHelper<T> status(HttpStatus status) {
this.status = status;
return this;
}
public HttpEntity<T> asHttpEntity() {
return new HttpEntity<T>(body, headers);
}
public ResponseEntity<T> asResponseEntity() {
return new ResponseEntity<T>(body, headers, status);
}
}

View File

@@ -4,7 +4,6 @@ import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -22,12 +21,13 @@ import org.springframework.context.ApplicationContextAware;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class JpaRepositoryMetadata implements InitializingBean, ApplicationContextAware {
public class JpaRepositoryMetadata
implements InitializingBean,
ApplicationContextAware {
private ApplicationContext applicationContext;
private Map<Class<?>, RepositoryCacheEntry> repositories = new HashMap<Class<?>, RepositoryCacheEntry>();
@@ -125,8 +125,10 @@ public class JpaRepositoryMetadata implements InitializingBean, ApplicationConte
return names;
}
public void setRepositories(Collection<? extends CrudRepository> repositories) {
for (CrudRepository repository : repositories) {
public void setRepositories(Map<String, CrudRepository> repositories) {
for (Map.Entry<String, CrudRepository> entry : repositories.entrySet()) {
String name = entry.getKey();
CrudRepository repository = entry.getValue();
Class<?> repoClass = AopUtils.getTargetClass(repository);
Field infoField = ReflectionUtils.findField(repoClass, "entityInformation");
ReflectionUtils.makeAccessible(infoField);
@@ -136,7 +138,10 @@ public class JpaRepositoryMetadata implements InitializingBean, ApplicationConte
SingletonTargetSource targetRepo = (SingletonTargetSource) m.invoke(repository);
EntityInformation entityInfo = (EntityInformation) infoField.get(targetRepo.getTarget());
Class<?>[] intfs = repository.getClass().getInterfaces();
String name = StringUtils.uncapitalize(intfs[0].getSimpleName().replaceAll("Repository", ""));
//String name = StringUtils.uncapitalize(intfs[0].getSimpleName().replaceAll("Repository", ""));
if (name.contains("Repository")) {
name = name.replaceAll("Repository", "");
}
this.repositories.put(entityInfo.getJavaType(), new RepositoryCacheEntry(name, repository, entityInfo, null));
} catch (Throwable t) {
throw new IllegalStateException(t);
@@ -149,7 +154,7 @@ public class JpaRepositoryMetadata implements InitializingBean, ApplicationConte
ApplicationContext appCtx = applicationContext;
while (null != appCtx) {
Map<String, CrudRepository> beans = appCtx.getBeansOfType(CrudRepository.class);
setRepositories(beans.values());
setRepositories(beans);
appCtx = appCtx.getParent();
}
}

View File

@@ -0,0 +1,22 @@
package org.springframework.data.rest.repository;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.validation.Errors;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class RepositoryConstraintViolationException extends DataIntegrityViolationException {
private Errors errors;
public RepositoryConstraintViolationException(Errors errors) {
super("Validation failed");
this.errors = errors;
}
public Errors getErrors() {
return errors;
}
}

View File

@@ -0,0 +1,61 @@
package org.springframework.data.rest.repository;
import java.util.ArrayList;
import java.util.List;
import org.springframework.validation.AbstractErrors;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class ValidationErrors extends AbstractErrors {
private String name;
private Object entity;
private JpaEntityMetadata entityMetadata;
private List<ObjectError> globalErrors = new ArrayList<ObjectError>();
private List<FieldError> fieldErrors = new ArrayList<FieldError>();
public ValidationErrors(String name, Object entity, JpaEntityMetadata entityMetadata) {
this.name = name;
this.entity = entity;
this.entityMetadata = entityMetadata;
}
@Override public String getObjectName() {
return name;
}
@Override public void reject(String errorCode, Object[] errorArgs, String defaultMessage) {
globalErrors.add(new ObjectError(name, new String[]{errorCode}, errorArgs, defaultMessage));
}
@Override public void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage) {
fieldErrors.add(new FieldError(name,
field,
getFieldValue(field),
true,
new String[]{errorCode},
errorArgs,
defaultMessage));
}
@Override public void addAllErrors(Errors errors) {
globalErrors.addAll(errors.getAllErrors());
}
@Override public List<ObjectError> getGlobalErrors() {
return globalErrors;
}
@Override public List<FieldError> getFieldErrors() {
return fieldErrors;
}
@Override public Object getFieldValue(String field) {
return entityMetadata.get(field, entity);
}
}

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 <jon@jbrisbin.com>
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleAfterChildSave {
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 <jon@jbrisbin.com>
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleAfterDelete {
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 <jon@jbrisbin.com>
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleAfterSave {
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 <jon@jbrisbin.com>
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleBeforeChildSave {
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 <jon@jbrisbin.com>
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleBeforeDelete {
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 <jon@jbrisbin.com>
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleBeforeSave {
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 <jon@jbrisbin.com>
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RepositoryEventHandler {
Class<?>[] value() default {};
}

View File

@@ -0,0 +1,71 @@
package org.springframework.data.rest.repository.context;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.data.rest.repository.JpaRepositoryMetadata;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public abstract class AbstractRepositoryEventListener<T extends AbstractRepositoryEventListener<? super T>>
implements ApplicationListener<RepositoryEvent>,
ApplicationContextAware {
@Autowired
protected JpaRepositoryMetadata repositoryMetadata;
protected ApplicationContext applicationContext;
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public JpaRepositoryMetadata getRepositoryMetadata() {
return repositoryMetadata;
}
public void setRepositoryMetadata(JpaRepositoryMetadata repositoryMetadata) {
this.repositoryMetadata = repositoryMetadata;
}
public JpaRepositoryMetadata repositoryMetadata() {
return repositoryMetadata;
}
@SuppressWarnings({"unchecked"})
public T repositoryMetadata(JpaRepositoryMetadata repositoryMetadata) {
this.repositoryMetadata = repositoryMetadata;
return (T) this;
}
@Override public final void onApplicationEvent(RepositoryEvent event) {
if (event instanceof BeforeSaveEvent) {
onBeforeSave(event.getSource());
} else if (event instanceof AfterSaveEvent) {
onAfterSave(event.getSource());
} else if (event instanceof BeforeChildSaveEvent) {
onBeforeChildSave(event.getSource(), ((BeforeChildSaveEvent) event).getChild());
} else if (event instanceof AfterChildSaveEvent) {
onAfterChildSave(event.getSource(), ((AfterChildSaveEvent) event).getChild());
} else if (event instanceof BeforeDeleteEvent) {
onBeforeDelete(event.getSource());
} else if (event instanceof AfterDeleteEvent) {
onAfterDelete(event.getSource());
}
}
protected void onBeforeSave(Object entity) {}
protected void onAfterSave(Object entity) {}
protected void onBeforeChildSave(Object parent, Object child) {}
protected void onAfterChildSave(Object parent, Object child) {}
protected void onBeforeDelete(Object entity) {}
protected void onAfterDelete(Object entity) {}
}

View File

@@ -0,0 +1,10 @@
package org.springframework.data.rest.repository.context;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class AfterChildSaveEvent extends ChildSaveEvent {
public AfterChildSaveEvent(Object source, Object child) {
super(source, child);
}
}

View File

@@ -0,0 +1,10 @@
package org.springframework.data.rest.repository.context;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class AfterDeleteEvent extends RepositoryEvent {
public AfterDeleteEvent(Object source) {
super(source);
}
}

View File

@@ -0,0 +1,12 @@
package org.springframework.data.rest.repository.context;
import org.springframework.context.ApplicationEvent;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class AfterSaveEvent extends RepositoryEvent {
public AfterSaveEvent(Object source) {
super(source);
}
}

View File

@@ -0,0 +1,167 @@
package org.springframework.data.rest.repository.context;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.rest.repository.annotation.HandleAfterChildSave;
import org.springframework.data.rest.repository.annotation.HandleAfterDelete;
import org.springframework.data.rest.repository.annotation.HandleAfterSave;
import org.springframework.data.rest.repository.annotation.HandleBeforeChildSave;
import org.springframework.data.rest.repository.annotation.HandleBeforeDelete;
import org.springframework.data.rest.repository.annotation.HandleBeforeSave;
import org.springframework.data.rest.repository.annotation.RepositoryEventHandler;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class AnnotatedHandlerRepositoryEventListener
implements ApplicationListener<RepositoryEvent>,
ApplicationContextAware,
InitializingBean {
private String basePackage;
private ApplicationContext applicationContext;
private Multimap<Class<? extends RepositoryEvent>, EventHandlerMethod> handlerMethods = ArrayListMultimap.create();
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public String getBasePackage() {
return basePackage;
}
public AnnotatedHandlerRepositoryEventListener setBasePackage(String basePackage) {
this.basePackage = basePackage;
return this;
}
public String basePackage() {
return basePackage;
}
public AnnotatedHandlerRepositoryEventListener basePackage(String basePackage) {
this.basePackage = basePackage;
return this;
}
@Override public void afterPropertiesSet() throws Exception {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(RepositoryEventHandler.class, true, true));
for (BeanDefinition beanDef : scanner.findCandidateComponents(basePackage)) {
String typeName = beanDef.getBeanClassName();
Class<?> handlerType = ClassUtils.forName(typeName, ClassUtils.getDefaultClassLoader());
RepositoryEventHandler typeAnno = AnnotationUtils.findAnnotation(handlerType, RepositoryEventHandler.class);
Class<?>[] targetTypes = typeAnno.value();
if (targetTypes.length == 0) {
targetTypes = new Class<?>[]{null};
}
for (final Class<?> targetType : targetTypes) {
for (final Object handler : applicationContext.getBeansOfType(handlerType).values()) {
ReflectionUtils.doWithMethods(
handler.getClass(),
new ReflectionUtils.MethodCallback() {
@Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
inspect(targetType, handler, method, HandleBeforeSave.class, BeforeSaveEvent.class);
inspect(targetType, handler, method, HandleAfterSave.class, AfterSaveEvent.class);
inspect(targetType, handler, method, HandleBeforeChildSave.class, BeforeChildSaveEvent.class);
inspect(targetType, handler, method, HandleAfterChildSave.class, AfterChildSaveEvent.class);
inspect(targetType, handler, method, HandleBeforeDelete.class, BeforeDeleteEvent.class);
inspect(targetType, handler, method, HandleAfterDelete.class, AfterDeleteEvent.class);
}
},
new ReflectionUtils.MethodFilter() {
@Override public boolean matches(Method method) {
return (!method.isSynthetic()
&& !method.isBridge()
&& method.getDeclaringClass() != Object.class
&& !method.getName().contains("$"));
}
}
);
}
}
}
}
@Override public void onApplicationEvent(RepositoryEvent event) {
Class<? extends RepositoryEvent> eventType = event.getClass();
if (handlerMethods.containsKey(eventType)) {
for (EventHandlerMethod handlerMethod : handlerMethods.get(eventType)) {
try {
Object src = event.getSource();
if (ClassUtils.isAssignable(handlerMethod.targetType, src.getClass())) {
List<Object> params = new ArrayList<Object>();
params.add(src);
if (event instanceof BeforeChildSaveEvent) {
params.add(((BeforeChildSaveEvent) event).getChild());
} else if (event instanceof AfterChildSaveEvent) {
params.add(((AfterChildSaveEvent) event).getChild());
}
handlerMethod.method.invoke(handlerMethod.handler, params.toArray());
}
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
}
}
private <T extends Annotation> void inspect(Class<?> targetType,
Object handler,
Method method,
Class<T> annoType,
Class<? extends RepositoryEvent> eventType) {
T anno = AnnotationUtils.findAnnotation(method, annoType);
if (null != anno) {
try {
Class<?>[] targetTypes;
if (null == targetType) {
targetTypes = (Class<?>[]) anno.getClass().getMethod("value", new Class[0]).invoke(anno);
} else {
targetTypes = new Class<?>[]{targetType};
}
for (Class<?> type : targetTypes) {
handlerMethods.put(eventType,
new EventHandlerMethod(type,
handler,
method));
}
} catch (NoSuchMethodException ignored) {
} catch (InvocationTargetException ignored) {
} catch (IllegalAccessException ignored) {
}
}
}
private class EventHandlerMethod {
final Class<?> targetType;
final Method method;
final Object handler;
private EventHandlerMethod(Class<?> targetType,
Object handler,
Method method) {
this.targetType = targetType;
this.method = method;
this.handler = handler;
}
}
}

View File

@@ -0,0 +1,10 @@
package org.springframework.data.rest.repository.context;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class BeforeChildSaveEvent extends ChildSaveEvent {
public BeforeChildSaveEvent(Object source, Object child) {
super(source, child);
}
}

View File

@@ -0,0 +1,12 @@
package org.springframework.data.rest.repository.context;
import org.springframework.context.ApplicationEvent;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class BeforeDeleteEvent extends RepositoryEvent {
public BeforeDeleteEvent(Object source) {
super(source);
}
}

View File

@@ -0,0 +1,12 @@
package org.springframework.data.rest.repository.context;
import org.springframework.context.ApplicationEvent;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class BeforeSaveEvent extends RepositoryEvent {
public BeforeSaveEvent(Object source) {
super(source);
}
}

View File

@@ -0,0 +1,19 @@
package org.springframework.data.rest.repository.context;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class ChildSaveEvent extends RepositoryEvent{
private final Object child;
public ChildSaveEvent(Object source, Object child) {
super(source);
this.child = child;
}
public Object getChild() {
return child;
}
}

View File

@@ -0,0 +1,12 @@
package org.springframework.data.rest.repository.context;
import org.springframework.context.ApplicationEvent;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public abstract class RepositoryEvent extends ApplicationEvent {
protected RepositoryEvent(Object source) {
super(source);
}
}

View File

@@ -0,0 +1,113 @@
package org.springframework.data.rest.repository.context;
import java.util.Collection;
import java.util.Map;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.rest.repository.RepositoryConstraintViolationException;
import org.springframework.data.rest.repository.ValidationErrors;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class ValidatingRepositoryEventListener
extends AbstractRepositoryEventListener<ValidatingRepositoryEventListener>
implements InitializingBean {
private static final Logger LOG = LoggerFactory.getLogger(ValidatingRepositoryEventListener.class);
private Multimap<String, Validator> validators = ArrayListMultimap.create();
@Override public void afterPropertiesSet() throws Exception {
if (validators.size() == 0) {
Map<String, Validator> validators = applicationContext.getBeansOfType(Validator.class);
for (Map.Entry<String, Validator> entry : validators.entrySet()) {
String name = entry.getKey();
Validator v = entry.getValue();
if (name.contains("Save")) {
name = name.substring(0, name.indexOf("Save") + 4);
} else if (name.contains("Delete")) {
name = name.substring(0, name.indexOf("Delete") + 6);
}
this.validators.put(name, v);
}
}
}
public Map<String, Collection<Validator>> getValidators() {
return validators.asMap();
}
public ValidatingRepositoryEventListener setValidators(Map<String, Collection<Validator>> validators) {
for (Map.Entry<String, Collection<Validator>> entry : validators.entrySet()) {
this.validators.replaceValues(entry.getKey(), entry.getValue());
}
return this;
}
public ValidatingRepositoryEventListener addValidator(String event, Validator validator) {
validators.put(event, validator);
return this;
}
@Override protected void onBeforeSave(Object entity) {
validate("beforeSave", entity);
}
@Override protected void onAfterSave(Object entity) {
LOG.info("onAfterSave: " + entity);
validate("afterSave", entity);
}
@Override protected void onBeforeChildSave(Object parent, Object child) {
LOG.info("onBeforeChildSave: " + parent + "/" + child);
validate("beforeChildSave", parent);
}
@Override protected void onAfterChildSave(Object parent, Object child) {
LOG.info("onAfterChildSave: " + parent + "/" + child);
validate("afterChildSave", parent);
}
@Override protected void onBeforeDelete(Object entity) {
LOG.info("onBeforeDelete: " + entity);
validate("beforeDelete", entity);
}
@Override protected void onAfterDelete(Object entity) {
LOG.info("onAfterDelete: " + entity);
validate("afterDelete", entity);
}
private Errors validate(String event, Object entity) {
Errors errors = null;
if (null != entity) {
errors = new ValidationErrors(entity.getClass().getSimpleName(),
entity,
repositoryMetadata.entityMetadataFor(entity.getClass()));
Collection<Validator> validators = this.validators.get(event);
if (null != validators) {
for (Validator v : validators) {
if (v.supports(entity.getClass())) {
LOG.debug(event + ": " + entity + " with " + v);
ValidationUtils.invokeValidator(v, entity, errors);
}
}
}
if (errors.getErrorCount() > 0) {
throw new RepositoryConstraintViolationException(errors);
}
}
return errors;
}
}

View File

@@ -0,0 +1,92 @@
package org.springframework.data.rest.repository.spec
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.data.rest.repository.annotation.HandleAfterChildSave
import org.springframework.data.rest.repository.annotation.HandleAfterDelete
import org.springframework.data.rest.repository.annotation.HandleAfterSave
import org.springframework.data.rest.repository.annotation.HandleBeforeChildSave
import org.springframework.data.rest.repository.annotation.HandleBeforeDelete
import org.springframework.data.rest.repository.annotation.HandleBeforeSave
import org.springframework.data.rest.repository.annotation.RepositoryEventHandler
import org.springframework.data.rest.repository.context.AfterChildSaveEvent
import org.springframework.data.rest.repository.context.AfterDeleteEvent
import org.springframework.data.rest.repository.context.AfterSaveEvent
import org.springframework.data.rest.repository.context.BeforeChildSaveEvent
import org.springframework.data.rest.repository.context.BeforeDeleteEvent
import org.springframework.data.rest.repository.context.BeforeSaveEvent
import org.springframework.data.rest.repository.test.Person
import org.springframework.test.context.ContextConfiguration
import spock.lang.Specification
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
@ContextConfiguration(locations = ["/ExtensionsSpec-test.xml"])
class ExtensionsSpec extends Specification {
@Autowired
ApplicationContext appCtx
@Autowired
PersonEventHandler handler
def "responds to ApplicationEvents in annotated handlers"() {
given:
def p = new Person("John Doe")
when:
appCtx.publishEvent(new BeforeSaveEvent(p))
appCtx.publishEvent(new AfterSaveEvent(p))
appCtx.publishEvent(new BeforeChildSaveEvent(p, new Object()))
appCtx.publishEvent(new AfterChildSaveEvent(p, new Object()))
appCtx.publishEvent(new BeforeDeleteEvent(p))
appCtx.publishEvent(new AfterDeleteEvent(p))
then:
handler.beforeSave
handler.afterSave
handler.beforeChildSave
handler.afterChildSave
handler.beforeDelete
handler.afterDelete
}
}
@RepositoryEventHandler(Person)
class PersonEventHandler {
def beforeSave = false
def afterSave = false
def beforeChildSave = false
def afterChildSave = false
def beforeDelete = false
def afterDelete = false
@HandleBeforeSave void handleBeforeSave(Person p) {
beforeSave = true
}
@HandleAfterSave void handleAfterSave(Person p) {
afterSave = true
}
@HandleBeforeChildSave void handleBeforeChildSave(Person p, Object child) {
beforeChildSave = true
}
@HandleAfterChildSave void handleAfterChildSave(Person p, Object child) {
afterChildSave = true
}
@HandleBeforeDelete void handleBeforeDelete(Person p) {
beforeDelete = true
}
@HandleAfterDelete void handleAfterDelete(Person p) {
afterDelete = true
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="org.springframework.data.rest.repository.spec.PersonEventHandler"/>
<bean class="org.springframework.data.rest.repository.context.AnnotatedHandlerRepositoryEventListener"
p:basePackage="org.springframework.data.rest.repository.spec"/>
</beans>

View File

@@ -23,6 +23,8 @@ import javax.persistence.metamodel.SingularAttribute;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.dao.OptimisticLockingFailureException;
@@ -34,6 +36,13 @@ import org.springframework.data.rest.core.SimpleLink;
import org.springframework.data.rest.core.util.UriUtils;
import org.springframework.data.rest.repository.JpaEntityMetadata;
import org.springframework.data.rest.repository.JpaRepositoryMetadata;
import org.springframework.data.rest.repository.RepositoryConstraintViolationException;
import org.springframework.data.rest.repository.context.AfterChildSaveEvent;
import org.springframework.data.rest.repository.context.AfterDeleteEvent;
import org.springframework.data.rest.repository.context.AfterSaveEvent;
import org.springframework.data.rest.repository.context.BeforeChildSaveEvent;
import org.springframework.data.rest.repository.context.BeforeDeleteEvent;
import org.springframework.data.rest.repository.context.BeforeSaveEvent;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpMethod;
@@ -44,21 +53,26 @@ import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
import org.springframework.web.util.UriComponentsBuilder;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
@Controller
public class RepositoryRestController implements InitializingBean {
public class RepositoryRestController
implements ApplicationEventPublisherAware,
InitializingBean {
public static final String STATUS = "status";
public static final String HEADERS = "headers";
@@ -67,14 +81,21 @@ public class RepositoryRestController implements InitializingBean {
public static final String SELF = "self";
public static final String LINKS = "_links";
private ApplicationEventPublisher eventPublisher;
private MediaType uriListMediaType = MediaType.parseMediaType("text/uri-list");
private MediaType jsonMediaType = MediaType.parseMediaType("application/x-spring-data+json");
private JpaRepositoryMetadata repositoryMetadata;
private Map<CrudRepository, TypeMetaCacheEntry> typeMetaCache = new ConcurrentHashMap<CrudRepository, TypeMetaCacheEntry>();
private ConversionService conversionService = new DefaultConversionService();
private List<HttpMessageConverter<?>> httpMessageConverters;
private ContentNegotiatingViewResolver viewResolver;
private ObjectMapper objectMapper = new ObjectMapper();
@Override public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public JpaRepositoryMetadata getRepositoryMetadata() {
return repositoryMetadata;
}
@@ -126,6 +147,23 @@ public class RepositoryRestController implements InitializingBean {
return this;
}
public ContentNegotiatingViewResolver getViewResolver() {
return viewResolver;
}
public void setViewResolver(ContentNegotiatingViewResolver viewResolver) {
this.viewResolver = viewResolver;
}
public ContentNegotiatingViewResolver viewResolver() {
return viewResolver;
}
public RepositoryRestController viewResolver(ContentNegotiatingViewResolver viewResolver) {
this.viewResolver = viewResolver;
return this;
}
public MediaType getUriListMediaType() {
return uriListMediaType;
}
@@ -263,7 +301,13 @@ public class RepositoryRestController implements InitializingBean {
if (null == incoming) {
model.addAttribute(STATUS, HttpStatus.NOT_ACCEPTABLE);
} else {
if (null != eventPublisher) {
eventPublisher.publishEvent(new BeforeSaveEvent(incoming));
}
Object savedEntity = repo.save(incoming);
if (null != eventPublisher) {
eventPublisher.publishEvent(new AfterSaveEvent(savedEntity));
}
String sId = typeMeta.entityInfo.getId(savedEntity).toString();
URI selfUri = buildUri(baseUri, repository, sId);
@@ -384,14 +428,26 @@ public class RepositoryRestController implements InitializingBean {
} else {
typeMeta.entityMetadata.id(serId, incoming);
if (request.getMethod() == HttpMethod.POST) {
repo.save(incoming);
if (null != eventPublisher) {
eventPublisher.publishEvent(new BeforeSaveEvent(incoming));
}
Object savedEntity = repo.save(incoming);
if (null != eventPublisher) {
eventPublisher.publishEvent(new AfterSaveEvent(savedEntity));
}
URI selfUri = buildUri(baseUri, repository, id);
HttpHeaders headers = new HttpHeaders();
headers.set(LOCATION, selfUri.toString());
model.addAttribute(HEADERS, headers);
model.addAttribute(STATUS, HttpStatus.CREATED);
} else {
repo.save(incoming);
if (null != eventPublisher) {
eventPublisher.publishEvent(new BeforeSaveEvent(incoming));
}
Object savedEntity = repo.save(incoming);
if (null != eventPublisher) {
eventPublisher.publishEvent(new AfterSaveEvent(savedEntity));
}
model.addAttribute(STATUS, HttpStatus.NO_CONTENT);
}
}
@@ -415,7 +471,13 @@ public class RepositoryRestController implements InitializingBean {
TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
Serializable serId = stringToSerializable(id, typeMeta.idType);
if (null != eventPublisher) {
eventPublisher.publishEvent(new BeforeDeleteEvent(serId));
}
repo.delete(serId);
if (null != eventPublisher) {
eventPublisher.publishEvent(new AfterDeleteEvent(serId));
}
model.addAttribute(STATUS, HttpStatus.NO_CONTENT);
}
@@ -547,6 +609,7 @@ public class RepositoryRestController implements InitializingBean {
if (null == attr) {
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
} else {
Object child = typeMeta.entityMetadata.get(attr.getName(), entity);
final AtomicReference<String> rel = new AtomicReference<String>();
Handler<Object, Void> entityHandler = new Handler<Object, Void>() {
@Override public Void handle(Object childEntity) {
@@ -621,7 +684,16 @@ public class RepositoryRestController implements InitializingBean {
}
}
repo.save(entity);
if (null != eventPublisher) {
eventPublisher.publishEvent(new BeforeSaveEvent(entity));
eventPublisher.publishEvent(new BeforeChildSaveEvent(entity, child));
}
Object savedEntity = repo.save(entity);
if (null != eventPublisher) {
child = typeMeta.entityMetadata.get(attr.getName(), savedEntity);
eventPublisher.publishEvent(new AfterChildSaveEvent(savedEntity, child));
eventPublisher.publishEvent(new AfterSaveEvent(savedEntity));
}
if (request.getMethod() == HttpMethod.PUT) {
model.addAttribute(STATUS, HttpStatus.NO_CONTENT);
@@ -658,9 +730,16 @@ public class RepositoryRestController implements InitializingBean {
} else {
final Attribute attr = typeMeta.entityMetadata.linkedAttributes().get(property);
if (null != attr) {
Object child = typeMeta.entityMetadata.get(property, entity);
typeMeta.entityMetadata.set(property, null, entity);
repo.save(entity);
if (null != eventPublisher) {
eventPublisher.publishEvent(new BeforeChildSaveEvent(entity, child));
}
Object savedEntity = repo.save(entity);
if (null != eventPublisher) {
eventPublisher.publishEvent(new AfterChildSaveEvent(savedEntity, null));
}
model.addAttribute(STATUS, HttpStatus.NO_CONTENT);
} else {
@@ -817,7 +896,24 @@ public class RepositoryRestController implements InitializingBean {
headers.setContentType(MediaType.APPLICATION_JSON);
Map m = new HashMap();
m.put("message", ex.getMessage());
return new ResponseEntity(objectMapper.writeValueAsBytes(m), headers, HttpStatus.BAD_REQUEST);
return new ResponseEntity(objectMapper.writeValueAsBytes(m), headers, HttpStatus.CONFLICT);
}
@SuppressWarnings({"unchecked"})
@ExceptionHandler(RepositoryConstraintViolationException.class)
public Model handleValidationFailure(RepositoryConstraintViolationException ex) throws IOException {
Model model = new ExtendedModelMap();
model.addAttribute(STATUS, HttpStatus.BAD_REQUEST);
Map m = new HashMap();
List<String> errors = new ArrayList<String>();
for (FieldError fe : ex.getErrors().getFieldErrors()) {
errors.add(fe.getDefaultMessage());
}
m.put("errors", errors);
model.addAttribute(RESOURCE, m);
return model;
}
private static URI buildUri(URI baseUri, String... pathSegments) {

View File

@@ -1,24 +1,12 @@
package org.springframework.data.rest.webmvc;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.persistence.EntityManagerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.rest.repository.JpaRepositoryMetadata;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver;
@@ -37,21 +25,23 @@ public class RepositoryRestMvcConfiguration {
@Autowired
RepositoryRestConfiguration parentConfig;
RepositoryRestController repositoryRestController;
@Autowired(required = false)
ContentNegotiatingViewResolver viewResolver;
@Bean ContentNegotiatingViewResolver contentNegotiatingViewResolver() {
ContentNegotiatingViewResolver viewResolver = new ContentNegotiatingViewResolver();
Map<String, String> jsonTypes = new HashMap<String, String>() {{
put("json", "application/json");
put("sdjson", "application/x-spring-data+json");
put("urilist", "text/uri-list");
}};
if (null == viewResolver) {
viewResolver = new ContentNegotiatingViewResolver();
Map<String, String> jsonTypes = new HashMap<String, String>() {{
put("json", "application/json");
put("urilist", "text/uri-list");
}};
viewResolver.setMediaTypes(jsonTypes);
viewResolver.setDefaultViews(
Arrays.asList((View) new JsonView("application/json"),
(View) new JsonView("application/x-spring-data+json"),
(View) new UriListView())
);
viewResolver.setMediaTypes(jsonTypes);
viewResolver.setDefaultViews(
Arrays.asList((View) new JsonView("application/json"),
(View) new UriListView())
);
}
return viewResolver;
}
@@ -61,6 +51,7 @@ public class RepositoryRestMvcConfiguration {
.repositoryMetadata(parentConfig.jpaRepositoryMetadata())
.conversionService(parentConfig.conversionService())
.httpMessageConverters(parentConfig.httpMessageConverters())
.viewResolver(contentNegotiatingViewResolver())
.jsonMediaType("application/json");
}
return repositoryRestController;

View File

@@ -3,8 +3,11 @@ package org.springframework.data.rest.webmvc.spec
import org.codehaus.jackson.map.ObjectMapper
import org.codehaus.jackson.map.ser.CustomSerializerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.rest.core.SimpleLink
import org.springframework.data.rest.core.util.FluentBeanSerializer
import org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener
import org.springframework.data.rest.test.webmvc.Address
import org.springframework.data.rest.webmvc.RepositoryRestConfiguration
import org.springframework.data.rest.webmvc.RepositoryRestController
@@ -22,7 +25,7 @@ import spock.lang.Specification
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
@ContextConfiguration(classes = [RepositoryRestConfiguration, RepositoryRestMvcConfiguration])
@ContextConfiguration(classes = [RepositoryRestConfiguration, RepositoryRestMvcConfiguration, RepositorySpecConfig])
class RepositoryRestControllerSpec extends Specification {
@Shared
@@ -132,3 +135,12 @@ class RepositoryRestControllerSpec extends Specification {
}
}
@Configuration
class RepositorySpecConfig {
@Bean ValidatingRepositoryEventListener validator() {
new ValidatingRepositoryEventListener()
}
}

View File

@@ -0,0 +1,27 @@
package org.springframework.data.rest.test.webmvc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ClassUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class PersonValidator implements Validator {
private static final Logger LOG = LoggerFactory.getLogger(PersonValidator.class);
@Override public boolean supports(Class<?> clazz) {
return ClassUtils.isAssignable(clazz, Person.class);
}
@Override public void validate(Object target, Errors errors) {
Person p = (Person) target;
LOG.debug("validating Person " + p);
ValidationUtils.rejectIfEmpty(errors, "name", "field.name.required", "Field 'name' cannot be blank.");
}
}

View File

@@ -9,6 +9,10 @@
<jpa:repositories base-package="org.springframework.data.rest.test.webmvc"/>
<bean id="beforeSavePersonValidator" class="org.springframework.data.rest.test.webmvc.PersonValidator"/>
<bean class="org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener"/>
<!--
<bean class="org.springframework.data.rest.test.webmvc.PersonLoader">
<property name="personRepository" ref="personRepository"/>