Skip to content

Commit e225207

Browse files
Datastore auditing (spring-attic#1513)
fixes spring-attic#1113 Datastore auditing support
1 parent 40d8b67 commit e225207

File tree

14 files changed

+462
-43
lines changed

14 files changed

+462
-43
lines changed

docs/src/main/asciidoc/datastore.adoc

+55
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,61 @@ Spring Data Cloud Datastore publishes events extending the Spring Framework's `A
11991199
| `AfterDeleteEvent`|Published immediately after delete operations are executed by `DatastoreTemplate`| The keys sent to Cloud Datastore. The target entities, ID values, or entity type originally specified for the delete operation.
12001200
|===
12011201

1202+
=== Auditing
1203+
1204+
Spring Data Cloud Datastore supports the `@LastModifiedDate` and `@LastModifiedBy` auditing annotations for properties:
1205+
1206+
[source,java]
1207+
----
1208+
@Entity
1209+
public class SimpleEntity {
1210+
@Id
1211+
String id;
1212+
1213+
@LastModifiedBy
1214+
String lastUser;
1215+
1216+
@LastModifiedDate
1217+
DateTime lastTouched;
1218+
}
1219+
----
1220+
1221+
Upon insert, update, or save, these properties will be set automatically by the framework before Datastore entities are generated and saved to Cloud Datastore.
1222+
1223+
To take advantage of these features, add the `@EnableDatastoreAuditing` annotation to your configuration class and provide a bean for an `AuditorAware<A>` implementation where the type `A` is the desired property type annotated by `@LastModifiedBy`:
1224+
1225+
[source,java]
1226+
----
1227+
@Configuration
1228+
@EnableDatastoreAuditing
1229+
public class Config {
1230+
1231+
@Bean
1232+
public AuditorAware<String> auditorProvider() {
1233+
return () -> Optional.of("YOUR_USERNAME_HERE");
1234+
}
1235+
}
1236+
----
1237+
1238+
The `AuditorAware` interface contains a single method that supplies the value for fields annotated by `@LastModifiedBy` and can be of any type.
1239+
One alternative is to use Spring Security's `User` type:
1240+
1241+
[source,java]
1242+
----
1243+
class SpringSecurityAuditorAware implements AuditorAware<User> {
1244+
1245+
public Optional<User> getCurrentAuditor() {
1246+
1247+
return Optional.ofNullable(SecurityContextHolder.getContext())
1248+
.map(SecurityContext::getAuthentication)
1249+
.filter(Authentication::isAuthenticated)
1250+
.map(Authentication::getPrincipal)
1251+
.map(User.class::cast);
1252+
}
1253+
}
1254+
----
1255+
1256+
You can also set a custom provider for properties annotated `@LastModifiedDate` by providing a bean for `DateTimeProvider` and providing the bean name to `@EnableDatastoreAuditing(dateTimeProviderRef = "customDateTimeProviderBean")`.
12021257

12031258
=== Sample
12041259

docs/src/main/asciidoc/spanner.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1323,7 +1323,7 @@ public class Config {
13231323
}
13241324
----
13251325

1326-
The `AuditorAware` interface contains a single method that supplies the value for fields annotated by `@LastModofiedBy` and can be of any type.
1326+
The `AuditorAware` interface contains a single method that supplies the value for fields annotated by `@LastModifiedBy` and can be of any type.
13271327
One alternative is to use Spring Security's `User` type:
13281328

13291329
[source,java]

spring-cloud-gcp-data-datastore/src/main/java/org/springframework/cloud/gcp/data/datastore/core/DatastoreTemplate.java

+6-5
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,13 @@ public <T> T findById(Object id, Class<T> entityClass) {
141141
@Override
142142
public <T> T save(T instance, Key... ancestors) {
143143
List<T> instances = Collections.singletonList(instance);
144-
saveEntities(instances, getEntitiesForSave(instances, new HashSet<>(), ancestors));
144+
saveEntities(instances, ancestors);
145145
return instance;
146146
}
147147

148148
@Override
149149
public <T> Iterable<T> saveAll(Iterable<T> entities, Key... ancestors) {
150-
saveEntities((List<T>) entities, getEntitiesForSave(entities, new HashSet<>(), ancestors));
150+
saveEntities((List<T>) entities, ancestors);
151151
return entities;
152152
}
153153

@@ -163,9 +163,10 @@ private <T> List<Entity> getEntitiesForSave(Iterable<T> entities, Set<Key> persi
163163
return entitiesForSave;
164164
}
165165

166-
private <T> void saveEntities(List<T> instances, List<Entity> entities) {
167-
if (!entities.isEmpty()) {
168-
maybeEmitEvent(new BeforeSaveEvent(entities, instances));
166+
private <T> void saveEntities(List<T> instances, Key[] ancestors) {
167+
if (!instances.isEmpty()) {
168+
maybeEmitEvent(new BeforeSaveEvent(instances));
169+
List<Entity> entities = getEntitiesForSave(instances, new HashSet<>(), ancestors);
169170
getDatastoreReadWriter().put(entities.toArray(new Entity[0]));
170171
maybeEmitEvent(new AfterSaveEvent(entities, instances));
171172
}

spring-cloud-gcp-data-datastore/src/main/java/org/springframework/cloud/gcp/data/datastore/core/mapping/event/AfterSaveEvent.java

+32-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.cloud.gcp.data.datastore.core.mapping.event;
1818

1919
import java.util.List;
20+
import java.util.Objects;
2021

2122
import com.google.cloud.datastore.Entity;
2223

@@ -26,6 +27,9 @@
2627
* @author Chengyuan Zhao
2728
*/
2829
public class AfterSaveEvent extends SaveEvent {
30+
31+
private final List<Entity> datastoreEntities;
32+
2933
/**
3034
* Constructor.
3135
*
@@ -34,6 +38,33 @@ public class AfterSaveEvent extends SaveEvent {
3438
* @param javaEntities The original Java entities being saved. Each entity may result in
3539
*/
3640
public AfterSaveEvent(List<Entity> datastoreEntities, List javaEntities) {
37-
super(datastoreEntities, javaEntities);
41+
super(javaEntities);
42+
this.datastoreEntities = datastoreEntities;
43+
}
44+
45+
/**
46+
* Get the Cloud Datastore entities that were saved.
47+
* @return The entities that were saved in Cloud Datastore.
48+
*/
49+
public List<Entity> getDatastoreEntities() {
50+
return this.datastoreEntities;
51+
}
52+
53+
@Override
54+
public boolean equals(Object o) {
55+
if (this == o) {
56+
return true;
57+
}
58+
if (o == null || getClass() != o.getClass()) {
59+
return false;
60+
}
61+
AfterSaveEvent that = (AfterSaveEvent) o;
62+
return getDatastoreEntities().containsAll(that.getDatastoreEntities())
63+
&& Objects.equals(getTargetEntities(), that.getTargetEntities());
64+
}
65+
66+
@Override
67+
public int hashCode() {
68+
return Objects.hash(getDatastoreEntities(), getTargetEntities());
3869
}
3970
}

spring-cloud-gcp-data-datastore/src/main/java/org/springframework/cloud/gcp/data/datastore/core/mapping/event/BeforeSaveEvent.java

+2-6
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818

1919
import java.util.List;
2020

21-
import com.google.cloud.datastore.Entity;
22-
2321
/**
2422
* An event published immediately before a save event to Cloud Datastore.
2523
*
@@ -29,11 +27,9 @@ public class BeforeSaveEvent extends SaveEvent {
2927
/**
3028
* Constructor.
3129
*
32-
* @param datastoreEntities The Cloud Datastore entities that are being saved. These
33-
* include any referenced or descendant entities of the original entities being saved.
3430
* @param javaEntities The original Java entities being saved. Each entity may result in
3531
*/
36-
public BeforeSaveEvent(List<Entity> datastoreEntities, List javaEntities) {
37-
super(datastoreEntities, javaEntities);
32+
public BeforeSaveEvent(List javaEntities) {
33+
super(javaEntities);
3834
}
3935
}

spring-cloud-gcp-data-datastore/src/main/java/org/springframework/cloud/gcp/data/datastore/core/mapping/event/SaveEvent.java

+7-25
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@
1919
import java.util.List;
2020
import java.util.Objects;
2121

22-
import com.google.cloud.datastore.Entity;
23-
2422
import org.springframework.context.ApplicationEvent;
25-
import org.springframework.util.Assert;
2623

2724
/**
2825
* An event published when entities are saved to Cloud Datastore.
@@ -31,36 +28,22 @@
3128
*/
3229
public class SaveEvent extends ApplicationEvent {
3330

34-
private final List javaEntities;
35-
3631
/**
3732
* Constructor.
3833
*
39-
* @param datastoreEntities The Cloud Datastore entities that are being saved. These
40-
* include any referenced or descendant entities of the original entities being saved.
41-
* @param javaEntities The original Java entities being saved. Each entity may result in
34+
* @param entities The original Java entities being saved. Each entity may result in
4235
* multiple Datastore entities being saved due to relationships.
4336
*/
44-
public SaveEvent(List<Entity> datastoreEntities, List javaEntities) {
45-
super(datastoreEntities);
46-
Assert.notNull(javaEntities, "The entities being saved must not be null.");
47-
this.javaEntities = javaEntities;
48-
}
49-
50-
/**
51-
* Get the Cloud Datastore entities that were saved.
52-
* @return The entities that were saved in Cloud Datastore.
53-
*/
54-
public List<Entity> getDatastoreEntities() {
55-
return (List<Entity>) getSource();
37+
public SaveEvent(List entities) {
38+
super(entities);
5639
}
5740

5841
/**
5942
* Get the original Java objects that were saved.
6043
* @return The original Java objects that were saved to Cloud Datastore.
6144
*/
62-
public List getJavaEntities() {
63-
return this.javaEntities;
45+
public List getTargetEntities() {
46+
return (List) getSource();
6447
}
6548

6649
@Override
@@ -72,12 +55,11 @@ public boolean equals(Object o) {
7255
return false;
7356
}
7457
SaveEvent that = (SaveEvent) o;
75-
return getDatastoreEntities().containsAll(that.getDatastoreEntities())
76-
&& Objects.equals(getJavaEntities(), that.getJavaEntities());
58+
return Objects.equals(getTargetEntities(), that.getTargetEntities());
7759
}
7860

7961
@Override
8062
public int hashCode() {
81-
return Objects.hash(getDatastoreEntities(), getJavaEntities());
63+
return Objects.hash(getTargetEntities());
8264
}
8365
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2017-2019 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+
* http://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+
17+
package org.springframework.cloud.gcp.data.datastore.repository.config;
18+
19+
import java.lang.annotation.Annotation;
20+
21+
import org.springframework.beans.factory.config.BeanDefinition;
22+
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
23+
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
24+
import org.springframework.cloud.gcp.data.datastore.repository.support.DatastoreAuditingEventListener;
25+
import org.springframework.data.auditing.AuditingHandler;
26+
import org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport;
27+
import org.springframework.data.auditing.config.AuditingConfiguration;
28+
29+
/**
30+
* Registers the annotations and classes for providing auditing support in Spring Data
31+
* Cloud Datastore.
32+
*
33+
* @author Chengyuan Zhao
34+
* @since 1.2
35+
*/
36+
public class DatastoreAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport {
37+
38+
private static final String AUDITING_HANDLER_BEAN_NAME = "datastoreAuditingHandler";
39+
40+
private static final String MAPPING_CONTEXT_BEAN_NAME = "datastoreMappingContext";
41+
42+
@Override
43+
protected Class<? extends Annotation> getAnnotation() {
44+
return EnableDatastoreAuditing.class;
45+
}
46+
47+
@Override
48+
protected void registerAuditListenerBeanDefinition(BeanDefinition auditingHandlerDefinition,
49+
BeanDefinitionRegistry registry) {
50+
Class<?> listenerClass = DatastoreAuditingEventListener.class;
51+
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(listenerClass)
52+
.addConstructorArgReference(AUDITING_HANDLER_BEAN_NAME);
53+
54+
registerInfrastructureBeanWithId(builder.getRawBeanDefinition(), listenerClass.getName(), registry);
55+
}
56+
57+
@Override
58+
protected BeanDefinitionBuilder getAuditHandlerBeanDefinitionBuilder(AuditingConfiguration configuration) {
59+
BeanDefinitionBuilder builder = configureDefaultAuditHandlerAttributes(configuration,
60+
BeanDefinitionBuilder.rootBeanDefinition(AuditingHandler.class));
61+
return builder.addConstructorArgReference(MAPPING_CONTEXT_BEAN_NAME);
62+
}
63+
64+
@Override
65+
protected String getAuditingHandlerBeanName() {
66+
return AUDITING_HANDLER_BEAN_NAME;
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2017-2019 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+
* http://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+
17+
package org.springframework.cloud.gcp.data.datastore.repository.config;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Inherited;
22+
import java.lang.annotation.Retention;
23+
import java.lang.annotation.RetentionPolicy;
24+
import java.lang.annotation.Target;
25+
26+
import org.springframework.context.annotation.Import;
27+
import org.springframework.data.auditing.DateTimeProvider;
28+
import org.springframework.data.domain.AuditorAware;
29+
30+
/**
31+
* The annotation used to activate auditing functionality.
32+
*
33+
* @author Chengyuan Zhao
34+
* @since 1.2
35+
*/
36+
@Inherited
37+
@Documented
38+
@Target(ElementType.TYPE)
39+
@Retention(RetentionPolicy.RUNTIME)
40+
@Import(DatastoreAuditingRegistrar.class)
41+
public @interface EnableDatastoreAuditing {
42+
43+
/**
44+
* Configures a {@link AuditorAware} bean to be used to lookup the current principal.
45+
*
46+
* @return the name of a custom auditor provider. If blank then one will be looked up bean
47+
* type.
48+
*/
49+
String auditorAwareRef() default "";
50+
51+
/**
52+
* Configures whether the creation and modification dates are set. Defaults to
53+
* {@literal true}.
54+
*
55+
* @return whether dates are set by the auditing functionality.
56+
*/
57+
boolean setDates() default true;
58+
59+
/**
60+
* Configures whether the entity shall be marked as modified on creation. Defaults to
61+
* {@literal true}.
62+
*
63+
* @return whether an entity is marked as modified when it is created.
64+
*/
65+
boolean modifyOnCreate() default true;
66+
67+
/**
68+
* Configures a {@link DateTimeProvider} bean name that allows customizing the
69+
* {@link org.joda.time.DateTime} to be used for setting creation and modification dates.
70+
*
71+
* @return the name of the custom time provider. If blank then one will be looked up bean
72+
* type.
73+
*/
74+
String dateTimeProviderRef() default "";
75+
}

0 commit comments

Comments
 (0)