diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/AnnotatedStoreRestEventInvoker.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/AnnotatedStoreRestEventInvoker.java new file mode 100644 index 000000000..358d2d2f3 --- /dev/null +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/AnnotatedStoreRestEventInvoker.java @@ -0,0 +1,225 @@ +package internal.org.springframework.content.rest.contentservice; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.content.commons.annotations.HandleAfterAssociate; +import org.springframework.content.commons.annotations.HandleAfterGetContent; +import org.springframework.content.commons.annotations.HandleAfterGetResource; +import org.springframework.content.commons.annotations.HandleAfterSetContent; +import org.springframework.content.commons.annotations.HandleAfterUnassociate; +import org.springframework.content.commons.annotations.HandleAfterUnsetContent; +import org.springframework.content.commons.annotations.HandleBeforeAssociate; +import org.springframework.content.commons.annotations.HandleBeforeGetContent; +import org.springframework.content.commons.annotations.HandleBeforeGetResource; +import org.springframework.content.commons.annotations.HandleBeforeSetContent; +import org.springframework.content.commons.annotations.HandleBeforeUnassociate; +import org.springframework.content.commons.annotations.HandleBeforeUnsetContent; +import org.springframework.content.commons.utils.ReflectionService; +import org.springframework.content.commons.utils.ReflectionServiceImpl; +import org.springframework.content.rest.AfterAssociateEvent; +import org.springframework.content.rest.AfterGetContentEvent; +import org.springframework.content.rest.AfterGetResourceEvent; +import org.springframework.content.rest.AfterSetContentEvent; +import org.springframework.content.rest.AfterUnassociateEvent; +import org.springframework.content.rest.AfterUnsetContentEvent; +import org.springframework.content.rest.BeforeAssociateEvent; +import org.springframework.content.rest.BeforeGetContentEvent; +import org.springframework.content.rest.BeforeGetResourceEvent; +import org.springframework.content.rest.BeforeSetContentEvent; +import org.springframework.content.rest.BeforeUnassociateEvent; +import org.springframework.content.rest.BeforeUnsetContentEvent; +import org.springframework.content.rest.StoreRestEvent; +import org.springframework.content.rest.StoreRestEventHandler; +import org.springframework.context.ApplicationListener; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ClassUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ReflectionUtils; + +public class AnnotatedStoreRestEventInvoker + implements ApplicationListener, BeanPostProcessor { + + private static final Log logger = LogFactory.getLog(AnnotatedStoreRestEventInvoker.class); + + private final MultiValueMap, EventHandlerMethod> handlerMethods = new LinkedMultiValueMap, EventHandlerMethod>(); + + private ReflectionService reflectionService; + + public AnnotatedStoreRestEventInvoker() { + reflectionService = new ReflectionServiceImpl(); + } + + public AnnotatedStoreRestEventInvoker(ReflectionService reflectionService) { + this.reflectionService = reflectionService; + } + + MultiValueMap, EventHandlerMethod> getHandlers() { + return handlerMethods; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + + Class beanType = ClassUtils.getUserClass(bean); + StoreRestEventHandler typeAnno = AnnotationUtils.findAnnotation(beanType, StoreRestEventHandler.class); + + if (typeAnno == null) { + return bean; + } + + ReflectionUtils.doWithMethods(beanType, new ReflectionUtils.MethodCallback() { + + @Override + public void doWith(Method method) + throws IllegalArgumentException, IllegalAccessException { + findHandler(bean, method, HandleBeforeGetResource.class, + BeforeGetResourceEvent.class); + findHandler(bean, method, HandleAfterGetResource.class, + AfterGetResourceEvent.class); + findHandler(bean, method, HandleBeforeAssociate.class, + BeforeAssociateEvent.class); + findHandler(bean, method, HandleAfterAssociate.class, + AfterAssociateEvent.class); + findHandler(bean, method, HandleBeforeUnassociate.class, + BeforeUnassociateEvent.class); + findHandler(bean, method, HandleAfterUnassociate.class, + AfterUnassociateEvent.class); + findHandler(bean, method, HandleBeforeGetContent.class, + BeforeGetContentEvent.class); + findHandler(bean, method, HandleAfterGetContent.class, + AfterGetContentEvent.class); + findHandler(bean, method, HandleBeforeSetContent.class, + BeforeSetContentEvent.class); + findHandler(bean, method, HandleAfterSetContent.class, + AfterSetContentEvent.class); + findHandler(bean, method, HandleBeforeUnsetContent.class, + BeforeUnsetContentEvent.class); + findHandler(bean, method, HandleAfterUnsetContent.class, + AfterUnsetContentEvent.class); + } + + }); + + return bean; + } + + @Override + public void onApplicationEvent(StoreRestEvent event) { + Class eventType = event.getClass(); + + if (!handlerMethods.containsKey(eventType)) { + return; + } + + for (EventHandlerMethod handlerMethod : handlerMethods.get(eventType)) { + + Object src = event.getSource(); + + if ((ClassUtils.isAssignable(StoreRestEvent.class, handlerMethod.targetType) && + ClassUtils.isAssignable(handlerMethod.targetType, event.getClass()) == false) || + (ClassUtils.isAssignable(StoreRestEvent.class, handlerMethod.targetType) == false && + ClassUtils.isAssignable(handlerMethod.targetType, src.getClass()) == false)) { + continue; + } + + List parameters = new ArrayList(); + if (ClassUtils.isAssignable(StoreRestEvent.class, handlerMethod.targetType)) { + parameters.add(event); + } else { + parameters.add(src); + } + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Invoking {} handler for {}.", + event.getClass().getSimpleName(), event.getSource())); + } + + reflectionService.invokeMethod(handlerMethod.method, handlerMethod.handler, parameters.toArray()); + } + } + + void findHandler(Object bean, Method method, Class handler, Class eventType) { + + H annotation = AnnotationUtils.findAnnotation(method, handler); + + if (annotation == null) { + return; + } + + Class[] parameterTypes = method.getParameterTypes(); + + if (parameterTypes.length == 0) { + throw new IllegalStateException(String.format( + "Event handler method %s must have a content object argument", + method.getName())); + } + + EventHandlerMethod handlerMethod = new EventHandlerMethod(parameterTypes[0], bean, method); + + logger.debug(String.format("Annotated handler method found: {%s}", handlerMethod)); + + List events = handlerMethods.get(eventType); + + if (events == null) { + events = new ArrayList<>(); + } + + if (events.isEmpty()) { + handlerMethods.add(eventType, handlerMethod); + return; + } + + events.add(handlerMethod); + Collections.sort(events); + handlerMethods.put(eventType, events); + } + + static class EventHandlerMethod implements Comparable { + + 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; + + ReflectionUtils.makeAccessible(this.method); + } + + /* + * (non-Javadoc) + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ + @Override + public int compareTo(EventHandlerMethod o) { + return AnnotationAwareOrderComparator.INSTANCE.compare(this.method, o.method); + } + + @Override + public String toString() { + return String.format( + "EventHandlerMethod{ targetType=%s, method=%s, handler=%s }", + targetType, method, handler); + } + } +} diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentServiceFactory.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentServiceFactory.java index 3141ee7dd..cb61c27df 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentServiceFactory.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentServiceFactory.java @@ -4,6 +4,7 @@ import org.springframework.content.commons.repository.ContentStore; import org.springframework.content.commons.storeservice.Stores; import org.springframework.content.rest.config.RestConfiguration; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.repository.support.Repositories; import org.springframework.data.repository.support.RepositoryInvokerFactory; @@ -18,13 +19,15 @@ public class ContentServiceFactory { private final RepositoryInvokerFactory repoInvokerFactory; private final Stores stores; private final StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler; + private final ApplicationEventPublisher publisher; - public ContentServiceFactory(RestConfiguration config, Repositories repositories, RepositoryInvokerFactory repoInvokerFactory, Stores stores, StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler) { + public ContentServiceFactory(RestConfiguration config, Repositories repositories, RepositoryInvokerFactory repoInvokerFactory, Stores stores, StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler, ApplicationEventPublisher publisher) { this.config = config; this.repositories = repositories; this.repoInvokerFactory = repoInvokerFactory; this.stores = stores; this.byteRangeRestRequestHandler = byteRangeRestRequestHandler; + this.publisher = publisher; } public ContentService getContentService(StoreResource resource) { @@ -33,7 +36,7 @@ public ContentService getContentService(StoreResource resource) { Object entity = ((AssociatedStoreResource)resource).getAssociation(); - return new ContentStoreContentService(config, null, repoInvokerFactory.getInvokerFor(entity.getClass()), entity, byteRangeRestRequestHandler); + return new EventingContentService(publisher, new ContentStoreContentService(config, null, repoInvokerFactory.getInvokerFor(entity.getClass()), entity, byteRangeRestRequestHandler)); } else if (AssociativeStore.class.isAssignableFrom(resource.getStoreInfo().getInterface())) { diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java index a146e5fc4..daebd4ddd 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java @@ -192,9 +192,9 @@ public void setContent(HttpServletRequest request, HttpServletResponse response, Object updatedDomainObj = ReflectionUtils.invokeMethod(methodToUse, targetObj, updateObject, PropertyPath.from(property.getContentPropertyPath()), contentArg); - Object saveObject = updatedDomainObj; + Object savedUpdatedObject = repoInvoker.invokeSave(updatedDomainObj); - repoInvoker.invokeSave(saveObject); + storeResource.setAssociation(savedUpdatedObject); } finally { cleanup(contentArg); } diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/EventingContentService.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/EventingContentService.java new file mode 100644 index 000000000..968e5b561 --- /dev/null +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/EventingContentService.java @@ -0,0 +1,68 @@ +package internal.org.springframework.content.rest.contentservice; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.content.commons.property.PropertyPath; +import org.springframework.content.rest.AfterGetContentEvent; +import org.springframework.content.rest.AfterSetContentEvent; +import org.springframework.content.rest.AfterUnsetContentEvent; +import org.springframework.content.rest.BeforeGetContentEvent; +import org.springframework.content.rest.BeforeSetContentEvent; +import org.springframework.content.rest.BeforeUnsetContentEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import internal.org.springframework.content.rest.controllers.MethodNotAllowedException; +import internal.org.springframework.content.rest.io.AssociatedStoreResource; + +public class EventingContentService implements ContentService { + + private ContentService contentService; + private ApplicationEventPublisher publisher; + + public EventingContentService(ApplicationEventPublisher publisher, ContentService contentService) { + this.publisher = publisher; + this.contentService = contentService; + } + + @Override + public void getContent(HttpServletRequest request, HttpServletResponse response, HttpHeaders headers, Resource resource, MediaType resourceType) + throws MethodNotAllowedException { + + this.publisher.publishEvent(new BeforeGetContentEvent(resource, resourceType)); + this.contentService.getContent(request, response, headers, resource, resourceType); + this.publisher.publishEvent(new AfterGetContentEvent(resource, resourceType)); + } + + @Override + public void setContent(HttpServletRequest request, HttpServletResponse response, HttpHeaders headers, Resource source, MediaType sourceMimeType, Resource target) + throws IOException, + MethodNotAllowedException { + + this.publisher.publishEvent(new BeforeSetContentEvent(source, sourceMimeType)); + + this.contentService.setContent(request, response, headers, source, sourceMimeType, target); + + Object s = ((AssociatedStoreResource)target).getAssociation(); + PropertyPath path = PropertyPath.from(((AssociatedStoreResource)target).getContentProperty().getContentPropertyPath()); + this.publisher.publishEvent(new AfterSetContentEvent(s, path, target, null)); + } + + @Override + public void unsetContent(Resource resource) + throws MethodNotAllowedException { + + Object s = ((AssociatedStoreResource)resource).getAssociation(); + PropertyPath path = PropertyPath.from(((AssociatedStoreResource)resource).getContentProperty().getContentPropertyPath()); + this.publisher.publishEvent(new BeforeUnsetContentEvent(s, path, resource, null)); + + this.contentService.unsetContent(resource); + + this.publisher.publishEvent(new AfterUnsetContentEvent(resource, null)); + } +} diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java index fc6403b27..66a5e6c53 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java @@ -14,6 +14,7 @@ import org.springframework.content.commons.storeservice.Stores; import org.springframework.content.rest.config.RestConfiguration; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.io.Resource; import org.springframework.data.repository.support.DefaultRepositoryInvokerFactory; import org.springframework.data.repository.support.Repositories; @@ -62,6 +63,9 @@ public class StoreRestController implements InitializingBean { @Autowired private StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler; + @Autowired + private ApplicationEventPublisher publisher; + private ContentServiceFactory contentServiceFactory; public StoreRestController() { @@ -229,6 +233,6 @@ public void afterPropertiesSet() throws Exception { this.repoInvokerFactory = new DefaultRepositoryInvokerFactory(this.repositories); } - contentServiceFactory = new ContentServiceFactory(config, repositories, repoInvokerFactory, stores, byteRangeRestRequestHandler); + contentServiceFactory = new ContentServiceFactory(config, repositories, repoInvokerFactory, stores, byteRangeRestRequestHandler, publisher); } } \ No newline at end of file diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResource.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResource.java index 40dcf2b0e..db2ca6308 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResource.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResource.java @@ -7,6 +7,7 @@ public interface AssociatedStoreResource extends WritableResource, StoreResource, RangeableResource { S getAssociation(); + void setAssociation(S entity); ContentProperty getContentProperty(); } diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResourceImpl.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResourceImpl.java index de541f36b..500e50518 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResourceImpl.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResourceImpl.java @@ -80,6 +80,11 @@ public S getAssociation() { return entity; } + @Override + public void setAssociation(S entity) { + this.entity = entity; + } + @Override public ContentProperty getContentProperty() { return this.contentProperty; diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/AfterAssociateEvent.java b/spring-content-rest/src/main/java/org/springframework/content/rest/AfterAssociateEvent.java new file mode 100644 index 000000000..06942eb0e --- /dev/null +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/AfterAssociateEvent.java @@ -0,0 +1,13 @@ +package org.springframework.content.rest; + +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; + +public class AfterAssociateEvent extends StoreRestEvent { + + private static final long serialVersionUID = -1256654776081821449L; + + public AfterAssociateEvent(Resource resource, MediaType resourceType) { + super(resource, resourceType); + } +} diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/AfterGetContentEvent.java b/spring-content-rest/src/main/java/org/springframework/content/rest/AfterGetContentEvent.java new file mode 100644 index 000000000..9ae7fd237 --- /dev/null +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/AfterGetContentEvent.java @@ -0,0 +1,13 @@ +package org.springframework.content.rest; + +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; + +public class AfterGetContentEvent extends StoreRestEvent { + + private static final long serialVersionUID = -3209578443616933734L; + + public AfterGetContentEvent(Resource resource, MediaType resourceType) { + super(resource, resourceType); + } +} diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/AfterGetResourceEvent.java b/spring-content-rest/src/main/java/org/springframework/content/rest/AfterGetResourceEvent.java new file mode 100644 index 000000000..3965a5db4 --- /dev/null +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/AfterGetResourceEvent.java @@ -0,0 +1,13 @@ +package org.springframework.content.rest; + +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; + +public class AfterGetResourceEvent extends StoreRestEvent { + + private static final long serialVersionUID = -52677793449429582L; + + public AfterGetResourceEvent(Resource resource, MediaType resourceType) { + super(resource, resourceType); + } +} diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/AfterSetContentEvent.java b/spring-content-rest/src/main/java/org/springframework/content/rest/AfterSetContentEvent.java new file mode 100644 index 000000000..6ba7cab4d --- /dev/null +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/AfterSetContentEvent.java @@ -0,0 +1,27 @@ +package org.springframework.content.rest; + +import org.springframework.content.commons.property.PropertyPath; +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; + +public class AfterSetContentEvent extends StoreRestEvent { + + private static final long serialVersionUID = -4974444274997145136L; + + private Object source; + private PropertyPath path; + + public AfterSetContentEvent(Object source, PropertyPath path, Resource resource, MediaType resourceType) { + super(resource, resourceType); + this.source = source; + this.path = path; + } + + public Object getActualSource() { + return source; + } + + public PropertyPath getPropetyPath() { + return path; + } +} diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/AfterUnassociateEvent.java b/spring-content-rest/src/main/java/org/springframework/content/rest/AfterUnassociateEvent.java new file mode 100644 index 000000000..096445fc4 --- /dev/null +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/AfterUnassociateEvent.java @@ -0,0 +1,13 @@ +package org.springframework.content.rest; + +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; + +public class AfterUnassociateEvent extends StoreRestEvent { + + private static final long serialVersionUID = -1981687210695835698L; + + public AfterUnassociateEvent(Resource resource, MediaType resourceType) { + super(resource, resourceType); + } +} diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/AfterUnsetContentEvent.java b/spring-content-rest/src/main/java/org/springframework/content/rest/AfterUnsetContentEvent.java new file mode 100644 index 000000000..ca05a03de --- /dev/null +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/AfterUnsetContentEvent.java @@ -0,0 +1,13 @@ +package org.springframework.content.rest; + +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; + +public class AfterUnsetContentEvent extends StoreRestEvent { + + private static final long serialVersionUID = 3984922393423249069L; + + public AfterUnsetContentEvent(Resource resource, MediaType resourceType) { + super(resource, resourceType); + } +} diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/BeforeAssociateEvent.java b/spring-content-rest/src/main/java/org/springframework/content/rest/BeforeAssociateEvent.java new file mode 100644 index 000000000..c3005c7eb --- /dev/null +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/BeforeAssociateEvent.java @@ -0,0 +1,11 @@ +package org.springframework.content.rest; + +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; + +public class BeforeAssociateEvent extends StoreRestEvent { + + public BeforeAssociateEvent(Resource resource, MediaType resourceType) { + super(resource, resourceType); + } +} diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/BeforeGetContentEvent.java b/spring-content-rest/src/main/java/org/springframework/content/rest/BeforeGetContentEvent.java new file mode 100644 index 000000000..0cc56b33d --- /dev/null +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/BeforeGetContentEvent.java @@ -0,0 +1,13 @@ +package org.springframework.content.rest; + +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; + +public class BeforeGetContentEvent extends StoreRestEvent { + + private static final long serialVersionUID = -6943798939368100773L; + + public BeforeGetContentEvent(Resource resource, MediaType resourceType) { + super(resource, resourceType); + } +} diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/BeforeGetResourceEvent.java b/spring-content-rest/src/main/java/org/springframework/content/rest/BeforeGetResourceEvent.java new file mode 100644 index 000000000..ce4cc3326 --- /dev/null +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/BeforeGetResourceEvent.java @@ -0,0 +1,13 @@ +package org.springframework.content.rest; + +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; + +public class BeforeGetResourceEvent extends StoreRestEvent { + + private static final long serialVersionUID = -4288863659935527531L; + + public BeforeGetResourceEvent(Resource resource, MediaType resourceType) { + super(resource, resourceType); + } +} diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/BeforeSetContentEvent.java b/spring-content-rest/src/main/java/org/springframework/content/rest/BeforeSetContentEvent.java new file mode 100644 index 000000000..f1e6be7c6 --- /dev/null +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/BeforeSetContentEvent.java @@ -0,0 +1,21 @@ +package org.springframework.content.rest; + +import java.io.InputStream; + +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; + +import lombok.Getter; + +@Getter +public class BeforeSetContentEvent extends StoreRestEvent { + + private static final long serialVersionUID = -7299354365313770L; + + private InputStream is; + private Resource resource; + + public BeforeSetContentEvent(Resource resource, MediaType resourceType) { + super(resource, resourceType); + } +} diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/BeforeUnassociateEvent.java b/spring-content-rest/src/main/java/org/springframework/content/rest/BeforeUnassociateEvent.java new file mode 100644 index 000000000..308c517f8 --- /dev/null +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/BeforeUnassociateEvent.java @@ -0,0 +1,11 @@ +package org.springframework.content.rest; + +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; + +public class BeforeUnassociateEvent extends StoreRestEvent { + + public BeforeUnassociateEvent(Resource resource, MediaType resourceType) { + super(resource, resourceType); + } +} diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/BeforeUnsetContentEvent.java b/spring-content-rest/src/main/java/org/springframework/content/rest/BeforeUnsetContentEvent.java new file mode 100644 index 000000000..8ff46b0db --- /dev/null +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/BeforeUnsetContentEvent.java @@ -0,0 +1,27 @@ +package org.springframework.content.rest; + +import org.springframework.content.commons.property.PropertyPath; +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; + +public class BeforeUnsetContentEvent extends StoreRestEvent { + + private static final long serialVersionUID = 2662992853516955647L; + + private Object source; + private PropertyPath path; + + public BeforeUnsetContentEvent(Object source, PropertyPath path, Resource resource, MediaType resourceType) { + super(resource, resourceType); + this.source = source; + this.path = path; + } + + public Object getActualSource() { + return source; + } + + public PropertyPath getPropetyPath() { + return path; + } +} diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/StoreRestEvent.java b/spring-content-rest/src/main/java/org/springframework/content/rest/StoreRestEvent.java new file mode 100644 index 000000000..899412e53 --- /dev/null +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/StoreRestEvent.java @@ -0,0 +1,25 @@ +package org.springframework.content.rest; + +import org.springframework.context.ApplicationEvent; +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; + +public class StoreRestEvent extends ApplicationEvent { + + private Resource resource; + private MediaType resourceType; + + public StoreRestEvent(Resource resource, MediaType resourceType) { + super(resource); + this.resource = resource; + this.resourceType = resourceType; + } + + public Resource getResource() { + return resource; + } + + public MediaType getResourceType() { + return resourceType; + } +} diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/StoreRestEventHandler.java b/spring-content-rest/src/main/java/org/springframework/content/rest/StoreRestEventHandler.java new file mode 100644 index 000000000..5461272e1 --- /dev/null +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/StoreRestEventHandler.java @@ -0,0 +1,14 @@ +package org.springframework.content.rest; + +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; + +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface StoreRestEventHandler { + +} diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/config/RestConfiguration.java b/spring-content-rest/src/main/java/org/springframework/content/rest/config/RestConfiguration.java index ce72ddab5..3444662eb 100644 --- a/spring-content-rest/src/main/java/org/springframework/content/rest/config/RestConfiguration.java +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/config/RestConfiguration.java @@ -33,6 +33,7 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import internal.org.springframework.content.commons.storeservice.StoresImpl; +import internal.org.springframework.content.rest.contentservice.AnnotatedStoreRestEventInvoker; import internal.org.springframework.content.rest.controllers.ResourceHandlerMethodArgumentResolver; import internal.org.springframework.content.rest.controllers.resolvers.DefaultEntityResolver; import internal.org.springframework.content.rest.controllers.resolvers.EntityResolvers; @@ -173,6 +174,11 @@ EntityResolvers entityResolvers(ApplicationContext context, Stores stores, Mappi return entityResolvers; } + @Bean + AnnotatedStoreRestEventInvoker storeRestEventInvoker() { + return new AnnotatedStoreRestEventInvoker(); + } + @Override public void afterPropertiesSet() throws Exception { diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/events/StoreRestEventsIT.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/events/StoreRestEventsIT.java new file mode 100644 index 000000000..6a8ec90d5 --- /dev/null +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/events/StoreRestEventsIT.java @@ -0,0 +1,232 @@ +package internal.org.springframework.content.rest.events; + +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.BeforeEach; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Context; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Describe; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.FIt; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Optional; +import java.util.UUID; + +import org.apache.commons.io.IOUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.content.commons.annotations.HandleAfterGetContent; +import org.springframework.content.commons.annotations.HandleAfterSetContent; +import org.springframework.content.commons.annotations.HandleAfterUnsetContent; +import org.springframework.content.commons.annotations.HandleBeforeGetContent; +import org.springframework.content.commons.annotations.HandleBeforeSetContent; +import org.springframework.content.commons.annotations.HandleBeforeUnsetContent; +import org.springframework.content.commons.property.PropertyPath; +import org.springframework.content.rest.AfterGetContentEvent; +import org.springframework.content.rest.AfterSetContentEvent; +import org.springframework.content.rest.AfterUnsetContentEvent; +import org.springframework.content.rest.BeforeGetContentEvent; +import org.springframework.content.rest.BeforeSetContentEvent; +import org.springframework.content.rest.BeforeUnsetContentEvent; +import org.springframework.content.rest.StoreRestEventHandler; +import org.springframework.content.rest.config.RestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.core.io.WritableResource; +import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration; + +import com.github.paulcwarren.ginkgo4j.Ginkgo4jSpringRunner; + +import internal.org.springframework.content.rest.support.StoreConfig; +import internal.org.springframework.content.rest.support.TestEntity8; +import internal.org.springframework.content.rest.support.TestEntity8Repository; +import internal.org.springframework.content.rest.support.TestEntity8Store; +import lombok.Getter; + +@RunWith(Ginkgo4jSpringRunner.class) +// @Ginkgo4jConfiguration(threads=1) +@WebAppConfiguration +@ContextConfiguration(classes = { + StoreRestEventsIT.EventsConfig.class, + StoreConfig.class, + DelegatingWebMvcConfiguration.class, + RepositoryRestMvcConfiguration.class, + RestConfiguration.class }) +@Transactional +@ActiveProfiles("store") +public class StoreRestEventsIT { + + @Autowired private TestEntity8Repository repo; + @Autowired private TestEntity8Store store; + + private TestEntity8 testEntity8; + + @Autowired + private WebApplicationContext context; + + @Autowired + private TestEventHandler eventHandler; + + private MockMvc mvc; + + { + Describe("Store Rest Events", () -> { + BeforeEach(() -> { + mvc = MockMvcBuilders.webAppContextSetup(context).build(); + }); + Context("given an Entity with a content property", () -> { + BeforeEach(() -> { + testEntity8 = repo.save(new TestEntity8()); + }); + + Context("given that it has content", () -> { + BeforeEach(() -> { + String content = "Hello Spring Content World!"; + + testEntity8.getChild().contentMimeType = "text/plain"; + UUID contentId = UUID.randomUUID(); + store.associate(testEntity8, PropertyPath.from("child"), contentId); + WritableResource r = (WritableResource)store.getResource(testEntity8, PropertyPath.from("child")); + try (OutputStream out = r.getOutputStream()) { + out.write(content.getBytes()); + } + testEntity8 = repo.save(testEntity8); + }); + + Context("given the content property is accessed via the fully-qualified URL", () -> { + Context("a GET to /{repository}/{id}/{contentProperty}/{contentId}", () -> { + FIt("should return the content", () -> { + assertThat(eventHandler.isBeforeGetContentEvent(), is(false)); + assertThat(eventHandler.isAfterGetContentEvent(), is(false)); + + mvc.perform(get("/testEntity8s/" + testEntity8.getId() + "/child/content") + .accept("text/plain")) + .andExpect(status().isOk()) + .andExpect(header().string("etag", is("\"1\""))) + .andExpect(content().string(is("Hello Spring Content World!"))); + + assertThat(eventHandler.isBeforeGetContentEvent(), is(true)); + assertThat(eventHandler.isAfterGetContentEvent(), is(true)); + }); + }); + Context("a PUT to /{repository}/{id}/{contentProperty}/{contentId}", () -> { + FIt("should overwrite the content", () -> { + assertThat(eventHandler.isBeforeSetContentEvent(), is(false)); + assertThat(eventHandler.isAfterSetContentEvent(), is(false)); + + mvc.perform(put("/testEntity8s/" + + testEntity8.getId() + "/child/content") + .content("Hello Modified Spring Content World!") + .contentType("text/plain")) + .andExpect(status().isOk()); + + + Resource r = store.getResource(testEntity8, PropertyPath.from("child.content")); + try (InputStream actual = r.getInputStream()) { + assertThat(IOUtils.toString(actual), + is("Hello Modified Spring Content World!")); + } + + assertThat(eventHandler.isBeforeSetContentEvent(), is(true)); + assertThat(eventHandler.isAfterSetContentEvent(), is(true)); + }); + }); + Context("a DELETE to /{repository}/{id}/{contentProperty}/{contentId}", () -> { + FIt("should delete the content", () -> { + assertThat(eventHandler.isBeforeUnsetContentEvent(), is(false)); + assertThat(eventHandler.isAfterUnsetContentEvent(), is(false)); + + mvc.perform(delete("/testEntity8s/" + + testEntity8.getId() + "/child/content")) + .andExpect(status().isNoContent()); + + Optional fetched = repo.findById(testEntity8.getId()); + assertThat(fetched.isPresent(), is(true)); + assertThat(fetched.get().getChild().contentId, is(nullValue())); + assertThat(fetched.get().getChild().contentLen, is(0L)); + assertThat(fetched.get().getChild().contentMimeType, is(nullValue())); + + assertThat(eventHandler.isBeforeUnsetContentEvent(), is(true)); + assertThat(eventHandler.isAfterUnsetContentEvent(), is(true)); + }); + }); + }); + }); + }); + }); + } + + @Test + public void noop() { + } + + @Configuration + public static class EventsConfig { + + @Bean + public TestEventHandler eventHandler() { + return new TestEventHandler(); + } + } + + @Getter + @StoreRestEventHandler + public static class TestEventHandler { + + private boolean beforeGetContentEvent = false; + private boolean afterGetContentEvent = false; + + private boolean beforeSetContentEvent = false; + private boolean afterSetContentEvent = false; + + private boolean beforeUnsetContentEvent = false; + private boolean afterUnsetContentEvent = false; + + @HandleBeforeGetContent + public void onBeforeGetContent(BeforeGetContentEvent event) { + beforeGetContentEvent = true; + } + + @HandleAfterGetContent + public void onAfterGetContent(AfterGetContentEvent event) { + afterGetContentEvent = true; + } + + @HandleBeforeSetContent + public void onBeforeSetContent(BeforeSetContentEvent event) { + beforeSetContentEvent = true; + } + + @HandleAfterSetContent + public void onAfterSetContent(AfterSetContentEvent event) { + afterSetContentEvent = true; + } + + @HandleBeforeUnsetContent + public void onBeforeUnsetContent(BeforeUnsetContentEvent event) { + beforeUnsetContentEvent = true; + } + + @HandleAfterUnsetContent + public void onAfterUnsetContent(AfterUnsetContentEvent event) { + afterUnsetContentEvent = true; + } + } +} diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/storedrenditions/StoredRenditionsRestIT.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/storedrenditions/StoredRenditionsRestIT.java new file mode 100644 index 000000000..9ebaec955 --- /dev/null +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/storedrenditions/StoredRenditionsRestIT.java @@ -0,0 +1,205 @@ +package internal.org.springframework.content.rest.storedrenditions; + +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.BeforeEach; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Context; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Describe; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.It; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Optional; + +import org.apache.commons.io.IOUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.content.commons.annotations.HandleAfterGetContent; +import org.springframework.content.commons.annotations.HandleAfterSetContent; +import org.springframework.content.commons.annotations.HandleAfterUnsetContent; +import org.springframework.content.commons.annotations.HandleBeforeGetContent; +import org.springframework.content.commons.annotations.HandleBeforeSetContent; +import org.springframework.content.commons.annotations.HandleBeforeUnsetContent; +import org.springframework.content.commons.property.PropertyPath; +import org.springframework.content.rest.AfterGetContentEvent; +import org.springframework.content.rest.AfterSetContentEvent; +import org.springframework.content.rest.AfterUnsetContentEvent; +import org.springframework.content.rest.BeforeGetContentEvent; +import org.springframework.content.rest.BeforeSetContentEvent; +import org.springframework.content.rest.BeforeUnsetContentEvent; +import org.springframework.content.rest.StoreRestEventHandler; +import org.springframework.content.rest.config.RestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration; + +import com.github.paulcwarren.ginkgo4j.Ginkgo4jSpringRunner; + +import internal.org.springframework.content.rest.support.StoreConfig; +import internal.org.springframework.content.rest.support.TestEntity5; +import internal.org.springframework.content.rest.support.TestEntity5Repository; +import internal.org.springframework.content.rest.support.TestEntity5Store; +import lombok.Getter; + +@RunWith(Ginkgo4jSpringRunner.class) +// @Ginkgo4jConfiguration(threads=1) +@WebAppConfiguration +@ContextConfiguration(classes = { + StoredRenditionsRestIT.StoredRenditionsConfig.class, + StoreConfig.class, + DelegatingWebMvcConfiguration.class, + RepositoryRestMvcConfiguration.class, + RestConfiguration.class }) +@Transactional +@ActiveProfiles("store") +public class StoredRenditionsRestIT { + + @Autowired private TestEntity5Repository repo; + @Autowired private TestEntity5Store store; + + private TestEntity5 testEntity5; + + @Autowired + private WebApplicationContext context; + + @Autowired + private TestEventHandler eventHandler; + + private MockMvc mvc; + + { + Describe("Stored Renditions", () -> { + + BeforeEach(() -> { + mvc = MockMvcBuilders.webAppContextSetup(context).build(); + }); + + Context("given an Entity with a content property", () -> { + + BeforeEach(() -> { + testEntity5 = repo.save(new TestEntity5()); + }); + + Context("a PUT to /{repository}/{id}/{contentProperty}", () -> { + + BeforeEach(() -> { + mvc.perform(put("/testEntity5s/" + + testEntity5.getId() + "/content") + .content("Hello Spring Content World!") + .contentType("text/plain")) + .andExpect(status().is2xxSuccessful()); + }); + + It("should store the rendition", () -> { + + testEntity5 = repo.findById(testEntity5.getId()).get(); + Resource r = store.getResource(testEntity5, PropertyPath.from("content")); + try (InputStream actual = r.getInputStream()) { + assertThat(IOUtils.toString(actual), + is("Hello Spring Content World!")); + } + + r = store.getResource(testEntity5, PropertyPath.from("rendition")); + try (InputStream actual = r.getInputStream()) { + assertThat(IOUtils.toString(actual), + is("HELLO SPRING CONTENT WORLD!")); + } + }); + + Context("a DELETE to /{repository}/{id}/{contentProperty}", () -> { + + It("should also delete the rendition", () -> { + + Long id = testEntity5.getId(); + + mvc.perform(delete("/testEntity5s/" + + testEntity5.getId() + "/content")) + .andExpect(status().isNoContent()); + + Optional fetched = repo.findById(id); + assertThat(fetched.isPresent(), is(true)); + assertThat(fetched.get().getContentId(), is(nullValue())); + assertThat(fetched.get().getContentLen(), is(0L)); + assertThat(fetched.get().getContentMimeType(), is(nullValue())); + + assertThat(fetched.get().getRenditionId(), is(nullValue())); + assertThat(fetched.get().getRenditionLen(), is(0L)); + assertThat(fetched.get().getRenditionMimeType(), is(nullValue())); + }); + }); + }); + }); + }); + } + + @Test + public void noop() { + } + + @Configuration + public static class StoredRenditionsConfig { + + @Bean + public TestEventHandler eventHandler() { + return new TestEventHandler(); + } + } + + @Getter + @StoreRestEventHandler + public static class TestEventHandler { + + @Autowired + private TestEntity5Repository repo; + + @Autowired + private TestEntity5Store store; + + @HandleBeforeGetContent + public void onBeforeGetContent(BeforeGetContentEvent event) { + } + + @HandleAfterGetContent + public void onAfterGetContent(AfterGetContentEvent event) { + } + + @HandleBeforeSetContent + public void onBeforeSetContent(BeforeSetContentEvent event) { + } + + @HandleAfterSetContent + public void onAfterSetContent(AfterSetContentEvent event) throws IOException { + String content = IOUtils.toString(event.getResource().getInputStream()); + TestEntity5 entity = (TestEntity5) event.getActualSource(); + + entity = store.setContent(entity, PropertyPath.from("rendition"), new ByteArrayInputStream(content.toUpperCase().getBytes())); + entity = repo.save(entity); + } + + @HandleBeforeUnsetContent + public void onBeforeUnsetContent(BeforeUnsetContentEvent event) { + TestEntity5 entity = (TestEntity5) event.getActualSource(); + + store.unsetContent(entity, PropertyPath.from("rendition")); + } + + @HandleAfterUnsetContent + public void onAfterUnsetContent(AfterUnsetContentEvent event) { + } + } +}