1 /*
2  * Copyright 2014-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.auditing;
17
18 import java.lang.annotation.Annotation;
19 import java.time.temporal.TemporalAccessor;
20 import java.util.Map;
21 import java.util.Optional;
22 import java.util.function.Predicate;
23 import java.util.stream.Stream;
24
25 import org.springframework.core.convert.ConversionService;
26 import org.springframework.data.annotation.CreatedBy;
27 import org.springframework.data.annotation.CreatedDate;
28 import org.springframework.data.annotation.LastModifiedBy;
29 import org.springframework.data.annotation.LastModifiedDate;
30 import org.springframework.data.domain.Auditable;
31 import org.springframework.data.mapping.AccessOptions;
32 import org.springframework.data.mapping.AccessOptions.SetOptions;
33 import org.springframework.data.mapping.AccessOptions.SetOptions.Propagation;
34 import org.springframework.data.mapping.PersistentEntity;
35 import org.springframework.data.mapping.PersistentProperty;
36 import org.springframework.data.mapping.PersistentPropertyAccessor;
37 import org.springframework.data.mapping.PersistentPropertyPathAccessor;
38 import org.springframework.data.mapping.PersistentPropertyPaths;
39 import org.springframework.data.mapping.context.MappingContext;
40 import org.springframework.data.mapping.context.PersistentEntities;
41 import org.springframework.data.util.Lazy;
42 import org.springframework.util.Assert;
43 import org.springframework.util.ConcurrentReferenceHashMap;
44
45 /**
46  * {@link AuditableBeanWrapperFactory} that will create am {@link AuditableBeanWrapper} using mapping information
47  * obtained from a {@link MappingContext} to detect auditing configuration and eventually invoking setting the auditing
48  * values.
49  *
50  * @author Oliver Gierke
51  * @author Christoph Strobl
52  * @author Pavel Horal
53  * @since 1.8
54  */

