DATACOUCH-588 - Part 2 of framework changes. Add support for projection and distinct. (#1040)

Support for projection is only for properties of the top-level entity. For instance, in UserSubmission, only the properties below can be specified in the projection. Projection support does not provide means of specifying something like address.street - you can only project (or not project) the whole address property. However, the address type in your resultType could have a subset of the properties in Address.

If the corresponding submissions in the resultType contained only the userId property

public class UserSubmission extends ComparableEntity {
	private String id;
	private String username;
	private List<String> roles;
	private Address address;
	private List<Submission> submissions;

Support for Distinct - I have appropriated the MongoDB model for Distinct.  It defines a separate DistinctOperationSupport class (within ExecutableFindByQuerySupport) which supports the distinct( distinctFields ) api and execution. The DistinctOperationSupport class has only a distinctFields member, and a 'delegate' member, which is an ExecutableFindByQuerySupport object. TBH, I don't see the advantage over simply adding a distinctFields member to ExecutableFindByQuerySupport

Amend #1 - changes as discussed in Pull Request
         - clean up test entity types

Amend #2
- Eliminate DistinctOperationSupport class. In MongoDB, only distinct on a single field is supported, so the returnType from distinct was very different from the returnType of other query operations (all(), one() etc. (but so is count(), and it doesn't need it's own class)). In Couchbase, distinct on any fields in the entity is allowed - so the returned type could be the domainType or resultType. And as(resultType) still allows any resultType to be specified. This makes it unnecessary to have combinations of interfaces such as DistinctWithProjection and DistinctWithQuery.

- Clean up the interfaces in ExecutableFindByQuery. There are two types of interfaces (a) the TerminatingFindByQuery which has the one(), oneValue() first(), firstValue(), all(), count(), exists() and stream(); and (b) the option interfaces (FindByQueryWithConsistency etc), which are essentially with-er interfaces. The changes are:
1) make all the with-er interfaces base interfaces instead of chaining them together. (I don't know why there isn't simply one interface with all the with-er methods).
2) make the ExecutableFindByQuery interface extend the Terminating interface and all the with-er interfaces.

Amend #3
- Add execution support for collections

Amend #4
- Add tests for collections. This includes a new CollectionAwareIntegrationTests class which extends a new JavaIntegratationTests class which extends the existing ClusterAwareIntegrationTests.
- Fixed up several issues collections issues that were uncovered by the tests.
- Did further cleanup of OperationSupport interfaces.

Amend #5
- Revert changes to interfaces in *Operation
- Sorted interfaces in same order for consistency (because of the chaining of interfaces, fluent methods must be called in order).

Co-authored-by: mikereiche <michael.reiche@couchbase.com>
This commit is contained in:
Michael Reiche
2021-01-14 11:03:20 -08:00
committed by GitHub
parent a4e6027f01
commit 0e8d5a255a
78 changed files with 2439 additions and 609 deletions

View File

