1 /*
2  * Copyright 2016-2020 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      https://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16 package org.springframework.data.repository.core.support;
17
18 import lombok.RequiredArgsConstructor;
19
20 import java.lang.annotation.Annotation;
21 import java.lang.reflect.Method;
22 import java.util.Collection;
23 import java.util.Collections;
24 import java.util.Map;
25 import java.util.function.Supplier;
26
27 import org.aopalliance.intercept.MethodInterceptor;
28 import org.aopalliance.intercept.MethodInvocation;
29 import org.springframework.aop.framework.ProxyFactory;
30 import org.springframework.context.ApplicationEventPublisher;
31 import org.springframework.data.domain.AfterDomainEventPublication;
32 import org.springframework.data.domain.DomainEvents;
33 import org.springframework.data.repository.CrudRepository;
34 import org.springframework.data.repository.core.RepositoryInformation;
35 import org.springframework.data.util.AnnotationDetectionMethodCallback;
36 import org.springframework.lang.Nullable;
37 import org.springframework.util.Assert;
38 import org.springframework.util.ConcurrentReferenceHashMap;
39 import org.springframework.util.ReflectionUtils;
40
41 /**
42  * {@link RepositoryProxyPostProcessor} to register a {@link MethodInterceptor} to intercept the
43  * {@link CrudRepository#save(Object)} method and publish events potentially exposed via a method annotated with
44  * {@link DomainEvents}. If no such method can be detected on the aggregate root, no interceptor is added. Additionally,
45  * the aggregate root can expose a method annotated with {@link AfterDomainEventPublication}. If present, the method
46  * will be invoked after all events have been published.
47  *
48  * @author Oliver Gierke
49  * @author Christoph Strobl
50  * @author Yuki Yoshida
51  * @since 1.13
52  * @soundtrack Henrik Freischlader Trio - Master Plan (Openness)
53  */