55 public class MappingAuditableBeanWrapperFactory extends DefaultAuditableBeanWrapperFactory {
56
57     private final PersistentEntities entities;
58     private final Map<Class<?>, MappingAuditingMetadata> metadataCache;
59
60     /**
61      * Creates a new {@link MappingAuditableBeanWrapperFactory} using the given {@link PersistentEntities}.
62      *
63      * @param entities must not be {@literal null}.
64      */

65     public MappingAuditableBeanWrapperFactory(PersistentEntities entities) {
66
67         Assert.notNull(entities, "PersistentEntities must not be null!");
68
69         this.entities = entities;
70         this.metadataCache = new ConcurrentReferenceHashMap<>();
71     }
72
73     /*
74      * (non-Javadoc)
75      * @see org.springframework.data.auditing.AuditableBeanWrapperFactory#getBeanWrapperFor(java.lang.Object)
76      */

77     @Override
78     public <T> Optional<AuditableBeanWrapper<T>> getBeanWrapperFor(T source) {
79
80         return Optional.of(source).flatMap(it -> {
81
82             if (it instanceof Auditable) {
83                 return super.getBeanWrapperFor(source);
84             }
85
86             return entities.mapOnContext(it.getClass(), (context, entity) -> {
87
88                 MappingAuditingMetadata metadata = metadataCache.computeIfAbsent(it.getClass(),
89                         key -> new MappingAuditingMetadata(context, it.getClass()));
90
91                 return Optional.<AuditableBeanWrapper<T>> ofNullable(metadata.isAuditable() //
92                         ? new MappingMetadataAuditableBeanWrapper<>(getConversionService(), entity.getPropertyPathAccessor(it),
93                                 metadata)
94                         : null);
95
96             }).orElseGet(() -> super.getBeanWrapperFor(source));
97         });
98     }
99
100     /**
101      * Captures {@link PersistentProperty} instances equipped with auditing annotations.
102      *
103      * @author Oliver Gierke
104      * @since 1.8
105      */

106     static class MappingAuditingMetadata {
107
108         private static final Predicate<? super PersistentProperty<?>> HAS_COLLECTION_PROPERTY = it -> it.isCollectionLike()
109                 || it.isMap();
110
111         private final PersistentPropertyPaths<?, ? extends PersistentProperty<?>> createdByPaths, createdDatePaths,
112                 lastModifiedByPaths, lastModifiedDatePaths;
113
114         private final Lazy<Boolean> isAuditable;
115
116         /**
117          * Creates a new {@link MappingAuditingMetadata} instance from the given {@link PersistentEntity}.
118          *
119          * @param entity must not be {@literal null}.
120          */

121         public <P> MappingAuditingMetadata(MappingContext<?, ? extends PersistentProperty<?>> context, Class<?> type) {
122
123             Assert.notNull(type, "Type must not be null!");
124
125             this.createdByPaths = findPropertyPaths(type, CreatedBy.class, context);
126             this.createdDatePaths = findPropertyPaths(type, CreatedDate.class, context);
127             this.lastModifiedByPaths = findPropertyPaths(type, LastModifiedBy.class, context);
128             this.lastModifiedDatePaths = findPropertyPaths(type, LastModifiedDate.class, context);
129
130             this.isAuditable = Lazy.of( //
131                     () -> //
132                     Stream.of(createdByPaths, createdDatePaths, lastModifiedByPaths, lastModifiedDatePaths) //
133                             .anyMatch(it -> !it.isEmpty())//
134             );
135         }
136
137         /**
138          * Returns whether the {@link PersistentEntity} is auditable at all (read: any of the auditing annotations is
139          * present).
140          *
141          * @return
142          */

143         public boolean isAuditable() {
144             return isAuditable.get();
145         }
146
147         private PersistentPropertyPaths<?, ? extends PersistentProperty<?>> findPropertyPaths(Class<?> type,
148                 Class<? extends Annotation> annotation, MappingContext<?, ? extends PersistentProperty<?>> context) {
149
150             return context //
151                     .findPersistentPropertyPaths(type, withAnnotation(annotation)) //
152                     .dropPathIfSegmentMatches(HAS_COLLECTION_PROPERTY);
153         }
154
155         private static Predicate<PersistentProperty<?>> withAnnotation(Class<? extends Annotation> type) {
156             return t -> t.findAnnotation(type) != null;
157         }
158     }
159
160     /**
161      * {@link AuditableBeanWrapper} using {@link MappingAuditingMetadata} and a {@link PersistentPropertyAccessor} to set
162      * values on auditing properties.
163      *
164      * @author Oliver Gierke
165      * @since 1.8
166      */

167     static class MappingMetadataAuditableBeanWrapper<T> extends DateConvertingAuditableBeanWrapper<T> {
168
169         private static final SetOptions OPTIONS = AccessOptions.defaultSetOptions() //
170                 .skipNulls() // ;
171                 .withCollectionAndMapPropagation(Propagation.SKIP);
172
173         private final PersistentPropertyPathAccessor<T> accessor;
174         private final MappingAuditingMetadata metadata;
175
176         /**
177          * Creates a new {@link MappingMetadataAuditableBeanWrapper} for the given target and
178          * {@link MappingAuditingMetadata}.
179          *
180          * @param accessor must not be {@literal null}.
181          * @param metadata must not be {@literal null}.
182          */

183         public MappingMetadataAuditableBeanWrapper(
184                 ConversionService conversionService,
185                 PersistentPropertyPathAccessor<T> accessor,
186                 MappingAuditingMetadata metadata) {
187             super(conversionService);
188
189             Assert.notNull(accessor, "PersistentPropertyAccessor must not be null!");
190             Assert.notNull(metadata, "Auditing metadata must not be null!");
191
192             this.accessor = accessor;
193             this.metadata = metadata;
194         }
195
196         /*
197          * (non-Javadoc)
198          * @see org.springframework.data.auditing.AuditableBeanWrapper#setCreatedBy(java.lang.Object)
199          */

200         @Override
201         public Object setCreatedBy(Object value) {
202             return setProperty(metadata.createdByPaths, value);
203         }
204
205         /*
206          * (non-Javadoc)
207          * @see org.springframework.data.auditing.AuditableBeanWrapper#setCreatedDate(java.util.Optional)
208          */

209         @Override
210         public TemporalAccessor setCreatedDate(TemporalAccessor value) {
211             return setDateProperty(metadata.createdDatePaths, value);
212         }
213
214         /*
215          * (non-Javadoc)
216          * @see org.springframework.data.auditing.AuditableBeanWrapper#setLastModifiedBy(java.util.Optional)
217          */

218         @Override
219         public Object setLastModifiedBy(Object value) {
220             return setProperty(metadata.lastModifiedByPaths, value);
221         }
222
223         /*
224          * (non-Javadoc)
225          * @see org.springframework.data.auditing.AuditableBeanWrapper#getLastModifiedDate()
226          */

227         @Override
228         public Optional<TemporalAccessor> getLastModifiedDate() {
229
230             Optional<Object> firstValue = metadata.lastModifiedDatePaths.getFirst() //
231                     .map(accessor::getProperty);
232
233             return getAsTemporalAccessor(firstValue, TemporalAccessor.class);
234         }
235
236         /*
237          * (non-Javadoc)
238          * @see org.springframework.data.auditing.AuditableBeanWrapper#setLastModifiedDate(java.util.Optional)
239          */

240         @Override
241         public TemporalAccessor setLastModifiedDate(TemporalAccessor value) {
242             return setDateProperty(metadata.lastModifiedDatePaths, value);
243         }
244
245         /*
246          * (non-Javadoc)
247          * @see org.springframework.data.auditing.AuditableBeanWrapper#getBean()
248          */

249         @Override
250         public T getBean() {
251             return accessor.getBean();
252         }
253
254         private <S> S setProperty(
255                 PersistentPropertyPaths<?, ? extends PersistentProperty<?>> paths, S value) {
256
257             paths.forEach(it -> this.accessor.setProperty(it, value, OPTIONS));
258
259             return value;
260         }
261
262         private TemporalAccessor setDateProperty(
263                 PersistentPropertyPaths<?, ? extends PersistentProperty<?>> property, TemporalAccessor value) {
264
265             property.forEach(it -> {
266
267                 Class<?> type = it.getRequiredLeafProperty().getType();
268
269                 this.accessor.setProperty(it, getDateValueToSet(value, type, accessor.getBean()), OPTIONS);
270             });
271
272             return value;
273         }
274     }
275 }
276