@@ -23,10 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE;
import static org.springframework.data.couchbase.config.BeanNames.REACTIVE_COUCHBASE_TEMPLATE;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.time.Duration;
@@ -34,32 +31,26 @@ import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import com.couchbase.client.java.manager.query.CreatePrimaryQueryIndexOptions;;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.couchbase.CouchbaseClientFactory;
import org.springframework.data.couchbase.SimpleCouchbaseClientFactory;
import org.springframework.data.couchbase.core.ExecutableReplaceByIdOperation.ExecutableReplaceById;
import org.springframework.data.couchbase.core.ExecutableRemoveByIdOperation.ExecutableRemoveById;
import org.springframework.data.couchbase.domain.Config;
import org.springframework.data.couchbase.core.ExecutableReplaceByIdOperation.ExecutableReplaceById;
import org.springframework.data.couchbase.core.support.OneAndAllEntity;
import org.springframework.data.couchbase.domain.PersonValue;
import org.springframework.data.couchbase.domain.User;
import org.springframework.data.couchbase.domain.UserAnnotated;
import org.springframework.data.couchbase.domain.UserAnnotated2;
import org.springframework.data.couchbase.domain.UserAnnotated3;
import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests;
import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.IgnoreWhen;
import com.couchbase.client.java.kv.PersistTo;
import com.couchbase.client.java.kv.ReplicateTo;
import org.springframework.data.couchbase.util.JavaIntegrationTests;
;
/**
* KV tests Theses tests rely on a cb server running.
@@ -68,30 +59,12 @@ import com.couchbase.client.java.kv.ReplicateTo;
* @author Michael Reiche
*/
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
class CouchbaseTemplateKeyValueIntegrationTests extends ClusterAwareIntegrationTests {
private static CouchbaseClientFactory couchbaseClientFactory;
private CouchbaseTemplate couchbaseTemplate;
private ReactiveCouchbaseTemplate reactiveCouchbaseTemplate;
@BeforeAll
static void beforeAll() {
couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectionString(), authenticator(), bucketName());
couchbaseClientFactory.getBucket().waitUntilReady(Duration.ofSeconds(10));
couchbaseClientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName(),
CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions().ignoreIfExists(true));
}
@AfterAll
static void afterAll() throws IOException {
couchbaseClientFactory.close();
}
class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests {
@BeforeEach
void beforeEach() {
ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE);
reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE);
@Override
public void beforeEach() {
super.beforeEach();
couchbaseTemplate.removeByQuery(User.class).all();
couchbaseTemplate.removeByQuery(UserAnnotated.class).all();
couchbaseTemplate.removeByQuery(UserAnnotated2.class).all();
@@ -110,6 +83,7 @@ class CouchbaseTemplateKeyValueIntegrationTests extends ClusterAwareIntegrationT
assertThrows(DataIntegrityViolationException.class, () -> couchbaseTemplate.replaceById(User.class).one(user));
User found = couchbaseTemplate.findById(User.class).one(user.getId());
user.setVersion(found.getVersion());
assertEquals(user, found);
couchbaseTemplate.removeById().one(user.getId());
@@ -121,7 +95,7 @@ class CouchbaseTemplateKeyValueIntegrationTests extends ClusterAwareIntegrationT
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class clazz = User.class; // for now, just User.class. There is no Durability annotation.
// insert, replace, upsert
for (OneAndAll<User> operator : new OneAndAll[] { couchbaseTemplate.insertById(clazz),
for (OneAndAllEntity<User> operator : new OneAndAllEntity[] { couchbaseTemplate.insertById(clazz),
couchbaseTemplate.replaceById(clazz), couchbaseTemplate.upsertById(clazz) }) {
// create an entity of type clazz
Constructor cons = clazz.getConstructor(String.class, String.class, String.class);
@@ -129,7 +103,7 @@ class CouchbaseTemplateKeyValueIntegrationTests extends ClusterAwareIntegrationT
"firstname", "lastname");
if (clazz.equals(User.class)) { // User.java doesn't have an durability annotation
operator = (OneAndAll) ((WithDurability<User>) operator).withDurability(PersistTo.ACTIVE, ReplicateTo.NONE);
operator = (OneAndAllEntity) ((WithDurability<User>) operator).withDurability(PersistTo.ACTIVE, ReplicateTo.NONE);
}
// if replace, we need to insert a document to replace
@@ -160,7 +134,7 @@ class CouchbaseTemplateKeyValueIntegrationTests extends ClusterAwareIntegrationT
// Entity classes
for (Class clazz : new Class[] { User.class, UserAnnotated.class, UserAnnotated2.class, UserAnnotated3.class }) {
// insert, replace, upsert
for (OneAndAll<User> operator : new OneAndAll[] { couchbaseTemplate.insertById(clazz),
for (OneAndAllEntity<User> operator : new OneAndAllEntity[] { couchbaseTemplate.insertById(clazz),
couchbaseTemplate.replaceById(clazz), couchbaseTemplate.upsertById(clazz) }) {
// create an entity of type clazz
@@ -169,9 +143,9 @@ class CouchbaseTemplateKeyValueIntegrationTests extends ClusterAwareIntegrationT
"firstname", "lastname");
if (clazz.equals(User.class)) { // User.java doesn't have an expiry annotation
operator = (OneAndAll) ((WithExpiry<User>) operator).withExpiry(Duration.ofSeconds(1));
operator = (OneAndAllEntity) ((WithExpiry<User>) operator).withExpiry(Duration.ofSeconds(1));
} else if (clazz.equals(UserAnnotated3.class)) { // override the expiry from the annotation with no expiry
operator = (OneAndAll) ((WithExpiry<User>) operator).withExpiry(Duration.ofSeconds(0));
operator = (OneAndAllEntity) ((WithExpiry<User>) operator).withExpiry(Duration.ofSeconds(0));
}
// if replace or remove, we need to insert a document to replace

View File

@@ -0,0 +1,339 @@
/*
* Copyright 2021 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 org.springframework.data.couchbase.core;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Instant;
import java.time.temporal.TemporalAccessor;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.couchbase.core.query.Query;
import org.springframework.data.couchbase.core.query.QueryCriteria;
import org.springframework.data.couchbase.domain.Address;
import org.springframework.data.couchbase.domain.Airport;
import org.springframework.data.couchbase.domain.Course;
import org.springframework.data.couchbase.domain.NaiveAuditorAware;
import org.springframework.data.couchbase.domain.Submission;
import org.springframework.data.couchbase.domain.User;
import org.springframework.data.couchbase.domain.UserJustLastName;
import org.springframework.data.couchbase.domain.UserSubmission;
import org.springframework.data.couchbase.domain.UserSubmissionProjected;
import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider;
import org.springframework.data.couchbase.util.Capabilities;
import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests;
import org.springframework.data.couchbase.util.IgnoreWhen;
import com.couchbase.client.java.query.QueryScanConsistency;
/**
* Query tests Theses tests rely on a cb server running This class tests collection support with
* inCollection(collection) It should be identical to CouchbaseTemplateQueryIntegrationTests except for the setup and
* the inCollection(collectionName) calls. Testing without collections could also be done by this class simply by using
* scopeName = null and collectionName = null (except for inCollection() checks that the collectionName is not null)
*
* @author Michael Reiche
*/
@IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED)
class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests {
@BeforeAll
public static void beforeAll() {
// first call the super method
callSuperBeforeAll(new Object() {});
// then do processing for this class
// collectionName = null;
// scopeName = null;
}
@AfterAll
public static void afterAll() {
// first do the processing for this class
// no-op
// then call the super method
callSuperAfterAll(new Object() {});
}
@BeforeEach
@Override
public void beforeEach() {
// first call the super method
super.beforeEach();
// then do processing for this class
couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all();
}
@AfterEach
@Override
public void afterEach() {
// first call the super method
super.afterEach();
// then do processing for this class
// no-op
}
@Test
void findByQueryAll() {
try {
User user1 = new User(UUID.randomUUID().toString(), "user1", "user1");
User user2 = new User(UUID.randomUUID().toString(), "user2", "user2");
couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2));
final List<User> foundUsers = couchbaseTemplate.findByQuery(User.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all();
for (User u : foundUsers) {
if (!(u.equals(user1) || u.equals(user2))) {
// somebody didn't clean up after themselves.
couchbaseTemplate.removeById().inCollection(collectionName).one(u.getId());
}
}
assertEquals(2, foundUsers.size());
TemporalAccessor auditTime = new AuditingDateTimeProvider().getNow().get();
long auditMillis = Instant.from(auditTime).toEpochMilli();
String auditUser = new NaiveAuditorAware().getCurrentAuditor().get();
for (User u : foundUsers) {
assertTrue(u.equals(user1) || u.equals(user2));
assertEquals(auditUser, u.getCreator());
assertEquals(auditMillis, u.getCreatedDate());
assertEquals(auditUser, u.getLastModifiedBy());
assertEquals(auditMillis, u.getLastModifiedDate());
}
couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId());
reactiveCouchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId()).block();
} finally {
couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all();
}
User usery = couchbaseTemplate.findById(User.class).inCollection(collectionName).one("userx");
assertNull(usery, "usery should be null");
User userz = reactiveCouchbaseTemplate.findById(User.class).inCollection(collectionName).one("userx").block();
assertNull(userz, "userz should be null");
}
@Test
void findByMatchingQuery() {
User user1 = new User(UUID.randomUUID().toString(), "user1", "user1");
User user2 = new User(UUID.randomUUID().toString(), "user2", "user2");
User specialUser = new User(UUID.randomUUID().toString(), "special", "special");
couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser));
Query specialUsers = new Query(QueryCriteria.where("firstname").like("special"));
final List<User> foundUsers = couchbaseTemplate.findByQuery(User.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all();
assertEquals(1, foundUsers.size());
}
@Test
void findByMatchingQueryProjected() {
UserSubmission user = new UserSubmission();
user.setId(UUID.randomUUID().toString());
user.setUsername("dave");
user.setRoles(Arrays.asList("role1", "role2"));
Address address = new Address();
address.setStreet("1234 Olcott Street");
user.setAddress(address);
user.setSubmissions(
Arrays.asList(new Submission(UUID.randomUUID().toString(), user.getId(), "tid", "status", 123)));
user.setCourses(Arrays.asList(new Course(UUID.randomUUID().toString(), user.getId(), "581"),
new Course(UUID.randomUUID().toString(), user.getId(), "777")));
couchbaseTemplate.upsertById(UserSubmission.class).inCollection(collectionName).one(user);
Query daveUsers = new Query(QueryCriteria.where("username").like("dave"));
final List<UserSubmissionProjected> foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class)
.as(UserSubmissionProjected.class).withConsistency(QueryScanConsistency.REQUEST_PLUS)
.inCollection(collectionName).matching(daveUsers).all();
assertEquals(1, foundUserSubmissions.size());
assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername());
assertEquals(user.getId(), foundUserSubmissions.get(0).getId());
assertEquals(user.getCourses(), foundUserSubmissions.get(0).getCourses());
assertEquals(user.getAddress(), foundUserSubmissions.get(0).getAddress());
couchbaseTemplate.removeByQuery(UserSubmission.class).inCollection(collectionName).all();
User user1 = new User(UUID.randomUUID().toString(), "user1", "user1");
User user2 = new User(UUID.randomUUID().toString(), "user2", "user2");
User specialUser = new User(UUID.randomUUID().toString(), "special", "special");
couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser));
Query specialUsers = new Query(QueryCriteria.where("firstname").like("special"));
final List<UserJustLastName> foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all();
assertEquals(1, foundUsers.size());
final List<UserJustLastName> foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class)
.as(UserJustLastName.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName)
.matching(specialUsers).all().collectList().block();
assertEquals(1, foundUsersReactive.size());
}
@Test
void removeByQueryAll() {
User user1 = new User(UUID.randomUUID().toString(), "user1", "user1");
User user2 = new User(UUID.randomUUID().toString(), "user2", "user2");
couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2));
assertTrue(couchbaseTemplate.existsById().inCollection(collectionName).one(user1.getId()));
assertTrue(couchbaseTemplate.existsById().inCollection(collectionName).one(user2.getId()));
couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS)
.inCollection(collectionName).all();
assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId()));
assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user2.getId()));
}
@Test
void removeByMatchingQuery() {
User user1 = new User(UUID.randomUUID().toString(), "user1", "user1");
User user2 = new User(UUID.randomUUID().toString(), "user2", "user2");
User specialUser = new User(UUID.randomUUID().toString(), "special", "special");
couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser));
assertTrue(couchbaseTemplate.existsById().inCollection(collectionName).one(user1.getId()));
assertTrue(couchbaseTemplate.existsById().inCollection(collectionName).one(user2.getId()));
assertTrue(couchbaseTemplate.existsById().inCollection(collectionName).one(specialUser.getId()));
Query nonSpecialUsers = new Query(QueryCriteria.where("firstname").notLike("special"));
couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS)
.inCollection(collectionName).matching(nonSpecialUsers).all();
assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId()));
assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user2.getId()));
assertNotNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(specialUser.getId()));
}
@Test
void distinct() {
String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" };
String[] icaos = { "ic0", "ic1", "ic0", "ic1", "ic0", "ic1", "ic0" };
try {
for (int i = 0; i < iatas.length; i++) {
Airport airport = new Airport("airports::" + iatas[i], iatas[i] /*iata*/, icaos[i] /* icao */);
couchbaseTemplate.insertById(Airport.class).inCollection(collectionName).one(airport);
}
// distinct and count(distinct(...)) calls. use as() and consistentWith to verify fluent api
// as the fluent api for Distinct is tricky
// distinct icao
List<Airport> airports1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" })
.as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all();
assertEquals(2, airports1.size());
// distinct all-fields-in-Airport.class
List<Airport> airports2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all();
assertEquals(7, airports2.size());
// count( distinct { iata, icao } )
long count1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" })
.as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count();
assertEquals(7, count1);
// count( distinct (all fields in icaoClass)
Class icaoClass = (new Object() {
String iata;
String icao;
}).getClass();
long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count();
assertEquals(7, count2);
} finally {
couchbaseTemplate.removeById().inCollection(collectionName)
.all(Arrays.stream(iatas).map((iata) -> "airports::" + iata).collect(Collectors.toSet()));
}
}
@Test
void distinctReactive() {
String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" };
String[] icaos = { "ic0", "ic1", "ic0", "ic1", "ic0", "ic1", "ic0" };
try {
for (int i = 0; i < iatas.length; i++) {
Airport airport = new Airport("airports::" + iatas[i], iatas[i] /*iata*/, icaos[i] /* icao */);
reactiveCouchbaseTemplate.insertById(Airport.class).inCollection(collectionName).one(airport).block();
}
// distinct and count(distinct(...)) calls. use as() and consistentWith to verify fluent api
// as the fluent api for Distinct is tricky
// distinct icao
List<Airport> airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" })
.as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all()
.collectList().block();
assertEquals(2, airports1.size());
// distinct all-fields-in-Airport.class
List<Airport> airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {})
.as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all()
.collectList().block();
assertEquals(7, airports2.size());
// count( distinct icao )
// not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475
long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" })
.as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count()
.block();
assertEquals(2, count1);
// count( distinct (all fields in icaoClass) // which only has one field
// not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475
Class icaoClass = (new Object() {
String icao;
}).getClass();
long count2 = (long) reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count().block();
assertEquals(2, count2);
} finally {
reactiveCouchbaseTemplate.removeById().inCollection(collectionName)
.all(Arrays.stream(iatas).map((iata) -> "airports::" + iata).collect(Collectors.toSet())).collectList()
.block();
}
}
}