54 @RequiredArgsConstructor
55 public class EventPublishingRepositoryProxyPostProcessor implements RepositoryProxyPostProcessor {
56
57     private final ApplicationEventPublisher publisher;
58
59     /*
60      * (non-Javadoc)
61      * @see org.springframework.data.repository.core.support.RepositoryProxyPostProcessor#postProcess(org.springframework.aop.framework.ProxyFactory, org.springframework.data.repository.core.RepositoryInformation)
62      */

63     @Override
64     public void postProcess(ProxyFactory factory, RepositoryInformation repositoryInformation) {
65
66         EventPublishingMethod method = EventPublishingMethod.of(repositoryInformation.getDomainType());
67
68         if (method == null) {
69             return;
70         }
71
72         factory.addAdvice(new EventPublishingMethodInterceptor(method, publisher));
73     }
74
75     /**
76      * {@link MethodInterceptor} to publish events exposed an aggregate on calls to a save method on the repository.
77      *
78      * @author Oliver Gierke
79      * @since 1.13
80      */

81     @RequiredArgsConstructor(staticName = "of")
82     static class EventPublishingMethodInterceptor implements MethodInterceptor {
83
84         private final EventPublishingMethod eventMethod;
85         private final ApplicationEventPublisher publisher;
86
87         /*
88          * (non-Javadoc)
89          * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
90          */

91         @Override
92         public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable {
93
94             Object[] arguments = invocation.getArguments();
95             Object result = invocation.proceed();
96
97             if (!invocation.getMethod().getName().startsWith("save")) {
98                 return result;
99             }
100
101             Object eventSource = arguments.length == 1 ? arguments[0] : result;
102
103             eventMethod.publishEventsFrom(eventSource, publisher);
104
105             return result;
106         }
107     }
108
109     /**
110      * Abstraction of a method on the aggregate root that exposes the events to publish.
111      *
112      * @author Oliver Gierke
113      * @since 1.13
114      */

115     @RequiredArgsConstructor
116     static class EventPublishingMethod {
117
118         private static Map<Class<?>, EventPublishingMethod> CACHE = new ConcurrentReferenceHashMap<>();
119         private static @SuppressWarnings("null") EventPublishingMethod NONE = new EventPublishingMethod(nullnull);
120
121         private final Method publishingMethod;
122         private final @Nullable Method clearingMethod;
123
124         /**
125          * Creates an {@link EventPublishingMethod} for the given type.
126          *
127          * @param type must not be {@literal null}.
128          * @return an {@link EventPublishingMethod} for the given type or {@literal null} in case the given type does not
129          *         expose an event publishing method.
130          */

131         @Nullable
132         public static EventPublishingMethod of(Class<?> type) {
133
134             Assert.notNull(type, "Type must not be null!");
135
136             EventPublishingMethod eventPublishingMethod = CACHE.get(type);
137
138             if (eventPublishingMethod != null) {
139                 return eventPublishingMethod.orNull();
140             }
141
142             EventPublishingMethod result = from(getDetector(type, DomainEvents.class),
143                     () -> getDetector(type, AfterDomainEventPublication.class));
144
145             CACHE.put(type, result);
146
147             return result.orNull();
148         }
149
150         /**
151          * Publishes all events in the given aggregate root using the given {@link ApplicationEventPublisher}.
152          *
153          * @param object can be {@literal null}.
154          * @param publisher must not be {@literal null}.
155          */

156         public void publishEventsFrom(@Nullable Object object, ApplicationEventPublisher publisher) {
157
158             if (object == null) {
159                 return;
160             }
161
162             for (Object aggregateRoot : asCollection(object)) {
163
164                 for (Object event : asCollection(ReflectionUtils.invokeMethod(publishingMethod, aggregateRoot))) {
165                     publisher.publishEvent(event);
166                 }
167
168                 if (clearingMethod != null) {
169                     ReflectionUtils.invokeMethod(clearingMethod, aggregateRoot);
170                 }
171             }
172         }
173
174         /**
175          * Returns the current {@link EventPublishingMethod} or {@literal nullif it's the default value.
176          *
177          * @return
178          */

179         @Nullable
180         private EventPublishingMethod orNull() {
181             return this == EventPublishingMethod.NONE ? null : this;
182         }
183
184         private static <T extends Annotation> AnnotationDetectionMethodCallback<T> getDetector(Class<?> type,
185                 Class<T> annotation) {
186
187             AnnotationDetectionMethodCallback<T> callback = new AnnotationDetectionMethodCallback<>(annotation);
188             ReflectionUtils.doWithMethods(type, callback);
189
190             return callback;
191         }
192
193         /**
194          * Creates a new {@link EventPublishingMethod} using the given pre-populated
195          * {@link AnnotationDetectionMethodCallback} looking up an optional clearing method from the given callback.
196          *
197          * @param publishing must not be {@literal null}.
198          * @param clearing must not be {@literal null}.
199          * @return
200          */

201         private static EventPublishingMethod from(AnnotationDetectionMethodCallback<?> publishing,
202                 Supplier<AnnotationDetectionMethodCallback<?>> clearing) {
203
204             if (!publishing.hasFoundAnnotation()) {
205                 return EventPublishingMethod.NONE;
206             }
207
208             Method eventMethod = publishing.getRequiredMethod();
209             ReflectionUtils.makeAccessible(eventMethod);
210
211             return new EventPublishingMethod(eventMethod, getClearingMethod(clearing.get()));
212         }
213
214         /**
215          * Returns the {@link Method} supposed to be invoked for event clearing or {@literal nullif none is found.
216          *
217          * @param clearing must not be {@literal null}.
218          * @return
219          */

220         @Nullable
221         private static Method getClearingMethod(AnnotationDetectionMethodCallback<?> clearing) {
222
223             if (!clearing.hasFoundAnnotation()) {
224                 return null;
225             }
226
227             Method method = clearing.getRequiredMethod();
228             ReflectionUtils.makeAccessible(method);
229
230             return method;
231         }
232
233         /**
234          * Returns the given source object as collection, i.e. collections are returned as is, objects are turned into a
235          * one-element collection, {@literal null} will become an empty collection.
236          *
237          * @param source can be {@literal null}.
238          * @return
239          */

240         @SuppressWarnings("unchecked")
241         private static Collection<Object> asCollection(@Nullable Object source) {
242
243             if (source == null) {
244                 return Collections.emptyList();
245             }
246
247             if (Collection.class.isInstance(source)) {
248                 return (Collection<Object>) source;
249             }
250
251             return Collections.singletonList(source);
252         }
253     }
254 }
255