From fe9e485d812087dafcaec9fdf584a4684f3548c7 Mon Sep 17 00:00:00 2001 From: Jon Brisbin Date: Thu, 26 Apr 2012 11:40:14 -0500 Subject: [PATCH] Added ApplicationEvent handling and annotation-based extension mechanism. --- build.gradle | 4 +- spring-data-rest-core/build.gradle | 2 +- .../data/rest/core/util/RestHelper.java | 43 +++++ .../repository/JpaRepositoryMetadata.java | 19 +- ...epositoryConstraintViolationException.java | 22 +++ .../rest/repository/ValidationErrors.java | 61 +++++++ .../annotation/HandleAfterChildSave.java | 19 ++ .../annotation/HandleAfterDelete.java | 19 ++ .../annotation/HandleAfterSave.java | 19 ++ .../annotation/HandleBeforeChildSave.java | 19 ++ .../annotation/HandleBeforeDelete.java | 19 ++ .../annotation/HandleBeforeSave.java | 19 ++ .../annotation/RepositoryEventHandler.java | 19 ++ .../AbstractRepositoryEventListener.java | 71 ++++++++ .../context/AfterChildSaveEvent.java | 10 ++ .../repository/context/AfterDeleteEvent.java | 10 ++ .../repository/context/AfterSaveEvent.java | 12 ++ ...notatedHandlerRepositoryEventListener.java | 167 ++++++++++++++++++ .../context/BeforeChildSaveEvent.java | 10 ++ .../repository/context/BeforeDeleteEvent.java | 12 ++ .../repository/context/BeforeSaveEvent.java | 12 ++ .../repository/context/ChildSaveEvent.java | 19 ++ .../repository/context/RepositoryEvent.java | 12 ++ .../ValidatingRepositoryEventListener.java | 113 ++++++++++++ .../repository/spec/ExtensionsSpec.groovy | 92 ++++++++++ .../test/resources/ExtensionsSpec-test.xml | 12 ++ .../rest/webmvc/RepositoryRestController.java | 108 ++++++++++- .../RepositoryRestMvcConfiguration.java | 39 ++-- .../spec/RepositoryRestControllerSpec.groovy | 14 +- .../rest/test/webmvc/PersonValidator.java | 27 +++ .../spring-data-rest/repositories-export.xml | 4 + 31 files changed, 987 insertions(+), 41 deletions(-) create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/RestHelper.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryConstraintViolationException.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/ValidationErrors.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterChildSave.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterDelete.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterSave.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeChildSave.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeDelete.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeSave.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/RepositoryEventHandler.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterChildSaveEvent.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterDeleteEvent.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterSaveEvent.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AnnotatedHandlerRepositoryEventListener.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeChildSaveEvent.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeDeleteEvent.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeSaveEvent.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ChildSaveEvent.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RepositoryEvent.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ValidatingRepositoryEventListener.java create mode 100644 spring-data-rest-repository/src/test/groovy/org/springframework/data/rest/repository/spec/ExtensionsSpec.groovy create mode 100644 spring-data-rest-repository/src/test/resources/ExtensionsSpec-test.xml create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonValidator.java diff --git a/build.gradle b/build.gradle index a461316c9..9086565af 100644 --- a/build.gradle +++ b/build.gradle @@ -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 { diff --git a/spring-data-rest-core/build.gradle b/spring-data-rest-core/build.gradle index 1b01ab021..828755e09 100644 --- a/spring-data-rest-core/build.gradle +++ b/spring-data-rest-core/build.gradle @@ -1,6 +1,6 @@ dependencies { // Google Guava - compile "com.google.guava:guava:11.0.1" + compile "com.google.guava:guava:11.0.2" } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/RestHelper.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/RestHelper.java new file mode 100644 index 000000000..704d6b3dc --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/RestHelper.java @@ -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 + */ +public class RestHelper { + + public HttpStatus status; + public HttpHeaders headers = new HttpHeaders(); + public T body; + + private RestHelper(T body) { + this.body = body; + } + + public static RestHelper resource(T body) { + return new RestHelper(body); + } + + public RestHelper header(String key, String value) { + headers.add(key, value); + return this; + } + + public RestHelper status(HttpStatus status) { + this.status = status; + return this; + } + + public HttpEntity asHttpEntity() { + return new HttpEntity(body, headers); + } + + public ResponseEntity asResponseEntity() { + return new ResponseEntity(body, headers, status); + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/JpaRepositoryMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/JpaRepositoryMetadata.java index 239f96daa..1ba440bb2 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/JpaRepositoryMetadata.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/JpaRepositoryMetadata.java @@ -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 */ -public class JpaRepositoryMetadata implements InitializingBean, ApplicationContextAware { +public class JpaRepositoryMetadata + implements InitializingBean, + ApplicationContextAware { private ApplicationContext applicationContext; private Map, RepositoryCacheEntry> repositories = new HashMap, RepositoryCacheEntry>(); @@ -125,8 +125,10 @@ public class JpaRepositoryMetadata implements InitializingBean, ApplicationConte return names; } - public void setRepositories(Collection repositories) { - for (CrudRepository repository : repositories) { + public void setRepositories(Map repositories) { + for (Map.Entry 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 beans = appCtx.getBeansOfType(CrudRepository.class); - setRepositories(beans.values()); + setRepositories(beans); appCtx = appCtx.getParent(); } } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryConstraintViolationException.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryConstraintViolationException.java new file mode 100644 index 000000000..508c91263 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryConstraintViolationException.java @@ -0,0 +1,22 @@ +package org.springframework.data.rest.repository; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.validation.Errors; + +/** + * @author Jon Brisbin + */ +public class RepositoryConstraintViolationException extends DataIntegrityViolationException { + + private Errors errors; + + public RepositoryConstraintViolationException(Errors errors) { + super("Validation failed"); + this.errors = errors; + } + + public Errors getErrors() { + return errors; + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/ValidationErrors.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/ValidationErrors.java new file mode 100644 index 000000000..9590d9245 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/ValidationErrors.java @@ -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 + */ +public class ValidationErrors extends AbstractErrors { + + private String name; + private Object entity; + private JpaEntityMetadata entityMetadata; + private List globalErrors = new ArrayList(); + private List fieldErrors = new ArrayList(); + + 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 getGlobalErrors() { + return globalErrors; + } + + @Override public List getFieldErrors() { + return fieldErrors; + } + + @Override public Object getFieldValue(String field) { + return entityMetadata.get(field, entity); + } +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterChildSave.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterChildSave.java new file mode 100644 index 000000000..70f674249 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterChildSave.java @@ -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 HandleAfterChildSave { + + Class[] value() default {}; + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterDelete.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterDelete.java new file mode 100644 index 000000000..7aacb594c --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterDelete.java @@ -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 HandleAfterDelete { + + Class[] value() default {}; + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterSave.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterSave.java new file mode 100644 index 000000000..ab3f05889 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterSave.java @@ -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 HandleAfterSave { + + Class[] value() default {}; + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeChildSave.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeChildSave.java new file mode 100644 index 000000000..d274c3b65 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeChildSave.java @@ -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 HandleBeforeChildSave { + + Class[] value() default {}; + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeDelete.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeDelete.java new file mode 100644 index 000000000..0220120d4 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeDelete.java @@ -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 HandleBeforeDelete { + + Class[] value() default {}; + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeSave.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeSave.java new file mode 100644 index 000000000..44ee55485 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeSave.java @@ -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 HandleBeforeSave { + + Class[] value() default {}; + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/RepositoryEventHandler.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/RepositoryEventHandler.java new file mode 100644 index 000000000..56b3d0ac6 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/RepositoryEventHandler.java @@ -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.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface RepositoryEventHandler { + + Class[] value() default {}; + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java new file mode 100644 index 000000000..28390f2d9 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java @@ -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 + */ +public abstract class AbstractRepositoryEventListener> + implements ApplicationListener, + 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) {} + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterChildSaveEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterChildSaveEvent.java new file mode 100644 index 000000000..142aab4af --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterChildSaveEvent.java @@ -0,0 +1,10 @@ +package org.springframework.data.rest.repository.context; + +/** + * @author Jon Brisbin + */ +public class AfterChildSaveEvent extends ChildSaveEvent { + public AfterChildSaveEvent(Object source, Object child) { + super(source, child); + } +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterDeleteEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterDeleteEvent.java new file mode 100644 index 000000000..a4f828728 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterDeleteEvent.java @@ -0,0 +1,10 @@ +package org.springframework.data.rest.repository.context; + +/** + * @author Jon Brisbin + */ +public class AfterDeleteEvent extends RepositoryEvent { + public AfterDeleteEvent(Object source) { + super(source); + } +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterSaveEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterSaveEvent.java new file mode 100644 index 000000000..9302b37aa --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterSaveEvent.java @@ -0,0 +1,12 @@ +package org.springframework.data.rest.repository.context; + +import org.springframework.context.ApplicationEvent; + +/** + * @author Jon Brisbin + */ +public class AfterSaveEvent extends RepositoryEvent { + public AfterSaveEvent(Object source) { + super(source); + } +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AnnotatedHandlerRepositoryEventListener.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AnnotatedHandlerRepositoryEventListener.java new file mode 100644 index 000000000..ab2518d88 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AnnotatedHandlerRepositoryEventListener.java @@ -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 + */ +public class AnnotatedHandlerRepositoryEventListener + implements ApplicationListener, + ApplicationContextAware, + InitializingBean { + + private String basePackage; + private ApplicationContext applicationContext; + private Multimap, 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 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 params = new ArrayList(); + 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 void inspect(Class targetType, + Object handler, + Method method, + Class annoType, + Class 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; + } + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeChildSaveEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeChildSaveEvent.java new file mode 100644 index 000000000..f165107bf --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeChildSaveEvent.java @@ -0,0 +1,10 @@ +package org.springframework.data.rest.repository.context; + +/** + * @author Jon Brisbin + */ +public class BeforeChildSaveEvent extends ChildSaveEvent { + public BeforeChildSaveEvent(Object source, Object child) { + super(source, child); + } +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeDeleteEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeDeleteEvent.java new file mode 100644 index 000000000..e3f6340cf --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeDeleteEvent.java @@ -0,0 +1,12 @@ +package org.springframework.data.rest.repository.context; + +import org.springframework.context.ApplicationEvent; + +/** + * @author Jon Brisbin + */ +public class BeforeDeleteEvent extends RepositoryEvent { + public BeforeDeleteEvent(Object source) { + super(source); + } +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeSaveEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeSaveEvent.java new file mode 100644 index 000000000..c4832b898 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeSaveEvent.java @@ -0,0 +1,12 @@ +package org.springframework.data.rest.repository.context; + +import org.springframework.context.ApplicationEvent; + +/** + * @author Jon Brisbin + */ +public class BeforeSaveEvent extends RepositoryEvent { + public BeforeSaveEvent(Object source) { + super(source); + } +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ChildSaveEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ChildSaveEvent.java new file mode 100644 index 000000000..3a765655f --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ChildSaveEvent.java @@ -0,0 +1,19 @@ +package org.springframework.data.rest.repository.context; + +/** + * @author Jon Brisbin + */ +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; + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RepositoryEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RepositoryEvent.java new file mode 100644 index 000000000..b07493fa5 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RepositoryEvent.java @@ -0,0 +1,12 @@ +package org.springframework.data.rest.repository.context; + +import org.springframework.context.ApplicationEvent; + +/** + * @author Jon Brisbin + */ +public abstract class RepositoryEvent extends ApplicationEvent { + protected RepositoryEvent(Object source) { + super(source); + } +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ValidatingRepositoryEventListener.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ValidatingRepositoryEventListener.java new file mode 100644 index 000000000..18097eddd --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ValidatingRepositoryEventListener.java @@ -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 + */ +public class ValidatingRepositoryEventListener + extends AbstractRepositoryEventListener + implements InitializingBean { + + private static final Logger LOG = LoggerFactory.getLogger(ValidatingRepositoryEventListener.class); + + private Multimap validators = ArrayListMultimap.create(); + + @Override public void afterPropertiesSet() throws Exception { + if (validators.size() == 0) { + Map validators = applicationContext.getBeansOfType(Validator.class); + for (Map.Entry 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> getValidators() { + return validators.asMap(); + } + + public ValidatingRepositoryEventListener setValidators(Map> validators) { + for (Map.Entry> 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 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; + } + +} diff --git a/spring-data-rest-repository/src/test/groovy/org/springframework/data/rest/repository/spec/ExtensionsSpec.groovy b/spring-data-rest-repository/src/test/groovy/org/springframework/data/rest/repository/spec/ExtensionsSpec.groovy new file mode 100644 index 000000000..698d8a4be --- /dev/null +++ b/spring-data-rest-repository/src/test/groovy/org/springframework/data/rest/repository/spec/ExtensionsSpec.groovy @@ -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 + */ +@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 + } + +} diff --git a/spring-data-rest-repository/src/test/resources/ExtensionsSpec-test.xml b/spring-data-rest-repository/src/test/resources/ExtensionsSpec-test.xml new file mode 100644 index 000000000..b37321f86 --- /dev/null +++ b/spring-data-rest-repository/src/test/resources/ExtensionsSpec-test.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java index 2788c06a2..f05197cc4 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java @@ -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 */ @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 typeMetaCache = new ConcurrentHashMap(); private ConversionService conversionService = new DefaultConversionService(); private List> 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 rel = new AtomicReference(); Handler entityHandler = new Handler() { @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 errors = new ArrayList(); + 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) { diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java index 14305d867..aa92eb283 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java @@ -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 jsonTypes = new HashMap() {{ - put("json", "application/json"); - put("sdjson", "application/x-spring-data+json"); - put("urilist", "text/uri-list"); - }}; + if (null == viewResolver) { + viewResolver = new ContentNegotiatingViewResolver(); + Map jsonTypes = new HashMap() {{ + 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; diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy index d6f93f106..ea534bb36 100644 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy +++ b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy @@ -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 */ -@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() + } + +} \ No newline at end of file diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonValidator.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonValidator.java new file mode 100644 index 000000000..82665abf7 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonValidator.java @@ -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 + */ +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."); + } + +} diff --git a/spring-data-rest-webmvc/src/test/resources/META-INF/spring-data-rest/repositories-export.xml b/spring-data-rest-webmvc/src/test/resources/META-INF/spring-data-rest/repositories-export.xml index 92fb2ef41..8b48df99b 100644 --- a/spring-data-rest-webmvc/src/test/resources/META-INF/spring-data-rest/repositories-export.xml +++ b/spring-data-rest-webmvc/src/test/resources/META-INF/spring-data-rest/repositories-export.xml @@ -9,6 +9,10 @@ + + + +