View File

@@ -23,33 +23,35 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE;
import static org.springframework.data.couchbase.config.BeanNames.REACTIVE_COUCHBASE_TEMPLATE;
import java.io.IOException;
import java.time.Instant;
import java.time.temporal.TemporalAccessor;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.data.couchbase.CouchbaseClientFactory;
import org.springframework.data.couchbase.SimpleCouchbaseClientFactory;
import org.springframework.data.couchbase.core.query.Query;
import org.springframework.data.couchbase.core.query.QueryCriteria;
import org.springframework.data.couchbase.domain.Address;
import org.springframework.data.couchbase.domain.Airport;
import org.springframework.data.couchbase.domain.Config;
import org.springframework.data.couchbase.domain.Course;
import org.springframework.data.couchbase.domain.NaiveAuditorAware;
import org.springframework.data.couchbase.domain.Submission;
import org.springframework.data.couchbase.domain.User;
import org.springframework.data.couchbase.domain.UserJustLastName;
import org.springframework.data.couchbase.domain.UserSubmission;
import org.springframework.data.couchbase.domain.UserSubmissionProjected;
import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider;
import org.springframework.data.couchbase.util.Capabilities;
import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests;
import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.IgnoreWhen;
import org.springframework.data.couchbase.util.JavaIntegrationTests;
import com.couchbase.client.core.error.IndexExistsException;
import com.couchbase.client.java.query.QueryScanConsistency;
/**
@@ -60,30 +62,11 @@ import com.couchbase.client.java.query.QueryScanConsistency;
* @author Haris Alesevic
*/
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
class CouchbaseTemplateQueryIntegrationTests extends ClusterAwareIntegrationTests {
private static CouchbaseClientFactory couchbaseClientFactory;
private CouchbaseTemplate couchbaseTemplate;
private ReactiveCouchbaseTemplate reactiveCouchbaseTemplate;
@BeforeAll
static void beforeAll() {
couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectionString(), authenticator(), bucketName());
try {
couchbaseClientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName());
} catch (IndexExistsException ex) {
// ignore, all good.
}
}
@AfterAll
static void afterAll() throws IOException {
couchbaseClientFactory.close();
}
class CouchbaseTemplateQueryIntegrationTests extends JavaIntegrationTests {
@BeforeEach
void beforeEach() {
@Override
public void beforeEach() {
ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE);
reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE);
@@ -100,7 +83,7 @@ class CouchbaseTemplateQueryIntegrationTests extends ClusterAwareIntegrationTest
couchbaseTemplate.upsertById(User.class).all(Arrays.asList(user1, user2));
final List<User> foundUsers = couchbaseTemplate.findByQuery(User.class)
.consistentWith(QueryScanConsistency.REQUEST_PLUS).all();
.withConsistency(QueryScanConsistency.REQUEST_PLUS).all();
for (User u : foundUsers) {
if (!(u.equals(user1) || u.equals(user2))) {
@@ -143,11 +126,57 @@ class CouchbaseTemplateQueryIntegrationTests extends ClusterAwareIntegrationTest
Query specialUsers = new Query(QueryCriteria.where("firstname").like("special"));
final List<User> foundUsers = couchbaseTemplate.findByQuery(User.class)
.consistentWith(QueryScanConsistency.REQUEST_PLUS).matching(specialUsers).all();
.withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(specialUsers).all();
assertEquals(1, foundUsers.size());
}
@Test
void findByMatchingQueryProjected() {
UserSubmission user = new UserSubmission();
user.setId(UUID.randomUUID().toString());
user.setUsername("dave");
user.setRoles(Arrays.asList("role1", "role2"));
Address address = new Address();
address.setStreet("1234 Olcott Street");
user.setAddress(address);
user.setSubmissions(
Arrays.asList(new Submission(UUID.randomUUID().toString(), user.getId(), "tid", "status", 123)));
user.setCourses(Arrays.asList(new Course(UUID.randomUUID().toString(), user.getId(), "581"),
new Course(UUID.randomUUID().toString(), user.getId(), "777")));
couchbaseTemplate.upsertById(UserSubmission.class).one(user);
Query daveUsers = new Query(QueryCriteria.where("username").like("dave"));
final List<UserSubmissionProjected> foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class)
.as(UserSubmissionProjected.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(daveUsers).all();
assertEquals(1, foundUserSubmissions.size());
assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername());
assertEquals(user.getId(), foundUserSubmissions.get(0).getId());
assertEquals(user.getCourses(), foundUserSubmissions.get(0).getCourses());
assertEquals(user.getAddress(), foundUserSubmissions.get(0).getAddress());
couchbaseTemplate.removeByQuery(UserSubmission.class).all();
User user1 = new User(UUID.randomUUID().toString(), "user1", "user1");
User user2 = new User(UUID.randomUUID().toString(), "user2", "user2");
User specialUser = new User(UUID.randomUUID().toString(), "special", "special");
couchbaseTemplate.upsertById(User.class).all(Arrays.asList(user1, user2, specialUser));
Query specialUsers = new Query(QueryCriteria.where("firstname").like("special"));
final List<UserJustLastName> foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(specialUsers).all();
assertEquals(1, foundUsers.size());
final List<UserJustLastName> foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class)
.as(UserJustLastName.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(specialUsers).all()
.collectList().block();
assertEquals(1, foundUsersReactive.size());
}
@Test
void removeByQueryAll() {
User user1 = new User(UUID.randomUUID().toString(), "user1", "user1");
@@ -158,7 +187,7 @@ class CouchbaseTemplateQueryIntegrationTests extends ClusterAwareIntegrationTest
assertTrue(couchbaseTemplate.existsById().one(user1.getId()));
assertTrue(couchbaseTemplate.existsById().one(user2.getId()));
couchbaseTemplate.removeByQuery(User.class).consistentWith(QueryScanConsistency.REQUEST_PLUS).all();
couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all();
assertNull(couchbaseTemplate.findById(User.class).one(user1.getId()));
assertNull(couchbaseTemplate.findById(User.class).one(user2.getId()));
@@ -179,7 +208,7 @@ class CouchbaseTemplateQueryIntegrationTests extends ClusterAwareIntegrationTest
Query nonSpecialUsers = new Query(QueryCriteria.where("firstname").notLike("special"));
couchbaseTemplate.removeByQuery(User.class).consistentWith(QueryScanConsistency.REQUEST_PLUS)
couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS)
.matching(nonSpecialUsers).all();
assertNull(couchbaseTemplate.findById(User.class).one(user1.getId()));
@@ -188,4 +217,94 @@ class CouchbaseTemplateQueryIntegrationTests extends ClusterAwareIntegrationTest
}
@Test
void distinct() {
String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" };
String[] icaos = { "ic0", "ic1", "ic0", "ic1", "ic0", "ic1", "ic0" };
try {
for (int i = 0; i < iatas.length; i++) {
Airport airport = new Airport("airports::" + iatas[i], iatas[i] /*iata*/, icaos[i] /* icao */);
couchbaseTemplate.insertById(Airport.class).one(airport);
}
// distinct and count(distinct(...)) calls. use as() and consistentWith to verify fluent api
// as the fluent api for Distinct is tricky
// distinct icao
List<Airport> airports1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" })
.as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all();
assertEquals(2, airports1.size());
// distinct all-fields-in-Airport.class
List<Airport> airports2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).all();
assertEquals(7, airports2.size());
// count( distinct { iata, icao } )
long count1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" })
.as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).count();
assertEquals(7, count1);
// count( distinct (all fields in icaoClass)
Class icaoClass = (new Object() {
String iata;
String icao;
}).getClass();
long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).count();
assertEquals(7, count2);
} finally {
couchbaseTemplate.removeById()
.all(Arrays.stream(iatas).map((iata) -> "airports::" + iata).collect(Collectors.toSet()));
}
}
@Test
void distinctReactive() {
String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" };
String[] icaos = { "ic0", "ic1", "ic0", "ic1", "ic0", "ic1", "ic0" };
try {
for (int i = 0; i < iatas.length; i++) {
Airport airport = new Airport("airports::" + iatas[i], iatas[i] /*iata*/, icaos[i] /* icao */);
reactiveCouchbaseTemplate.insertById(Airport.class).one(airport).block();
}
// distinct and count(distinct(...)) calls. use as() and consistentWith to verify fluent api
// as the fluent api for Distinct is tricky
// distinct icao
List<Airport> airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" })
.as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all().collectList().block();
assertEquals(2, airports1.size());
// distinct all-fields-in-Airport.class
List<Airport> airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {})
.as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all().collectList().block();
assertEquals(7, airports2.size());
// count( distinct icao )
// not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475
long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" })
.as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).count().block();
assertEquals(2, count1);
// count( distinct (all fields in icaoClass) // which only has one field
// not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475
Class icaoClass = (new Object() {
String icao;
}).getClass();
long count2 = (long) reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).count().block();
assertEquals(2, count2);
} finally {
reactiveCouchbaseTemplate.removeById()
.all(Arrays.stream(iatas).map((iata) -> "airports::" + iata).collect(Collectors.toSet())).collectList()
.block();
}
}
}

