diff --git a/spring-5-data-reactive/pom.xml b/spring-5-data-reactive/pom.xml index 5372803842..296da87529 100644 --- a/spring-5-data-reactive/pom.xml +++ b/spring-5-data-reactive/pom.xml @@ -14,6 +14,14 @@ + + io.projectreactor + reactor-core + + + org.springframework.boot + spring-boot-starter-data-couchbase-reactive + org.springframework.boot spring-boot-starter-data-mongodb-reactive @@ -105,6 +113,12 @@ httpclient ${httpclient.version} + + com.couchbase.mock + CouchbaseMock + ${couchbaseMock.version} + test + @@ -224,6 +238,7 @@ 0.8.0.M8 4.5.2 1.4.199 + 1.5.23 diff --git a/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/ReactiveCouchbaseApplication.java b/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/ReactiveCouchbaseApplication.java new file mode 100644 index 0000000000..917bcfdaa8 --- /dev/null +++ b/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/ReactiveCouchbaseApplication.java @@ -0,0 +1,12 @@ +package com.baeldung.couchbase; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ReactiveCouchbaseApplication { + + public static void main(String[] args) { + SpringApplication.run(ReactiveCouchbaseApplication.class, args); + } +} diff --git a/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/configuration/CouchbaseProperties.java b/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/configuration/CouchbaseProperties.java new file mode 100644 index 0000000000..480c96e986 --- /dev/null +++ b/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/configuration/CouchbaseProperties.java @@ -0,0 +1,40 @@ +package com.baeldung.couchbase.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +import java.util.Collections; +import java.util.List; + +@Configuration +public class CouchbaseProperties { + + private final List bootstrapHosts; + private final String bucketName; + private final String bucketPassword; + private final int port; + + public CouchbaseProperties(@Value("${spring.couchbase.bootstrap-hosts}") final List bootstrapHosts, @Value("${spring.couchbase.bucket.name}") final String bucketName, @Value("${spring.couchbase.bucket.password}") final String bucketPassword, + @Value("${spring.couchbase.port}") final int port) { + this.bootstrapHosts = Collections.unmodifiableList(bootstrapHosts); + this.bucketName = bucketName; + this.bucketPassword = bucketPassword; + this.port = port; + } + + public List getBootstrapHosts() { + return bootstrapHosts; + } + + public String getBucketName() { + return bucketName; + } + + public String getBucketPassword() { + return bucketPassword; + } + + public int getPort() { + return port; + } +} diff --git a/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/configuration/N1QLReactiveCouchbaseConfiguration.java b/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/configuration/N1QLReactiveCouchbaseConfiguration.java new file mode 100644 index 0000000000..059bd36cae --- /dev/null +++ b/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/configuration/N1QLReactiveCouchbaseConfiguration.java @@ -0,0 +1,15 @@ +package com.baeldung.couchbase.configuration; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; + +@Configuration +@EnableReactiveCouchbaseRepositories("com.baeldung.couchbase.domain.repository.n1ql") +@Primary +public class N1QLReactiveCouchbaseConfiguration extends ReactiveCouchbaseConfiguration { + + public N1QLReactiveCouchbaseConfiguration(CouchbaseProperties couchbaseProperties) { + super(couchbaseProperties); + } +} diff --git a/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/configuration/ReactiveCouchbaseConfiguration.java b/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/configuration/ReactiveCouchbaseConfiguration.java new file mode 100644 index 0000000000..2f3b5486d8 --- /dev/null +++ b/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/configuration/ReactiveCouchbaseConfiguration.java @@ -0,0 +1,39 @@ +package com.baeldung.couchbase.configuration; + +import com.couchbase.client.java.env.CouchbaseEnvironment; +import com.couchbase.client.java.env.DefaultCouchbaseEnvironment; +import org.springframework.data.couchbase.config.AbstractReactiveCouchbaseConfiguration; + +import java.util.List; + +public abstract class ReactiveCouchbaseConfiguration extends AbstractReactiveCouchbaseConfiguration { + + private CouchbaseProperties couchbaseProperties; + + public ReactiveCouchbaseConfiguration(final CouchbaseProperties couchbaseProperties) { + this.couchbaseProperties = couchbaseProperties; + } + + @Override + protected List getBootstrapHosts() { + return couchbaseProperties.getBootstrapHosts(); + } + + @Override + protected String getBucketName() { + return couchbaseProperties.getBucketName(); + } + + @Override + protected String getBucketPassword() { + return couchbaseProperties.getBucketPassword(); + } + + @Override + public CouchbaseEnvironment couchbaseEnvironment() { + return DefaultCouchbaseEnvironment + .builder() + .bootstrapHttpDirectPort(couchbaseProperties.getPort()) + .build(); + } +} diff --git a/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/configuration/ViewReactiveCouchbaseConfiguration.java b/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/configuration/ViewReactiveCouchbaseConfiguration.java new file mode 100644 index 0000000000..9b4d9b0319 --- /dev/null +++ b/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/configuration/ViewReactiveCouchbaseConfiguration.java @@ -0,0 +1,13 @@ +package com.baeldung.couchbase.configuration; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; + +@Configuration +@EnableReactiveCouchbaseRepositories("com.baeldung.couchbase.domain.repository.view") +public class ViewReactiveCouchbaseConfiguration extends ReactiveCouchbaseConfiguration { + + public ViewReactiveCouchbaseConfiguration(CouchbaseProperties couchbaseProperties) { + super(couchbaseProperties); + } +} diff --git a/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/domain/Person.java b/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/domain/Person.java new file mode 100644 index 0000000000..285de34df8 --- /dev/null +++ b/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/domain/Person.java @@ -0,0 +1,43 @@ +package com.baeldung.couchbase.domain; + +import org.springframework.data.annotation.Id; +import org.springframework.data.couchbase.core.mapping.Document; + +import java.util.Objects; +import java.util.UUID; + +@Document +public class Person { + + @Id private UUID id; + private String firstName; + + public Person(final UUID id, final String firstName) { + this.id = id; + this.firstName = firstName; + } + + private Person() { + } + + public UUID getId() { + return id; + } + + public String getFirstName() { + return firstName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Person person = (Person) o; + return Objects.equals(id, person.id) && Objects.equals(firstName, person.firstName); + } + + @Override + public int hashCode() { + return Objects.hash(id, firstName); + } +} diff --git a/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/domain/repository/n1ql/N1QLPersonRepository.java b/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/domain/repository/n1ql/N1QLPersonRepository.java new file mode 100644 index 0000000000..6f73a77ceb --- /dev/null +++ b/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/domain/repository/n1ql/N1QLPersonRepository.java @@ -0,0 +1,16 @@ +package com.baeldung.couchbase.domain.repository.n1ql; + +import com.baeldung.couchbase.domain.Person; +import org.springframework.data.couchbase.core.query.N1qlPrimaryIndexed; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; + +import java.util.UUID; + +@Repository +@N1qlPrimaryIndexed +public interface N1QLPersonRepository extends ReactiveCrudRepository { + + Flux findAllByFirstName(final String firstName); +} diff --git a/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/domain/repository/n1ql/N1QLSortingPersonRepository.java b/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/domain/repository/n1ql/N1QLSortingPersonRepository.java new file mode 100644 index 0000000000..57dd149425 --- /dev/null +++ b/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/domain/repository/n1ql/N1QLSortingPersonRepository.java @@ -0,0 +1,13 @@ +package com.baeldung.couchbase.domain.repository.n1ql; + +import com.baeldung.couchbase.domain.Person; +import org.springframework.data.couchbase.core.query.N1qlPrimaryIndexed; +import org.springframework.data.repository.reactive.ReactiveSortingRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +@N1qlPrimaryIndexed +public interface N1QLSortingPersonRepository extends ReactiveSortingRepository { +} diff --git a/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/domain/repository/view/ViewPersonRepository.java b/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/domain/repository/view/ViewPersonRepository.java new file mode 100644 index 0000000000..28a7ec1b78 --- /dev/null +++ b/spring-5-data-reactive/src/main/java/com/baeldung/couchbase/domain/repository/view/ViewPersonRepository.java @@ -0,0 +1,20 @@ +package com.baeldung.couchbase.domain.repository.view; + +import com.baeldung.couchbase.domain.Person; +import org.springframework.data.couchbase.core.query.View; +import org.springframework.data.couchbase.core.query.ViewIndexed; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; + +import java.util.UUID; + +@Repository +@ViewIndexed(designDoc = ViewPersonRepository.DESIGN_DOCUMENT) +public interface ViewPersonRepository extends ReactiveCrudRepository { + + String DESIGN_DOCUMENT = "persons"; + + @View(designDocument = ViewPersonRepository.DESIGN_DOCUMENT) + Flux findByFirstName(String firstName); +} diff --git a/spring-5-data-reactive/src/test/java/com/baeldung/couchbase/domain/repository/CouchbaseMockConfiguration.java b/spring-5-data-reactive/src/test/java/com/baeldung/couchbase/domain/repository/CouchbaseMockConfiguration.java new file mode 100644 index 0000000000..2a09fce4b0 --- /dev/null +++ b/spring-5-data-reactive/src/test/java/com/baeldung/couchbase/domain/repository/CouchbaseMockConfiguration.java @@ -0,0 +1,54 @@ +package com.baeldung.couchbase.domain.repository; + +import com.baeldung.couchbase.configuration.CouchbaseProperties; +import com.couchbase.mock.Bucket; +import com.couchbase.mock.BucketConfiguration; +import com.couchbase.mock.CouchbaseMock; +import org.springframework.boot.test.context.TestConfiguration; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Collections; + +@TestConfiguration +public class CouchbaseMockConfiguration { + + private CouchbaseMock couchbaseMock; + + public CouchbaseMockConfiguration(final CouchbaseProperties couchbaseProperties) { + final BucketConfiguration bucketConfiguration = new BucketConfiguration(); + bucketConfiguration.numNodes = 1; + bucketConfiguration.numReplicas = 1; + bucketConfiguration.numVBuckets = 1024; + bucketConfiguration.name = couchbaseProperties.getBucketName(); + bucketConfiguration.type = Bucket.BucketType.COUCHBASE; + bucketConfiguration.password = couchbaseProperties.getBucketPassword(); + + try { + couchbaseMock = new CouchbaseMock(couchbaseProperties.getPort(), Collections.singletonList(bucketConfiguration)); + } catch (final IOException ex) { + throw new UncheckedIOException(ex); + } + } + + @PostConstruct + public void postConstruct() { + try { + couchbaseMock.start(); + } catch (final IOException ex) { + throw new UncheckedIOException(ex); + } + try { + couchbaseMock.waitForStartup(); + } catch (final InterruptedException ex) { + throw new RuntimeException(ex); + } + } + + @PreDestroy + public void preDestroy() { + couchbaseMock.stop(); + } +} diff --git a/spring-5-data-reactive/src/test/java/com/baeldung/couchbase/domain/repository/n1ql/N1QLPersonRepositoryLiveTest.java b/spring-5-data-reactive/src/test/java/com/baeldung/couchbase/domain/repository/n1ql/N1QLPersonRepositoryLiveTest.java new file mode 100644 index 0000000000..0c1b6bde6c --- /dev/null +++ b/spring-5-data-reactive/src/test/java/com/baeldung/couchbase/domain/repository/n1ql/N1QLPersonRepositoryLiveTest.java @@ -0,0 +1,55 @@ +package com.baeldung.couchbase.domain.repository.n1ql; + +import com.baeldung.couchbase.domain.Person; +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; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import java.util.UUID; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class N1QLPersonRepositoryLiveTest { + + @Autowired private N1QLPersonRepository personRepository; + + @Test + public void shouldFindAll_byLastName() { + //Given + final String firstName = "John"; + final Person matchingPerson = new Person(UUID.randomUUID(), firstName); + final Person nonMatchingPerson = new Person(UUID.randomUUID(), "NotJohn"); + wrap(() -> { + personRepository + .save(matchingPerson) + .subscribe(); + personRepository + .save(nonMatchingPerson) + .subscribe(); + //When + final Flux allByFirstName = personRepository.findAllByFirstName(firstName); + //Then + StepVerifier + .create(allByFirstName) + .expectNext(matchingPerson) + .verifyComplete(); + + }, matchingPerson, nonMatchingPerson); + } + + private void wrap(final Runnable runnable, final Person... people) { + try { + runnable.run(); + } finally { + for (final Person person : people) { + personRepository + .delete(person) + .subscribe(); + } + } + } +} \ No newline at end of file diff --git a/spring-5-data-reactive/src/test/java/com/baeldung/couchbase/domain/repository/n1ql/N1QLSortingPersonRepositoryLiveTest.java b/spring-5-data-reactive/src/test/java/com/baeldung/couchbase/domain/repository/n1ql/N1QLSortingPersonRepositoryLiveTest.java new file mode 100644 index 0000000000..4ba2206e0a --- /dev/null +++ b/spring-5-data-reactive/src/test/java/com/baeldung/couchbase/domain/repository/n1ql/N1QLSortingPersonRepositoryLiveTest.java @@ -0,0 +1,60 @@ +package com.baeldung.couchbase.domain.repository.n1ql; + +import com.baeldung.couchbase.domain.Person; +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.data.domain.Sort; +import org.springframework.test.context.junit4.SpringRunner; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import java.util.UUID; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class N1QLSortingPersonRepositoryLiveTest { + + @Autowired private N1QLSortingPersonRepository personRepository; + + @Test + public void shouldFindAll_sortedByFirstName() { + //Given + final Person firstPerson = new Person(UUID.randomUUID(), "John"); + final Person secondPerson = new Person(UUID.randomUUID(), "Mikki"); + wrap(() -> { + personRepository + .save(firstPerson) + .subscribe(); + personRepository + .save(secondPerson) + .subscribe(); + //When + final Flux allByFirstName = personRepository.findAll(Sort.by(Sort.Direction.DESC, "firstName")); + //Then + StepVerifier + .create(allByFirstName) + .expectNextMatches(person -> person + .getFirstName() + .equals(secondPerson.getFirstName())) + .expectNextMatches(person -> person + .getFirstName() + .equals(firstPerson.getFirstName())) + .verifyComplete(); + }, firstPerson, secondPerson); + } + + //workaround for deleteAll() + private void wrap(final Runnable runnable, final Person... people) { + try { + runnable.run(); + } finally { + for (final Person person : people) { + personRepository + .delete(person) + .subscribe(); + } + } + } +} \ No newline at end of file diff --git a/spring-5-data-reactive/src/test/java/com/baeldung/couchbase/domain/repository/view/ViewPersonRepositoryIntegrationTest.java b/spring-5-data-reactive/src/test/java/com/baeldung/couchbase/domain/repository/view/ViewPersonRepositoryIntegrationTest.java new file mode 100644 index 0000000000..712e7b0d37 --- /dev/null +++ b/spring-5-data-reactive/src/test/java/com/baeldung/couchbase/domain/repository/view/ViewPersonRepositoryIntegrationTest.java @@ -0,0 +1,79 @@ +package com.baeldung.couchbase.domain.repository.view; + +import com.baeldung.couchbase.configuration.CouchbaseProperties; +import com.baeldung.couchbase.configuration.ViewReactiveCouchbaseConfiguration; +import com.baeldung.couchbase.domain.Person; +import com.baeldung.couchbase.domain.repository.CouchbaseMockConfiguration; +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; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.UUID; + +@RunWith(SpringRunner.class) +@SpringBootTest(properties = { "spring.couchbase.port=10010" }, classes = { ViewReactiveCouchbaseConfiguration.class, CouchbaseProperties.class, CouchbaseMockConfiguration.class }) +public class ViewPersonRepositoryIntegrationTest { + + @Autowired private ViewPersonRepository personRepository; + + @Test + public void shouldSavePerson_findById_thenDeleteIt() { + //Given + final UUID id = UUID.randomUUID(); + final Person person = new Person(id, "John"); + wrap(() -> { + personRepository + .save(person) + .subscribe(); + //When + final Mono byId = personRepository.findById(id); + //Then + StepVerifier + .create(byId) + .expectNextMatches(result -> result + .getId() + .equals(id)) + .expectComplete() + .verify(); + }, person); + } + + @Test + public void shouldFindAll_thenDeleteIt() { + //Given + final Person person = new Person(UUID.randomUUID(), "John"); + final Person secondPerson = new Person(UUID.randomUUID(), "Mikki"); + wrap(() -> { + personRepository + .save(person) + .subscribe(); + personRepository + .save(secondPerson) + .subscribe(); + //When + final Flux all = personRepository.findAll(); + //Then + StepVerifier + .create(all) + .expectNextCount(2) + .verifyComplete(); + }, person, secondPerson); + } + + private void wrap(final Runnable runnable, final Person... people) { + try { + runnable.run(); + } finally { + for (final Person person : people) { + personRepository + .delete(person) + .subscribe(); + } + } + } +} \ No newline at end of file