diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/auditing/AuditedPerson.java b/cassandra/example/src/main/java/example/springdata/cassandra/auditing/AuditedPerson.java new file mode 100644 index 00000000..b8e517d5 --- /dev/null +++ b/cassandra/example/src/main/java/example/springdata/cassandra/auditing/AuditedPerson.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.cassandra.auditing; + +import lombok.Data; + +import java.time.Instant; + +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.annotation.Transient; +import org.springframework.data.cassandra.core.mapping.Table; +import org.springframework.data.domain.Persistable; + +/** + * Simple domain object that declares properties annotated with Spring Data's annotations for auditing. + * + * @author Mark Paluch + */ +@Data +@Table +public class AuditedPerson implements Persistable { + + @Id Long id; + + @CreatedBy String createdBy; + + @LastModifiedBy String lastModifiedBy; + + @CreatedDate Instant createdDate; + + @LastModifiedDate Instant lastModifiedDate; + + @Transient boolean isNew; + + @Override + public boolean isNew() { + return isNew; + } +} diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/auditing/AuditedPersonRepository.java b/cassandra/example/src/main/java/example/springdata/cassandra/auditing/AuditedPersonRepository.java new file mode 100644 index 00000000..f6a3c92a --- /dev/null +++ b/cassandra/example/src/main/java/example/springdata/cassandra/auditing/AuditedPersonRepository.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.cassandra.auditing; + +import org.springframework.data.repository.CrudRepository; + +/** + * Simple repository interface for {@link AuditedPerson} instances. + * + * @author Mark Paluch + */ +public interface AuditedPersonRepository extends CrudRepository {} diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/auditing/BasicConfiguration.java b/cassandra/example/src/main/java/example/springdata/cassandra/auditing/BasicConfiguration.java new file mode 100644 index 00000000..835e927c --- /dev/null +++ b/cassandra/example/src/main/java/example/springdata/cassandra/auditing/BasicConfiguration.java @@ -0,0 +1,69 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.cassandra.auditing; + +import java.util.Optional; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.cassandra.config.EnableCassandraAuditing; +import org.springframework.data.cassandra.core.convert.CassandraCustomConversions; +import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.core.mapping.SimpleUserTypeResolver; +import org.springframework.data.domain.AuditorAware; + +import com.datastax.oss.driver.api.core.CqlSession; + +/** + * Basic {@link Configuration} to create the necessary schema for the {@link AuditedPerson} table. + * + * @author Mark Paluch + */ +@SpringBootApplication +@EntityScan(basePackageClasses = AuditedPerson.class) +@EnableCassandraAuditing +class BasicConfiguration { + + /** + * {@code @Bean} method defining a supplier for an auditor. This could be also an integration with a security + * framework such as Spring Security. + */ + @Bean + AuditorAware auditorAware() { + return () -> Optional.of("Some user"); + } + + /** + * {@code @Bean} method defining a {@link MappingCassandraConverter} as currently the auditing requires a bean + * definition for {@link MappingCassandraConverter}. + */ + @Bean + public MappingCassandraConverter cassandraConverter(CassandraMappingContext mapping, + CassandraCustomConversions conversions, CqlSession session) { + + MappingCassandraConverter converter = new MappingCassandraConverter(mapping); + + converter.setCodecRegistry(session.getContext().getCodecRegistry()); + converter.setCustomConversions(conversions); + converter.setUserTypeResolver(new SimpleUserTypeResolver(session)); + + return converter; + } + +} diff --git a/cassandra/example/src/test/java/example/springdata/cassandra/auditing/AuditedPersonRepositoryTests.java b/cassandra/example/src/test/java/example/springdata/cassandra/auditing/AuditedPersonRepositoryTests.java new file mode 100644 index 00000000..7de35346 --- /dev/null +++ b/cassandra/example/src/test/java/example/springdata/cassandra/auditing/AuditedPersonRepositoryTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.cassandra.auditing; + +import static org.assertj.core.api.Assertions.*; + +import example.springdata.cassandra.util.CassandraKeyspace; + +import java.time.Duration; +import java.time.Instant; + +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration test showing the basic usage of {@link AuditedPersonRepository}. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = BasicConfiguration.class) +public class AuditedPersonRepositoryTests { + + @ClassRule public final static CassandraKeyspace CASSANDRA_KEYSPACE = CassandraKeyspace.onLocalhost(); + + @Autowired AuditedPersonRepository repository; + + @Before + public void setUp() { + repository.deleteAll(); + } + + /** + * Saving an object using the Cassandra Repository will create a persistent representation of the object in Cassandra. + */ + @Test + public void insertShouldSetCreatedDate() { + + AuditedPerson person = new AuditedPerson(); + person.setId(42L); + person.setNew(true); // Cassandra does not support auto-generation hence we need + // to supply whether our object is a new one. + AuditedPerson saved = repository.save(person); + + assertThat(saved.getCreatedBy()).isEqualTo("Some user"); + assertThat(saved.getLastModifiedBy()).isEqualTo("Some user"); + assertThat(saved.getCreatedDate()).isBetween(Instant.now().minus(Duration.ofMinutes(1)), + Instant.now().plus(Duration.ofMinutes(1))); + assertThat(saved.getLastModifiedDate()).isBetween(Instant.now().minus(Duration.ofMinutes(1)), + Instant.now().plus(Duration.ofMinutes(1))); + } + + /** + * Modifying an existing object will update the last modified fields. + */ + @Test + public void updateShouldSetLastModifiedDate() { + + AuditedPerson person = new AuditedPerson(); + person.setId(42L); + person.setNew(true); // Cassandra does not support auto-generation hence we need + // to supply whether our object is a new one. + repository.save(person); + + person.setNew(false); + + AuditedPerson modified = repository.save(person); + + assertThat(modified.getCreatedBy()).isEqualTo("Some user"); + assertThat(modified.getLastModifiedBy()).isEqualTo("Some user"); + assertThat(modified.getCreatedDate()).isBetween(Instant.now().minus(Duration.ofMinutes(1)), + Instant.now().plus(Duration.ofMinutes(1))); + assertThat(modified.getLastModifiedDate()) + .isBetween(Instant.now().minus(Duration.ofMinutes(1)), Instant.now().plus(Duration.ofMinutes(1))) + .isNotEqualTo(modified.getCreatedDate()); + } + +}