View File

@@ -1,17 +1,12 @@
package org.springframework.data.couchbase.domain;
import org.springframework.data.annotation.Id;
import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration;
import org.springframework.data.couchbase.core.mapping.Document;
import org.springframework.data.couchbase.core.mapping.id.GeneratedValue;
import org.springframework.data.couchbase.core.mapping.id.GenerationStrategy;
import java.util.UUID;
@Document
public class Address extends AbstractEntity {
public class Address extends ComparableEntity {
private String street;
private String street;
private String city;
public Address() {}
@@ -23,11 +18,12 @@ public class Address extends AbstractEntity {
this.street = street;
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("{\"street\"=\"");
sb.append(getStreet());
sb.append("\"}");
return sb.toString();
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
}

View File

@@ -23,7 +23,7 @@ import org.springframework.data.couchbase.core.mapping.Document;
@Document
@CompositeQueryIndex(fields = { "id", "name desc" })
public class Airline {
public class Airline extends ComparableEntity {
@Id String id;
@QueryIndexed String name;
@@ -42,15 +42,4 @@ public class Airline {
return name;
}
@Override
public String toString(){
StringBuilder sb=new StringBuilder();
sb.append("airline: { ");
sb.append(" id: ");
sb.append(id);
sb.append(" , name: ");
sb.append(name);
sb.append(" }");
return sb.toString();
}
}

View File

@@ -27,7 +27,7 @@ import org.springframework.data.couchbase.core.mapping.Document;
* @author Michael Reiche
*/
@Document
public class Airport {
public class Airport extends ComparableEntity {
@Id String id;
String iata;
@@ -53,42 +53,4 @@ public class Airport {
return icao;
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("{ id: ");
sb.append(getId());
sb.append(", iata: ");
sb.append(iata);
sb.append(", icao: ");
sb.append(icao);
sb.append(" }");
return sb.toString();
}
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (!(o instanceof Airport)) {
return false;
}
Airport that = (Airport) o;
if (diff(this.id,that.id)) {
return false;
}
if (diff(this.iata,that.iata)) {
return false;
}
if (diff(this.icao,that.icao)) {
return false;
}
return true;
}
private boolean diff(String s1, String s2){
if ((s1 == null && s2 != null) || !s1.equals(s2)) {
return true;
}
return false;
}
}

View File

@@ -16,7 +16,8 @@
package org.springframework.data.couchbase.domain;
import java.lang.reflect.Field;
import com.couchbase.mock.deps.com.google.gson.Gson;
import com.couchbase.mock.deps.com.google.gson.GsonBuilder;
/**
* Comparable entity base class for tests
@@ -26,7 +27,7 @@ import java.lang.reflect.Field;
public class ComparableEntity {
/**
* equals() method that recursively calls equals on on fields
* equals() method that relies on toString()
*
* @param that
* @return
@@ -41,54 +42,12 @@ public class ComparableEntity {
|| !(this.getClass().isAssignableFrom(that.getClass()) || that.getClass().isAssignableFrom(this.getClass()))) {
return false;
}
// check that all the fields in this have an equal field in that
for (Field f : this.getClass().getFields()) {
if (!same(f, this, that)) {
return false;
}
}
// check that all the fields in that have an equal field in this
for (Field f : that.getClass().getFields()) {
if (!same(f, that, this)) {
return false;
}
}
// check that all the declared fields in this have an equal field in that
for (Field f : this.getClass().getDeclaredFields()) {
if (!same(f, this, that)) {
return false;
}
}
// check that all the declared fields in that have an equal field in this
for (Field f : that.getClass().getDeclaredFields()) {
if (!same(f, that, this)) {
return false;
}
}
return true;
return this.toString().equals(that.toString());
}
private static boolean same(Field f, Object a, Object b) {
Object thisField = null;
Object thatField = null;
try {
thisField = f.get(a);
thatField = f.get(b);
} catch (IllegalAccessException e) {
// assume that the important fields are in toString()
thisField = a.toString();
thatField = b.toString();
}
if (thisField == null && thatField == null) {
return true;
}
if (thisField == null && thatField != null) {
return false;
}
if (!thisField.equals(thatField)) {
return false;
}
return true;
public String toString() throws RuntimeException {
Gson gson = new GsonBuilder().create();
return gson.toJson(this);
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2012-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
*
* 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 org.springframework.data.couchbase.domain;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.couchbase.repository.auditing.EnableCouchbaseAuditing;
import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories;
/**
* Configuration that uses a scope. This is a separate class as it is difficult to debug if you forget to unset the
* scopeName and the config is used for non-collection operations.
*
* @Author Michael Reiche
*/
@Configuration
@EnableCouchbaseRepositories
@EnableCouchbaseAuditing // this activates auditing
public class ConfigScoped extends Config {
static String scopeName = null;
@Override
protected String getScopeName() {
return scopeName;
}
public static void setScopeName(String scopeName) {
ConfigScoped.scopeName = scopeName;
}
}

View File

@@ -40,16 +40,4 @@ public class Course extends ComparableEntity {
return id;
}
public String toString() {
StringBuffer sb = new StringBuffer("Course(");
sb.append("id=");
sb.append(id);
sb.append(", userId=");
sb.append(userId);
sb.append(", room=");
sb.append(room);
sb.append(")");
return sb.toString();
}
}

View File

@@ -40,20 +40,4 @@ public class Submission extends ComparableEntity {
return id;
}
public String toString() {
StringBuffer sb = new StringBuffer("Submission(");
sb.append("id=");
sb.append(id);
sb.append(", userId=");
sb.append(userId);
sb.append(", talkId=");
sb.append(talkId);
sb.append(", status=");
sb.append(status);
sb.append(", number=");
sb.append(number);
sb.append(")");
return sb.toString();
}
}

View File

@@ -35,7 +35,7 @@ import org.springframework.data.couchbase.core.mapping.Document;
*/
@Document
public class User {
public class User extends ComparableEntity {
@Version long version;
@Id private String id;
@@ -89,26 +89,9 @@ public class User {
this.version = version;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
User user = (User) o;
return Objects.equals(id, user.id) && Objects.equals(firstname, user.firstname)
&& Objects.equals(lastname, user.lastname);
}
@Override
public int hashCode() {
return Objects.hash(id, firstname, lastname);
}
@Override
public String toString() {
return "User{" + "id='" + id + '\'' + ", firstname='" + firstname + '\'' + ", lastname='" + lastname + '\''
+ ", createdBy='" + createdBy + '\'' + ", createdDate='" + createdDate + '\'' + ", lastModifiedBy='"
+ lastModifiedBy + '\'' + ", lastModifiedDate='" + lastModifiedDate + '\'' + '}';
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2012-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
*
* 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 org.springframework.data.couchbase.domain;
import java.util.Objects;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.couchbase.core.mapping.Document;
/**
* User entity for tests
*
* @author Michael Nitschinger
* @author Michael Reiche
*/
@Document
public class UserJustLastName extends ComparableEntity {
@Id private String id;
private String lastname;
public User user;
@PersistenceConstructor
public UserJustLastName(final String id, final String lastname) {
this.id = id;
this.lastname = lastname;
this.user = new User("1", "first", "last");
}
public String getId() {
return id;
}
public String getLastname() {
return lastname;
}
@Override
public int hashCode() {
return Objects.hash(id, lastname);
}
}

View File

@@ -0,0 +1,47 @@
/*
* 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
*
* 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 org.springframework.data.couchbase.domain;
import lombok.Data;
import java.util.List;
import org.springframework.data.annotation.TypeAlias;
import org.springframework.data.couchbase.core.index.CompositeQueryIndex;
import org.springframework.data.couchbase.core.mapping.Document;
/**
* UserSubmission entity for tests
*
* @author Michael Reiche
*/
@Data
@Document
@TypeAlias("user")
@CompositeQueryIndex(fields = { "id", "username", "email" })
public class UserSubmissionProjected extends ComparableEntity {
private String id;
private String username;
private List<String> roles;
private Address address;
private List<Course> courses;
public void setCourses(List<Course> courses) {
this.courses = courses;
}
}

View File

@@ -56,6 +56,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.couchbase.client.core.error.IndexExistsException;
import reactor.core.publisher.Flux;
/**
* Repository tests
@@ -75,7 +76,7 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
@Autowired UserRepository userRepository;
@BeforeEach
void beforeEach() {
public void beforeEach() {
try {
clientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName());
} catch (IndexExistsException ex) {
@@ -171,12 +172,8 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" };
try {
for (int i = 0; i < iatas.length; i++) {
Airport airport = new Airport("airports::" + iatas[i], iatas[i] /*iata*/,
iatas[i].toLowerCase(Locale.ROOT) /* lcao */);
airportRepository.save(airport);
}
airportRepository.saveAll( Arrays.stream(iatas).map((iata) -> new Airport("airports::"+iata, iata, iata.toLowerCase(Locale.ROOT))).collect(Collectors.toSet()));
Long count = airportRepository.countFancyExpression(asList("JFK"), asList("jfk"), false);
assertEquals(1, count);
@@ -201,10 +198,7 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
assertEquals(0, airportCount);
} finally {
for (int i = 0; i < iatas.length; i++) {
Airport airport = new Airport("airports::" + iatas[i], iatas[i] /*iata*/, iatas[i] /* lcao */);
airportRepository.delete(airport);
}
airportRepository.deleteAllById(Arrays.stream(iatas).map((iata) -> "airports::"+iata).collect(Collectors.toSet()));
}
}

