diff --git a/data/data-cassandra/build.gradle b/data/data-cassandra/build.gradle new file mode 100644 index 0000000..a8f8884 --- /dev/null +++ b/data/data-cassandra/build.gradle @@ -0,0 +1,22 @@ +plugins { + id "java" + id "org.springframework.boot" + id "org.springframework.cr.smoke-test" +} + +dependencies { + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + implementation("org.springframework.boot:spring-boot-starter-data-cassandra") + + implementation("org.crac:crac:$cracVersion") + implementation(project(":cr-listener")) + + testImplementation("org.springframework.boot:spring-boot-starter-test") + + appTestImplementation(project(":cr-smoke-test-support")) + appTestImplementation("org.awaitility:awaitility:4.2.0") +} + +crSmokeTest { + webApplication = false +} diff --git a/data/data-cassandra/docker-compose.yml b/data/data-cassandra/docker-compose.yml new file mode 100644 index 0000000..3ad804c --- /dev/null +++ b/data/data-cassandra/docker-compose.yml @@ -0,0 +1,5 @@ +services: + cassandra: + image: 'cassandra:4' + ports: + - '9042' diff --git a/data/data-cassandra/src/appTest/java/com/example/data/cassandra/DataCassandraApplicationTest.java b/data/data-cassandra/src/appTest/java/com/example/data/cassandra/DataCassandraApplicationTest.java new file mode 100644 index 0000000..2459eb9 --- /dev/null +++ b/data/data-cassandra/src/appTest/java/com/example/data/cassandra/DataCassandraApplicationTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2023 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 com.example.data.cassandra; + +import static org.assertj.core.api.Assertions.*; + +import java.time.Duration; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.springframework.cr.smoketest.support.assertj.AssertableOutput; +import org.springframework.cr.smoketest.support.junit.ApplicationTest; + +@ApplicationTest +public class DataCassandraApplicationTest { + + @Test + void connectionTest(AssertableOutput output) { + Awaitility.await().atMost(Duration.ofSeconds(10)).untilAsserted(() -> { + assertThat(output).hasLineMatching("Starting CassandraReader: was (INITIALIZED|STOPPED)"); + assertThat(output).hasLineMatching("count \\d+: User\\{id=\\d, username='.*"); + }); + } + +} diff --git a/data/data-cassandra/src/main/java/com/example/data/cassandra/CassandraReader.java b/data/data-cassandra/src/main/java/com/example/data/cassandra/CassandraReader.java new file mode 100644 index 0000000..ec086b4 --- /dev/null +++ b/data/data-cassandra/src/main/java/com/example/data/cassandra/CassandraReader.java @@ -0,0 +1,110 @@ +/* + * Copyright 2023 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 + * + * https://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 com.example.data.cassandra; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import com.datastax.oss.driver.api.core.CqlSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.SmartLifecycle; +import org.springframework.data.util.Streamable; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +@Component +@EnableScheduling +class CassandraReader implements SmartLifecycle { + + private final Object lifecycleMonitor = new Object(); + + private final AtomicInteger counter = new AtomicInteger(0); + + private State state = State.INITIALIZED; + + private List userNames; + + @Autowired + CqlSession cqlSession; + + enum State { + + INITIALIZED, STARTED, STOPPED + + } + + @Autowired + UserRepository users; + + @Override + public void start() { + + synchronized (lifecycleMonitor) { + switch (state) { + case INITIALIZED, STOPPED -> { + + System.out.printf("Starting CassandraReader: was %s\n", state); + if (ObjectUtils.isEmpty(userNames)) { + userNames = insertUsers().stream().map(User::getUsername).collect(Collectors.toList()); + } + state = State.STARTED; + } + } + } + } + + @Scheduled(fixedDelay = 1000) + public void scheduled() { + + if (isRunning()) { + + int count = counter.incrementAndGet(); + User user = users.findUserByUsername(userNames.get(count % userNames.size())); + System.out.printf("count %03d: %s\n", count, user); + } + } + + @Override + public void stop() { + + synchronized (lifecycleMonitor) { + switch (state) { + case INITIALIZED, STARTED -> { + System.out.printf("Stopping CassandraReader: was %s\n", state); + state = State.STOPPED; + } + } + } + } + + @Override + public boolean isRunning() { + return State.STARTED.equals(state); + } + + private List insertUsers() { + + User brandonSanderson = new User(1L, "Brandon", "Sanderson"); + User brentWeeks = new User(2L, "Brent", "Weeks"); + User peterVBrett = new User(3L, "Peter V.", "Brett"); + + return Streamable.of(users.saveAll(List.of(brandonSanderson, brentWeeks, peterVBrett))).toList(); + } + +} diff --git a/data/data-cassandra/src/main/java/com/example/data/cassandra/DataCassandraApplication.java b/data/data-cassandra/src/main/java/com/example/data/cassandra/DataCassandraApplication.java new file mode 100644 index 0000000..76ddb84 --- /dev/null +++ b/data/data-cassandra/src/main/java/com/example/data/cassandra/DataCassandraApplication.java @@ -0,0 +1,50 @@ +/* + * Copyright 2023 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 + * + * https://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 com.example.data.cassandra; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.CqlSessionBuilder; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.cassandra.CassandraProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.cassandra.core.cql.generator.CreateKeyspaceCqlGenerator; +import org.springframework.data.cassandra.core.cql.keyspace.CreateKeyspaceSpecification; + +@SpringBootApplication +public class DataCassandraApplication { + + public static void main(String[] args) { + SpringApplication.run(DataCassandraApplication.class, args); + } + + @Configuration + class CassandraConfiguration { + + @Bean + CqlSession cqlSession(CqlSessionBuilder cqlSessionBuilder, CassandraProperties properties) { + // This creates the keyspace on startup + try (CqlSession session = cqlSessionBuilder.withKeyspace((String) null).build()) { + session.execute(CreateKeyspaceCqlGenerator + .toCql(CreateKeyspaceSpecification.createKeyspace(properties.getKeyspaceName()).ifNotExists())); + } + return cqlSessionBuilder.withKeyspace(properties.getKeyspaceName()).build(); + } + + } + +} diff --git a/data/data-cassandra/src/main/java/com/example/data/cassandra/User.java b/data/data-cassandra/src/main/java/com/example/data/cassandra/User.java new file mode 100644 index 0000000..42e2e8d --- /dev/null +++ b/data/data-cassandra/src/main/java/com/example/data/cassandra/User.java @@ -0,0 +1,82 @@ +/* + * Copyright 2023 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 + * + * https://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 com.example.data.cassandra; + +import org.springframework.data.cassandra.core.mapping.Column; +import org.springframework.data.cassandra.core.mapping.PrimaryKey; +import org.springframework.data.cassandra.core.mapping.Table; + +@Table(value = "users") +public class User { + + @PrimaryKey("user_id") + private Long id; + + @Column("uname") + private String username; + + @Column("fname") + private String firstname; + + @Column("lname") + private String lastname; + + public User(Long id, String firstname, String lastname) { + this.id = id; + this.firstname = firstname; + this.lastname = lastname; + this.username = "%s_%s".formatted(firstname, lastname); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + @Override + public String toString() { + return "User{" + "id=" + id + ", username='" + username + '\'' + ", firstname='" + firstname + '\'' + + ", lastname='" + lastname + '\'' + '}'; + } + +} diff --git a/data/data-cassandra/src/main/java/com/example/data/cassandra/UserRepository.java b/data/data-cassandra/src/main/java/com/example/data/cassandra/UserRepository.java new file mode 100644 index 0000000..254b224 --- /dev/null +++ b/data/data-cassandra/src/main/java/com/example/data/cassandra/UserRepository.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 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 + * + * https://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 com.example.data.cassandra; + +import org.springframework.data.cassandra.repository.AllowFiltering; +import org.springframework.data.repository.CrudRepository; + +public interface UserRepository extends CrudRepository { + + @AllowFiltering + User findUserByUsername(String username); + +} diff --git a/data/data-cassandra/src/main/resources/application.properties b/data/data-cassandra/src/main/resources/application.properties new file mode 100644 index 0000000..e33cdf1 --- /dev/null +++ b/data/data-cassandra/src/main/resources/application.properties @@ -0,0 +1,7 @@ +spring.cassandra.contact-points=${CASSANDRA_HOST:127.0.0.1}:${CASSANDRA_PORT_9042:9042} +spring.cassandra.local-datacenter=datacenter1 +spring.cassandra.keyspace-name=example +spring.cassandra.schema-action=recreate +spring.cassandra.connection.connect-timeout=60s +spring.cassandra.connection.init-query-timeout=60s +spring.cassandra.request.timeout=60s