View File

@@ -33,7 +33,6 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
@@ -47,14 +46,12 @@ import org.springframework.data.couchbase.domain.ReactiveUserRepository;
import org.springframework.data.couchbase.domain.User;
import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories;
import org.springframework.data.couchbase.util.Capabilities;
import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests;
import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.IgnoreWhen;
import org.springframework.data.couchbase.util.JavaIntegrationTests;
import org.springframework.data.domain.PageRequest;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.couchbase.client.core.error.IndexExistsException;
/**
* template class for Reactive Couchbase operations
*
@@ -63,22 +60,13 @@ import com.couchbase.client.core.error.IndexExistsException;
*/
@SpringJUnitConfig(ReactiveCouchbaseRepositoryQueryIntegrationTests.Config.class)
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
public class ReactiveCouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegrationTests {
public class ReactiveCouchbaseRepositoryQueryIntegrationTests extends JavaIntegrationTests {
@Autowired CouchbaseClientFactory clientFactory;
@Autowired ReactiveAirportRepository airportRepository; // intellij flags "Could not Autowire", but it runs ok.
@Autowired ReactiveUserRepository userRepository; // intellij flags "Could not Autowire", but it runs ok.
@BeforeEach
void beforeEach() {
try {
clientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName());
} catch (IndexExistsException ex) {
// ignore, all good.
}
}
@Test
void shouldSaveAndFindAll() {
Airport vie = null;

View File

@@ -15,11 +15,16 @@
*/
package org.springframework.data.couchbase.util;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import com.couchbase.client.core.env.Authenticator;
@@ -48,12 +53,18 @@ public abstract class ClusterAwareIntegrationTests {
return testClusterConfig;
}
public static Authenticator authenticator() {
protected static Authenticator authenticator() {
return PasswordAuthenticator.create(config().adminUsername(), config().adminPassword());
}
public static String username() { return config().adminUsername(); }
public static String password() { return config().adminPassword(); }
public static String username() {
return config().adminUsername();
}
public static String password() {
return config().adminPassword();
}
public static String bucketName() {
return config().bucketname();
}
@@ -64,39 +75,71 @@ public abstract class ClusterAwareIntegrationTests {
* @return the connection string to connect.
*/
public static String connectionString() {
/*
return seedNodes().stream().map(s -> {
if (s.kvPort().isPresent()) {
return s.address() + ":" + s.kvPort().get() + "=" + Services.KV;
} else if (s.clusterManagerPort().isPresent()) {
return s.address() + ":" + s.clusterManagerPort().get() + "=" + Services.MANAGER;
} else {
return s.address() ;
}
}).collect(Collectors.joining(","));
*/
StringBuffer sb = new StringBuffer();
for(SeedNode s:seedNodes()) {
for (SeedNode s : seedNodes()) {
if (s.kvPort().isPresent()) {
if(sb.length() > 0 ) sb.append(",");
sb.append (s.address() + ":" + s.kvPort().get() + "=" + Services.KV);
if (sb.length() > 0)
sb.append(",");
sb.append(s.address() + ":" + s.kvPort().get() + "=" + Services.KV);
}
if (s.clusterManagerPort().isPresent()) {
if (sb.length() > 0)
sb.append(",");
sb.append(s.address() + ":" + s.clusterManagerPort().get() + "=" + Services.MANAGER);
}
if(sb.length() == 0 ){
if (sb.length() == 0) {
sb.append(s.address());
}
}
return sb.toString();
}
public static Set<SeedNode> seedNodes() {
protected static Set<SeedNode> seedNodes() {
return config().nodes().stream().map(cfg -> SeedNode.create(cfg.hostname(),
Optional.ofNullable(cfg.ports().get(Services.KV)), Optional.ofNullable(cfg.ports().get(Services.MANAGER))))
.collect(Collectors.toSet());
}
@BeforeAll()
public static void beforeAll() {}
@AfterAll
public static void afterAll() {}
@BeforeEach
public void beforeEach() {}
@AfterEach
public void afterEach() {}
/**
* This should probably be the first call in the @BeforeAll method of a test class.
* This will call super.beforeAll() when called as callSuperBeforeAll(new Object() {}); this trickery is necessary
* because super.beforeAll() cannot be used because it is a static method. it is possible and likely that the
* beforeAll() method of should still be called even when a test class defines its own beforeAll() method which would
* hide the beforeAll() of the super class.
* This trickery is not necessary for before/AfterEach, as those are not static methods
*
* @Author Michael Reiche
*
* @param createdHere - an object from a class defined in the calling class
*/
public static void callSuperBeforeAll(Object createdHere) {
callSuper(createdHere, "beforeAll");
}
// see comments for callSuperBeforeAll()
public static void callSuperAfterAll(Object createdHere) {
callSuper(createdHere, "afterAll");
}
private static void callSuper(Object createdHere, String methodName) {
try {
Method method = createdHere.getClass().getEnclosingClass().getSuperclass().getMethod(methodName);
method.invoke(null);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2021 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 org.springframework.data.couchbase.util;
import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE;
import static org.springframework.data.couchbase.config.BeanNames.REACTIVE_COUCHBASE_TEMPLATE;
import java.time.Duration;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
import org.springframework.data.couchbase.domain.Config;
import com.couchbase.client.core.service.ServiceType;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.Cluster;
import com.couchbase.client.java.ClusterOptions;
import com.couchbase.client.java.env.ClusterEnvironment;
import com.couchbase.client.java.manager.collection.CollectionManager;
import org.springframework.data.couchbase.domain.ConfigScoped;
/**
* Provides Collection support for integration tests
*
* @Author Michael Reiche
*/
public class CollectionAwareIntegrationTests extends JavaIntegrationTests {
public static String scopeName = "scope_" + randomString();
public static String collectionName = "collection_" + randomString();
@BeforeAll
public static void beforeAll() {
callSuperBeforeAll(new Object() {});
ClusterEnvironment environment = environment().build();
Cluster cluster = Cluster.connect(seedNodes(),
ClusterOptions.clusterOptions(authenticator()).environment(environment));
Bucket bucket = cluster.bucket(config().bucketname());
bucket.waitUntilReady(Duration.ofSeconds(5));
waitForService(bucket, ServiceType.QUERY);
waitForQueryIndexerToHaveBucket(cluster, config().bucketname());
CollectionManager collectionManager = bucket.collections();
if (scopeName != null || collectionName != null) {
setupScopeCollection(cluster, scopeName, collectionName, collectionManager);
}
ConfigScoped.setScopeName(scopeName);
ApplicationContext ac = new AnnotationConfigApplicationContext(ConfigScoped.class);
couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE);
reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE);
}
@AfterAll
public static void afterAll(){
System.out.println("CollectionAwareIntegrationTests.afterAll()");
ConfigScoped.setScopeName(null);
callSuperBeforeAll(new Object() {});
}
}

View File

@@ -0,0 +1,359 @@
/*
* Copyright (c) 2018 Couchbase, Inc.
*
* 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 org.springframework.data.couchbase.util;
import static com.couchbase.client.core.util.CbThrowables.hasCause;
import static com.couchbase.client.core.util.CbThrowables.throwIfUnchecked;
import static com.couchbase.client.java.AsyncUtils.block;
import static com.couchbase.client.java.manager.query.CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE;
import static org.springframework.data.couchbase.config.BeanNames.REACTIVE_COUCHBASE_TEMPLATE;
import static org.springframework.data.couchbase.util.Util.waitUntilCondition;
import java.io.IOException;
import java.time.Duration;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Predicate;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Timeout;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.data.couchbase.CouchbaseClientFactory;
import org.springframework.data.couchbase.SimpleCouchbaseClientFactory;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
import com.couchbase.client.core.diagnostics.PingResult;
import com.couchbase.client.core.diagnostics.PingState;
import com.couchbase.client.core.error.CollectionNotFoundException;
import com.couchbase.client.core.error.CouchbaseException;
import com.couchbase.client.core.error.DocumentNotFoundException;
import com.couchbase.client.core.error.IndexExistsException;
import com.couchbase.client.core.error.ParsingFailureException;
import com.couchbase.client.core.error.QueryException;
import com.couchbase.client.core.error.ScopeNotFoundException;
import com.couchbase.client.core.error.UnambiguousTimeoutException;
import com.couchbase.client.core.json.Mapper;
import com.couchbase.client.core.service.ServiceType;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.Cluster;
import com.couchbase.client.java.ClusterOptions;
import com.couchbase.client.java.Collection;
import com.couchbase.client.java.CommonOptions;
import com.couchbase.client.java.Scope;
import com.couchbase.client.java.diagnostics.PingOptions;
import com.couchbase.client.java.env.ClusterEnvironment;
import com.couchbase.client.java.json.JsonObject;
import com.couchbase.client.java.manager.collection.CollectionManager;
import com.couchbase.client.java.manager.collection.CollectionSpec;
import com.couchbase.client.java.manager.collection.ScopeSpec;
import com.couchbase.client.java.manager.query.CreatePrimaryQueryIndexOptions;
import com.couchbase.client.java.manager.search.SearchIndex;
import com.couchbase.client.java.manager.search.UpsertSearchIndexOptions;
import com.couchbase.client.java.query.QueryOptions;
import com.couchbase.client.java.query.QueryResult;
import com.couchbase.client.java.search.SearchQuery;
import com.couchbase.client.java.search.result.SearchResult;
import org.springframework.data.couchbase.domain.Config;
/**
* Extends the {@link ClusterAwareIntegrationTests} with java-client specific code.
*
* @Author Michael Reiche
*/
// Temporarily increased timeout to (possibly) workaround MB-37011 when Developer Preview enabled
@Timeout(value = 10, unit = TimeUnit.MINUTES) // Safety timer so tests can't block CI executors
public class JavaIntegrationTests extends ClusterAwareIntegrationTests {
@Autowired static public CouchbaseTemplate couchbaseTemplate;
@Autowired static public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate;
@BeforeAll
public static void beforeAll() {
callSuperBeforeAll(new Object() {});
try (CouchbaseClientFactory couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectionString(),
authenticator(), bucketName())) {
couchbaseClientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName(),
CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions().ignoreIfExists(true));
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE);
reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE);
}
/**
* Creates a {@link ClusterEnvironment.Builder} which already has the seed nodes and credentials plugged and ready to
* use depending on the environment.
*
* @return the builder, ready to be further modified or used directly.
*/
protected static ClusterEnvironment.Builder environment() {
return ClusterEnvironment.builder();
}
/**
* Returns the pre-set cluster options with the environment and authenticator configured.
*
* @return the cluster options ready to be used.
*/
protected static ClusterOptions clusterOptions() {
return ClusterOptions.clusterOptions(authenticator()).environment(environment().build());
}
/**
* Helper method to create a primary index if it does not exist.
*/
protected static void createPrimaryIndex(final Cluster cluster, final String bucketName) {
cluster.queryIndexes().createPrimaryIndex(bucketName, createPrimaryQueryIndexOptions().ignoreIfExists(true));
}
public static void setupScopeCollection(Cluster cluster, String scopeName, String collectionName,
CollectionManager collectionManager) {
// Create the scope.collection (borrowed from CollectionManagerIntegrationTest )
ScopeSpec scopeSpec = ScopeSpec.create(scopeName);
CollectionSpec collSpec = CollectionSpec.create(collectionName, scopeName);
if (!scopeName.equals("_default")) {
collectionManager.createScope(scopeName);
}
waitUntilCondition(() -> scopeExists(collectionManager, scopeName));
ScopeSpec found = collectionManager.getScope(scopeName);
assertEquals(scopeSpec, found);
collectionManager.createCollection(collSpec);
waitUntilCondition(() -> collectionExists(collectionManager, collSpec));
waitUntilCondition(
() -> collectionReady(cluster.bucket(config().bucketname()).scope(scopeName).collection(collectionName)));
assertNotEquals(scopeSpec, collectionManager.getScope(scopeName));
assertTrue(collectionManager.getScope(scopeName).collections().contains(collSpec));
waitForQueryIndexerToHaveBucket(cluster, collectionName);
// the call to createPrimaryIndex takes about 60 seconds
try {
block(createPrimaryIndex(cluster, config().bucketname(), scopeName, collectionName));
} catch (Exception e) {
e.printStackTrace();
}
waitUntilCondition(
() -> collectionReadyQuery(cluster.bucket(config().bucketname()).scope(scopeName), collectionName));
}
protected static void waitForQueryIndexerToHaveBucket(final Cluster cluster, final String bucketName) {
boolean ready = false;
int guard = 100;
while (!ready && guard != 0) {
guard -= 1;
String statement = "SELECT COUNT(*) > 0 as present FROM system:keyspaces where name = '" + bucketName + "';";
QueryResult queryResult = cluster.query(statement);
List<JsonObject> rows = queryResult.rowsAsObject();
if (rows.size() == 1 && rows.get(0).getBoolean("present")) {
ready = true;
}
if (!ready) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {}
}
}
if (guard == 0) {
throw new IllegalStateException("Query indexer is still not aware of bucket " + bucketName);
}
}
/**
* Improve test stability by waiting for a given service to report itself ready.
*/
protected static void waitForService(final Bucket bucket, final ServiceType serviceType) {
bucket.waitUntilReady(Duration.ofSeconds(30));
Util.waitUntilCondition(() -> {
PingResult pingResult = bucket.ping(PingOptions.pingOptions().serviceTypes(Collections.singleton(serviceType)));
return pingResult.endpoints().containsKey(serviceType) && pingResult.endpoints().get(serviceType).size() > 0
&& pingResult.endpoints().get(serviceType).get(0).state() == PingState.OK;
});
}
public static boolean collectionExists(CollectionManager mgr, CollectionSpec spec) {
try {
ScopeSpec scope = mgr.getScope(spec.scopeName());
return scope.collections().contains(spec);
} catch (CollectionNotFoundException e) {
return false;
}
}
public static boolean collectionReady(Collection collection) {
try {
collection.get("123");
return true;
} catch (DocumentNotFoundException dnfe) {
return true;
} catch (UnambiguousTimeoutException e) {
if (!e.toString().contains("COLLECTION_NOT_FOUND")) {
throw e;
}
return false;
}
}
public static boolean collectionReadyQuery(Scope scope, String collectionName) {
try {
scope.query("select * from `" + collectionName + "` where meta().id=\"1\"");
return true;
} catch (DocumentNotFoundException dnfe) {
return true;
} catch (ParsingFailureException e) {
return false;
}
}
public static boolean scopeExists(CollectionManager mgr, String scopeName) {
try {
mgr.getScope(scopeName);
return true;
} catch (ScopeNotFoundException e) {
return false;
}
}
public static CompletableFuture<Void> createPrimaryIndex(Cluster cluster, String bucketName, String scopeName,
String collectionName) {
CreatePrimaryQueryIndexOptions options = CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions();
options.timeout(Duration.ofSeconds(300));
final CreatePrimaryQueryIndexOptions.Built builtOpts = options.build();
final String indexName = builtOpts.indexName().orElse(null);
String keyspace = "default:`" + bucketName + "`.`" + scopeName + "`.`" + collectionName + "`";
String statement = "CREATE PRIMARY INDEX ";
if (indexName != null) {
statement += (indexName) + " ";
}
statement += "ON " + (keyspace); // do not quote, this might be "default:bucketName.scopeName.collectionName"
return exec(cluster, false, statement, builtOpts.with(), builtOpts).exceptionally(t -> {
if (builtOpts.ignoreIfExists() && hasCause(t, IndexExistsException.class)) {
return null;
}
throwIfUnchecked(t);
throw new RuntimeException(t);
}).thenApply(result -> null);
}
private static CompletableFuture<QueryResult> exec(Cluster cluster,
/*AsyncQueryIndexManager.QueryType queryType*/ boolean queryType, CharSequence statement,
Map<String, Object> with, CommonOptions<?>.BuiltCommonOptions options) {
return with.isEmpty() ? exec(cluster, queryType, statement, options)
: exec(cluster, queryType, statement + " WITH " + Mapper.encodeAsString(with), options);
}
private static CompletableFuture<QueryResult> exec(Cluster cluster,
/*AsyncQueryIndexManager.QueryType queryType,*/ boolean queryType, CharSequence statement,
CommonOptions<?>.BuiltCommonOptions options) {
QueryOptions queryOpts = toQueryOptions(options).readonly(queryType /*requireNonNull(queryType) == READ_ONLY*/);
return cluster.async().query(statement.toString(), queryOpts).exceptionally(t -> {
throw translateException(t);
});
}
private static QueryOptions toQueryOptions(CommonOptions<?>.BuiltCommonOptions options) {
QueryOptions result = QueryOptions.queryOptions();
options.timeout().ifPresent(result::timeout);
options.retryStrategy().ifPresent(result::retryStrategy);
return result;
}
private static final Map<Predicate<QueryException>, Function<QueryException, ? extends QueryException>> errorMessageMap = new LinkedHashMap<>();
private static RuntimeException translateException(Throwable t) {
if (t instanceof QueryException) {
final QueryException e = ((QueryException) t);
for (Map.Entry<Predicate<QueryException>, Function<QueryException, ? extends QueryException>> entry : errorMessageMap
.entrySet()) {
if (entry.getKey().test(e)) {
return entry.getValue().apply(e);
}
}
}
return (t instanceof RuntimeException) ? (RuntimeException) t : new RuntimeException(t);
}
public static void createFtsCollectionIndex(Cluster cluster, String indexName, String bucketName, String scopeName,
String collectionName) {
SearchIndex searchIndex = new SearchIndex(indexName, bucketName);
if (scopeName != null) {
// searchIndex = searchIndex.forScopeCollection(scopeName, collectionName);
throw new RuntimeException("forScopeCollection not implemented in current java client version");
}
cluster.searchIndexes().upsertIndex(searchIndex,
UpsertSearchIndexOptions.upsertSearchIndexOptions().timeout(Duration.ofSeconds(60)));
int maxTries = 5;
for (int i = 0; i < maxTries; i++) {
try {
SearchResult result = cluster.searchQuery(indexName, SearchQuery.queryString("junk"));
break;
} catch (CouchbaseException | IllegalStateException ex) {
// this is a pretty dirty hack to avoid a race where we don't know if the index is ready yet
System.out.println("createFtsCollectionIndex: " + i + " " + ex);
if (i < (maxTries - 1) && (ex.getMessage().contains("no planPIndexes for indexName")
|| ex.getMessage().contains("pindex_consistency mismatched partition")
|| ex.getMessage().contains("pindex not available"))) {
sleepMs(1000);
continue;
}
throw ex;
}
}
}
public static String randomString() {
return UUID.randomUUID().toString().substring(0, 8);
}
public static void sleepMs(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException ie) {}
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright (c) 2018 Couchbase, Inc.
*
* 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 org.springframework.data.couchbase.util;
import java.io.InputStream;
import java.time.Duration;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.awaitility.Awaitility.with;
/**
* Provides a bunch of utility APIs that help with testing.
*/
public class Util {
/**
* Waits and sleeps for a little bit of time until the given condition is met.
*
* <p>Sleeps 1ms between "false" invocations. It will wait at most one minute to prevent hanging forever in case
* the condition never becomes true.</p>
*
* @param supplier return true once it should stop waiting.
*/
public static void waitUntilCondition(final BooleanSupplier supplier) {
waitUntilCondition(supplier, Duration.ofMinutes(1));
}
public static void waitUntilCondition(final BooleanSupplier supplier, Duration atMost) {
with().pollInterval(Duration.ofMillis(1)).await().atMost(atMost).until(supplier::getAsBoolean);
}
public static void waitUntilCondition(final BooleanSupplier supplier, Duration atMost, Duration delay) {
with().pollInterval(delay).await().atMost(atMost).until(supplier::getAsBoolean);
}
public static void waitUntilThrows(final Class<? extends Exception> clazz, final Supplier<Object> supplier) {
with()
.pollInterval(Duration.ofMillis(1))
.await()
.atMost(Duration.ofMinutes(1))
.until(() -> {
try {
supplier.get();
} catch (final Exception ex) {
return ex.getClass().isAssignableFrom(clazz);
}
return false;
});
}
/**
* Returns true if a thread with the given name is currently running.
*
* @param name the name of the thread.
* @return true if running, false otherwise.
*/
public static boolean threadRunning(final String name) {
for (Thread t : Thread.getAllStackTraces().keySet()) {
if (t.getName().equalsIgnoreCase(name)) {
return true;
}
}
return false;
}
/**
* Reads a file from the resources folder (in the same path as the requesting test class).
*
* <p>The class will be automatically loaded relative to the namespace and converted
* to a string.</p>
*
* @param filename the filename of the resource.
* @param clazz the reference class.
* @return the loaded string.
*/
public static String readResource(final String filename, final Class<?> clazz) {
String path = "/" + clazz.getPackage().getName().replace(".", "/") + "/" + filename;
InputStream stream = clazz.getResourceAsStream(path);
java.util.Scanner s = new java.util.Scanner(stream, UTF_8.name()).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
}