DATACOUCH-146 - Reactive repositories support

- Adds experimental support for reactor types in reactive CRUD and Sorting
  repositories using RxJava1Template
- Refactor configuration and query classes to be reusable by reactive repositories

Original pull request: #130.
This commit is contained in:
Subhashni Balakrishnan
2017-01-11 14:55:13 -08:00
parent 56b3842ac1
commit d62f4bbe8c
54 changed files with 4721 additions and 670 deletions

21
pom.xml
View File

@@ -84,6 +84,27 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>${reactor}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxjava</artifactId>
<version>${rxjava}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxjava-reactive-streams</artifactId>
<version>${rxjava-reactive-streams}</version>
<optional>true</optional>
</dependency>
<!-- JSR 303 Validation -->
<dependency>
<groupId>javax.validation</groupId>

View File

@@ -11,6 +11,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration;
import org.springframework.data.couchbase.config.CouchbaseConfigurer;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.WriteResultChecking;
import org.springframework.data.couchbase.core.query.Consistency;
@@ -77,4 +78,10 @@ public class IntegrationTestApplicationConfig extends AbstractCouchbaseConfigura
protected Consistency getDefaultConsistency() {
return Consistency.READ_YOUR_OWN_WRITES;
}
@Override
protected CouchbaseConfigurer couchbaseConfigurer() {
return this;
}
}

View File

@@ -0,0 +1,77 @@
package org.springframework.data.couchbase;
import java.util.Collections;
import java.util.List;
import com.couchbase.client.java.env.CouchbaseEnvironment;
import com.couchbase.client.java.env.DefaultCouchbaseEnvironment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.couchbase.config.AbstractReactiveCouchbaseConfiguration;
import org.springframework.data.couchbase.core.RxJavaCouchbaseTemplate;
import org.springframework.data.couchbase.core.WriteResultChecking;
import org.springframework.data.couchbase.core.query.Consistency;
import org.springframework.data.couchbase.repository.support.IndexManager;
@Configuration
public class ReactiveIntegrationTestApplicationConfig extends AbstractReactiveCouchbaseConfiguration {
@Autowired
private Environment springEnv;
@Bean
public String couchbaseAdminUser() {
return springEnv.getProperty("couchbase.adminUser", "Administrator");
}
@Bean
public String couchbaseAdminPassword() {
return springEnv.getProperty("couchbase.adminUser", "password");
}
@Override
protected List<String> getBootstrapHosts() {
return Collections.singletonList(springEnv.getProperty("couchbase.host", "127.0.0.1"));
}
@Override
protected String getBucketName() {
return springEnv.getProperty("couchbase.bucket", "default");
}
@Override
protected String getBucketPassword() {
return springEnv.getProperty("couchbase.password", "");
}
@Override
protected CouchbaseEnvironment getEnvironment() {
return DefaultCouchbaseEnvironment.builder()
.connectTimeout(10000)
.kvTimeout(10000)
.queryTimeout(10000)
.viewTimeout(10000)
.build();
}
@Override
public RxJavaCouchbaseTemplate reactiveCouchbaseTemplate() throws Exception {
RxJavaCouchbaseTemplate template = super.reactiveCouchbaseTemplate();
template.setWriteResultChecking(WriteResultChecking.LOG);
return template;
}
//this is for dev so it is ok to auto-create indexes
@Override
public IndexManager indexManager() {
return new IndexManager();
}
@Override
protected Consistency getDefaultConsistency() {
return Consistency.READ_YOUR_OWN_WRITES;
}
}

View File

@@ -16,6 +16,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.Id;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.WriteResultChecking;
import org.springframework.data.couchbase.repository.CouchbaseRepository;
import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories;
import org.springframework.stereotype.Repository;
@@ -23,7 +25,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* This test case demonstrates that the {@link AbstractCouchbaseDataConfiguration} can take its SDK beans
* This test case demonstrates that the {@link AbstractCouchbaseConfiguration} can take its SDK beans
* from a sibling {@link Configuration}.
*
* @author Simon Baslé
@@ -31,7 +33,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@SuppressWarnings("SpringJavaAutowiringInspection")
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class AbstractCouchbaseDataConfigurationTest {
public class AbstractCouchbaseConfigurationTest {
@Autowired
ItemRepository repository;
@@ -72,10 +74,10 @@ public class AbstractCouchbaseDataConfigurationTest {
}
@Configuration
@EnableCouchbaseRepositories(basePackageClasses = AbstractCouchbaseDataConfigurationTest.class, considerNestedRepositories = true)
static class Config extends AbstractCouchbaseDataConfiguration {
@EnableCouchbaseRepositories(basePackageClasses = AbstractCouchbaseConfigurationTest.class, considerNestedRepositories = true)
abstract static class Config extends AbstractCouchbaseConfiguration {
@Autowired
@Autowired
Cluster c;
@Autowired
@@ -87,6 +89,25 @@ public class AbstractCouchbaseDataConfigurationTest {
@Autowired
CouchbaseEnvironment e;
//TODO maybe create the bucket if doesn't exist
@Override
protected CouchbaseEnvironment getEnvironment() {
return DefaultCouchbaseEnvironment.builder()
.connectTimeout(10000)
.kvTimeout(10000)
.queryTimeout(10000)
.viewTimeout(10000)
.build();
}
@Override
public CouchbaseTemplate couchbaseTemplate() throws Exception {
CouchbaseTemplate template = super.couchbaseTemplate();
template.setWriteResultChecking(WriteResultChecking.LOG);
return template;
}
@Override
protected CouchbaseConfigurer couchbaseConfigurer() {
return new TestCouchbaseConfigurer(e, c, ci, b);

View File

@@ -93,6 +93,8 @@ public class CouchbaseTemplateTests {
@Test
public void saveSimpleEntityCorrectly() throws Exception {
String id = "beers:awesome-stout";
removeIfExist(id);
String name = "The Awesome Stout";
boolean active = false;
Beer beer = new Beer(id, name, active, "");

View File

@@ -58,4 +58,16 @@ public class CouchbaseTemplateViewListener extends DependencyInjectionTestExecut
client.bucketManager().upsertDesignDocument(designDoc);
}
@Override
public void afterTestClass(final TestContext testContext) throws Exception {
Bucket client = (Bucket) testContext.getApplicationContext().getBean(BeanNames.COUCHBASE_BUCKET);
ClusterInfo clusterInfo = (ClusterInfo) testContext.getApplicationContext().getBean(BeanNames.COUCHBASE_CLUSTER_INFO);
CouchbaseTemplate template = new CouchbaseTemplate(clusterInfo, client);
for (int i = 0; i < 100; i++) {
Beer b = new Beer("testbeer-" + i, "MyBeer" + i, true, "");
template.remove(b);
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.core;
import java.util.Collections;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.cluster.ClusterInfo;
import com.couchbase.client.java.error.DocumentDoesNotExistException;
import com.couchbase.client.java.view.*;
import org.springframework.data.couchbase.config.BeanNames;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import rx.Observable;
/**
* @author Subhashni Balakrishnan
*/
public class ReactiveCouchbaseTemplateViewListener extends DependencyInjectionTestExecutionListener {
@Override
public void beforeTestClass(final TestContext testContext) throws Exception {
Bucket client = (Bucket) testContext.getApplicationContext().getBean(BeanNames.COUCHBASE_BUCKET);
ClusterInfo clusterInfo = (ClusterInfo) testContext.getApplicationContext().getBean(BeanNames.COUCHBASE_CLUSTER_INFO);
populateTestData(client, clusterInfo);
createAndWaitForDesignDocs(client);
}
private void populateTestData(Bucket client, ClusterInfo clusterInfo) {
RxJavaCouchbaseTemplate template = new RxJavaCouchbaseTemplate(clusterInfo, client);
for (int i = 0; i < 100; i++) {
ReactiveBeer b = new ReactiveBeer("testbeer-" + i, "MyBeer" + i, true, "");
template.save(b).subscribe();
}
}
private void createAndWaitForDesignDocs(Bucket client) {
String mapFunction = "function (doc, meta) { if(doc._class == "
+ "\"org.springframework.data.couchbase.core.ReactiveBeer\") { emit(doc.name, null); } }";
View view = DefaultView.create("by_name", mapFunction);
DesignDocument designDoc = DesignDocument.create("reactive_test_beers", Collections.singletonList(view));
client.bucketManager().upsertDesignDocument(designDoc);
}
@Override
public void afterTestClass(final TestContext testContext) throws Exception {
Bucket client = (Bucket) testContext.getApplicationContext().getBean(BeanNames.COUCHBASE_BUCKET);
ClusterInfo clusterInfo = (ClusterInfo) testContext.getApplicationContext().getBean(BeanNames.COUCHBASE_CLUSTER_INFO);
RxJavaCouchbaseTemplate template = new RxJavaCouchbaseTemplate(clusterInfo, client);
for (int i = 0; i < 100; i++) {
ReactiveBeer b = new ReactiveBeer("testbeer-" + i, "MyBeer" + i, true, "");
template.remove(b).subscribe();
}
}
}

View File

@@ -0,0 +1,590 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.core;
import static com.couchbase.client.java.query.Select.select;
import static com.couchbase.client.java.query.dsl.Expression.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.*;
import java.util.*;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.PersistTo;
import com.couchbase.client.java.ReplicateTo;
import com.couchbase.client.java.document.RawJsonDocument;
import com.couchbase.client.java.error.DocumentAlreadyExistsException;
import com.couchbase.client.java.error.DocumentDoesNotExistException;
import com.couchbase.client.java.query.AsyncN1qlQueryResult;
import com.couchbase.client.java.query.N1qlParams;
import com.couchbase.client.java.query.N1qlQuery;
import com.couchbase.client.java.query.consistency.ScanConsistency;
import com.couchbase.client.java.repository.annotation.Field;
import com.couchbase.client.java.view.Stale;
import com.couchbase.client.java.view.ViewQuery;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Version;
import org.springframework.data.couchbase.ReactiveIntegrationTestApplicationConfig;
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
import org.springframework.data.couchbase.core.mapping.Document;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Subhashni Balakrishnan
**/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ReactiveIntegrationTestApplicationConfig.class)
@TestExecutionListeners(ReactiveCouchbaseTemplateViewListener.class)
public class RxJavaCouchbaseTemplateTests {
@Rule
public TestName testName = new TestName();
@Autowired
private Bucket client;
@Autowired
private RxJavaCouchbaseOperations template;
private static final ObjectMapper MAPPER = new ObjectMapper();
private void removeIfExist(String key) {
template.remove(key).subscribe();
}
private void removeCollectionIfExist(Collection<ReactiveBeer> beers) {
template.remove(beers, PersistTo.MASTER, ReplicateTo.NONE)
.subscribe();
}
@Test
public void saveSimpleEntityCorrectly() throws Exception {
String id = "reactivebeers:awesome-stout";
removeIfExist(id);
String name = "The Awesome Stout";
boolean active = false;
ReactiveBeer beer = new ReactiveBeer(id, name, active, "");
template.save(beer)
.subscribe();
RawJsonDocument resultDoc = client.get(id, RawJsonDocument.class);
assertNotNull(resultDoc);
String result = resultDoc.content();
assertNotNull(result);
Map<String, Object> resultConv = MAPPER.readValue(result, new TypeReference<Map<String, Object>>() {
});
assertNotNull(resultConv.get(MappingCouchbaseConverter.TYPEKEY_DEFAULT));
assertNull(resultConv.get("javaClass"));
assertEquals("org.springframework.data.couchbase.core.ReactiveBeer", resultConv.get(MappingCouchbaseConverter.TYPEKEY_DEFAULT));
assertEquals(false, resultConv.get("is_active"));
assertEquals("The Awesome Stout", resultConv.get("name"));
removeIfExist(id);
}
@Test
public void saveCollectionCorrectly() throws Exception {
Collection<ReactiveBeer> beers = new ArrayList<>();
String name = "The Awesome Stout";
for (int i=0; i < 10000; i++) {
beers.add(new ReactiveBeer("beerCollItem" + i, name + i, false, ""));
}
removeCollectionIfExist(beers);
template.save(beers).subscribe();
}
@Test
public void removeDocument() {
String id = "beers:to-delete-stout";
ReactiveBeer beer = new ReactiveBeer(id, "", false, "");
removeIfExist(id);
template.save(beer).subscribe();
Object result = client.get(id);
assertNotNull(result);
template.remove(beer).subscribe();
result = client.get(id);
assertNull(result);
}
@Test
public void storeListsAndMaps() {
String id = "persons:lots-of-names";
List<String> names = new ArrayList<String>();
names.add("Michael");
names.add("Thomas");
names.add(null);
List<Integer> votes = new LinkedList<Integer>();
Map<String, Boolean> info1 = new HashMap<String, Boolean>();
info1.put("foo", true);
info1.put("bar", false);
info1.put("nullValue", null);
Map<String, Integer> info2 = new HashMap<String, Integer>();
ComplexPerson complex = new ComplexPerson(id, names, votes, info1, info2);
template.save(complex).toBlocking();
assertNotNull(client.get(id));
ComplexPerson response = template.findById(id, ComplexPerson.class).toBlocking().single();
assertEquals(names, response.getFirstnames());
assertEquals(votes, response.getVotes());
assertEquals(id, response.getId());
assertEquals(info1, response.getInfo1());
assertEquals(info2, response.getInfo2());
}
@Test
public void validFindById() {
String id = "reactive beers:findme-stout";
String name = "Findme Stout";
boolean active = true;
ReactiveBeer beer = new ReactiveBeer(id, name, active, "");
template.save(beer).subscribe();
ReactiveBeer found = template.findById(id, ReactiveBeer.class).toBlocking().single();
assertNotNull(found);
assertEquals(id, found.getId());
assertEquals(name, found.getName());
assertEquals(active, found.getActive());
}
@Test
public void shouldLoadAndMapViewDocs() {
ViewQuery query = ViewQuery.from("reactive_test_beers", "by_name");
query.stale(Stale.FALSE);
final List<ReactiveBeer> beers = template.findByView(query, ReactiveBeer.class).toList().toBlocking().single();
assertTrue(beers.size() > 0);
for (ReactiveBeer beer : beers) {
assertNotNull(beer.getId());
assertNotNull(beer.getName());
assertNotNull(beer.getActive());
}
}
@Test
public void shouldQueryRaw() {
N1qlQuery query = N1qlQuery.simple(select("name").from(i(client.name()))
.where(x("name").isNotMissing()));
AsyncN1qlQueryResult queryResult = template.queryN1QL(query).toBlocking().single();
assertNotNull(queryResult);
assertTrue(queryResult.errors().toString(), queryResult.finalSuccess().toBlocking().single());
assertFalse(queryResult.rows().toList().toBlocking().single().isEmpty());
}
@Test
public void shouldQueryWithMapping() {
FullFragment ff1 = new FullFragment("fullFragment1", 1, "fullFragment", "test1");
FullFragment ff2 = new FullFragment("fullFragment2", 2, "fullFragment", "test2");
template.save(Arrays.asList(ff1, ff2));
N1qlQuery query = N1qlQuery.simple(select(i("value")) //"value" is a n1ql keyword apparently
.from(i(client.name()))
.where(x("type").eq(s("fullFragment"))
.and(x("criteria").gt(1))),
N1qlParams.build().consistency(ScanConsistency.REQUEST_PLUS));
List<Fragment> fragments = template.findByN1QLProjection(query, Fragment.class).toList().toBlocking().single();
assertNotNull(fragments);
assertFalse(fragments.isEmpty());
assertEquals(1, fragments.size());
assertEquals("test2", fragments.get(0).value);
}
@Test
public void shouldDeserialiseLongsAndInts() {
final long longValue = new Date().getTime();
final int intValue = new Random().nextInt();
template.save(new SimpleWithLongAndInt("simpleWithLong:simple", longValue, intValue)).toBlocking().single();
SimpleWithLongAndInt document = template.findById("simpleWithLong:simple", SimpleWithLongAndInt.class).toBlocking().single();
assertNotNull(document);
assertEquals(longValue, document.getLongValue());
assertEquals(intValue, document.getIntValue());
template.save(new SimpleWithLongAndInt("simpleWithLong:simple:other", intValue, intValue)).toBlocking().single();
document = template.findById("simpleWithLong:simple:other", SimpleWithLongAndInt.class).toBlocking().single();
assertNotNull(document);
assertEquals(intValue, document.getLongValue());
assertEquals(intValue, document.getIntValue());
}
@Test
public void shouldDeserialiseEnums() {
SimpleWithEnum simpleWithEnum = new SimpleWithEnum("simpleWithEnum:enum", SimpleWithEnum.Type.BIG);
template.save(simpleWithEnum).toBlocking().single();
simpleWithEnum = template.findById("simpleWithEnum:enum", SimpleWithEnum.class).toBlocking().single();
assertNotNull(simpleWithEnum);
assertEquals(simpleWithEnum.getType(), SimpleWithEnum.Type.BIG);
}
@Test
public void shouldDeserialiseClass() {
SimpleWithClass simpleWithClass = new SimpleWithClass("simpleWithClass:class", Integer.class);
simpleWithClass.setValue("The dish ran away with the spoon.");
template.save(simpleWithClass).toBlocking().single();
simpleWithClass = template.findById("simpleWithClass:class", SimpleWithClass.class).toBlocking().single();
assertNotNull(simpleWithClass);
assertThat(simpleWithClass.getValue(), equalTo("The dish ran away with the spoon."));
}
@Test
public void expiryWhenTouchOnReadDocument() throws InterruptedException {
String id = "simple-doc-with-update-expiry-for-read";
DocumentWithTouchOnRead doc = new DocumentWithTouchOnRead(id);
template.save(doc).subscribe();
Thread.sleep(1500);
assertNotNull(template.findById(id, DocumentWithTouchOnRead.class).toBlocking().single());
Thread.sleep(1500);
assertNotNull(template.findById(id, DocumentWithTouchOnRead.class).toBlocking().single());
Thread.sleep(3000);
assertNull(template.findById(id, DocumentWithTouchOnRead.class).toBlocking().single());
}
@Test
public void shouldRetainOrderWhenQueryingViewOrdered() {
ViewQuery q = ViewQuery.from("reactive_test_beers", "by_name");
q.descending().includeDocsOrdered(true);
String prev = null;
List<ReactiveBeer> beers = template.findByView(q, ReactiveBeer.class).toList().toBlocking().single();
assertTrue(q.isIncludeDocs());
assertTrue(q.isOrderRetained());
assertEquals(RawJsonDocument.class, q.includeDocsTarget());
for (ReactiveBeer beer : beers) {
if (prev != null) {
assertThat(beer.getName() + " not alphabetically < to " + prev, beer.getName().compareTo(prev) < 0);
}
prev = beer.getName();
}
}
/**
* A sample document with just an id and property.
*/
@Document
static class SimplePerson {
@Id
private final String id;
@Field
private final String name;
public SimplePerson(String id, String name) {
this.id = id;
this.name = name;
}
}
/**
* A sample document that expires in 2 seconds.
*/
@Document(expiry = 2)
static class DocumentWithExpiry {
@Id
private final String id;
public DocumentWithExpiry(String id) {
this.id = id;
}
}
/**
* A sample document that expires in 2 seconds and touchOnRead set.
*/
@Document(expiry = 2, touchOnRead = true)
static class DocumentWithTouchOnRead {
@Id
private final String id;
public DocumentWithTouchOnRead(String id) {
this.id = id;
}
}
@Document
static class ComplexPerson {
@Id
private final String id;
@Field
private final List<String> firstnames;
@Field
private final List<Integer> votes;
@Field
private final Map<String, Boolean> info1;
@Field
private final Map<String, Integer> info2;
public ComplexPerson(String id, List<String> firstnames,
List<Integer> votes, Map<String, Boolean> info1,
Map<String, Integer> info2) {
this.id = id;
this.firstnames = firstnames;
this.votes = votes;
this.info1 = info1;
this.info2 = info2;
}
List<String> getFirstnames() {
return firstnames;
}
List<Integer> getVotes() {
return votes;
}
Map<String, Boolean> getInfo1() {
return info1;
}
Map<String, Integer> getInfo2() {
return info2;
}
String getId() {
return id;
}
}
@Document
static class SimpleWithLongAndInt {
@Id
private String id;
private long longValue;
private int intValue;
SimpleWithLongAndInt(final String id, final long longValue, int intValue) {
this.id = id;
this.longValue = longValue;
this.intValue = intValue;
}
String getId() {
return id;
}
long getLongValue() {
return longValue;
}
void setLongValue(final long value) {
this.longValue = value;
}
public int getIntValue() {
return intValue;
}
public void setIntValue(int intValue) {
this.intValue = intValue;
}
}
static class SimpleWithEnum {
@Id
private String id;
private enum Type {
BIG
}
Type type;
SimpleWithEnum(final String id, final Type type) {
this.id = id;
this.type = type;
}
String getId() {
return id;
}
void setId(final String id) {
this.id = id;
}
Type getType() {
return type;
}
void setType(final Type type) {
this.type = type;
}
}
static class SimpleWithClass {
@Id
private String id;
private Class<Integer> integerClass;
private String value;
SimpleWithClass(final String id, final Class<Integer> integerClass) {
this.id = id;
this.integerClass = integerClass;
}
String getId() {
return id;
}
void setId(final String id) {
this.id = id;
}
Class<Integer> getIntegerClass() {
return integerClass;
}
void setIntegerClass(final Class<Integer> integerClass) {
this.integerClass = integerClass;
}
String getValue() {
return value;
}
void setValue(final String value) {
this.value = value;
}
}
static class VersionedClass {
@Id
private String id;
@Version
private long version;
private String field;
VersionedClass(String id, String field) {
this.id = id;
this.field = field;
}
public String getId() {
return id;
}
public long getVersion() {
return version;
}
public String getField() {
return field;
}
public void setField(String field) {
this.field = field;
}
@Override
public String toString() {
return "VersionedClass{" +
"id='" + id + '\'' +
", version=" + version +
", field='" + field + '\'' +
'}';
}
}
@Document
static class FullFragment {
@Id
private String id;
private long criteria;
private String type;
private String value;
public FullFragment(String id, long criteria, String type, String value) {
this.id = id;
this.criteria = criteria;
this.type = type;
this.value = value;
}
public String getId() {
return id;
}
public long getCriteria() {
return criteria;
}
public String getType() {
return type;
}
public String getValue() {
return value;
}
public void setCriteria(long criteria) {
this.criteria = criteria;
}
public void setType(String type) {
this.type = type;
}
public void setValue(String value) {
this.value = value;
}
}
static class Fragment {
public String value;
}
}

View File

@@ -134,9 +134,9 @@ public class CouchbaseRepositoryViewTests {
String highKey = "uname-11";
List<String> keys = Arrays.asList(lowKey, middleKey, highKey);
User u1 = repository.findByUsernameIs(lowKey);
User u2 = repository.findByUsernameIs(middleKey);
User u3 = repository.findByUsernameIs(highKey);
User u1 = repository.findByUsernameIs(lowKey).get(0);
User u2 = repository.findByUsernameIs(middleKey).get(0);
User u3 = repository.findByUsernameIs(highKey).get(0);
List<User> in = repository.findAllByUsernameIn(keys);

View File

@@ -46,7 +46,7 @@ public interface CustomUserRepository extends CouchbaseRepository<User, String>
long countByUsernameGreaterThanEqualAndUsernameLessThan(String lowBound, String highBound);
@View(viewName = "customFindByNameView")
User findByUsernameIs(String lowKey);
List<User> findByUsernameIs(String lowKey);
@View(viewName = "customFindByNameView")
List<User> findAllByUsernameIn(List<String> keys);

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository;
import static org.junit.Assert.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.data.couchbase.ReactiveIntegrationTestApplicationConfig;
import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping;
import org.springframework.data.couchbase.repository.support.IndexManager;
import org.springframework.data.couchbase.repository.support.ReactiveCouchbaseRepositoryFactory;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* This tests ReactiveSortingRepository features in the Couchbase connector.
*
* @author Subhashni Balakrishnan
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ReactiveIntegrationTestApplicationConfig.class)
@TestExecutionListeners(PartyPopulatorListener.class)
public class ReactiveN1qlCouchbaseRepositoryTests {
@Autowired
private ReactiveRepositoryOperationsMapping operationsMapping;
@Autowired
private IndexManager indexManager;
private ReactivePartySortingRepository repository;
private ReactivePartyRepository partyRepository;
private ItemRepository itemRepository;
private final String KEY_PARTY = "ReactiveParty1";
private final String KEY_ITEM = "ReactiveItem1";
@Before
public void setup() throws Exception {
ReactiveRepositoryFactorySupport factory = new ReactiveCouchbaseRepositoryFactory(operationsMapping, indexManager);
repository = factory.getRepository(ReactivePartySortingRepository.class);
partyRepository = factory.getRepository(ReactivePartyRepository.class);
itemRepository = factory.getRepository(ItemRepository.class);
}
@After
public void cleanUp() {
try { itemRepository.delete(KEY_ITEM); } catch (DataRetrievalFailureException e) {}
try { partyRepository.delete(KEY_PARTY); } catch (DataRetrievalFailureException e) {}
}
@Test
public void shouldFindAllWithSort() {
Iterable<Party> allByAttendanceDesc = repository.findAll(new Sort(Sort.Direction.DESC, "attendees")).collectList().block();
long previousAttendance = Long.MAX_VALUE;
for (Party party : allByAttendanceDesc) {
assertTrue(party.getAttendees() <= previousAttendance);
previousAttendance = party.getAttendees();
}
assertFalse("Expected to find several parties", previousAttendance == Long.MAX_VALUE);
}
@Test
public void shouldSortOnRenamedFieldIfJsonNameIsProvidedInSort() {
Iterable<Party> parties = repository.findAll(new Sort(Sort.Direction.DESC, "desc")).collectList().block();
String previousDesc = null;
for (Party party : parties) {
if (previousDesc != null) {
assertTrue(party.getDescription().compareTo(previousDesc) <= 0);
}
previousDesc = party.getDescription();
}
assertNotNull("Expected to find several parties", previousDesc);
}
@Test
public void shouldSortWithoutCaseSensitivity() {
Iterable<Party> parties = repository.findAll(new Sort(new Sort.Order(Sort.Direction.DESC, "desc").ignoreCase())).collectList().block();
String previousDesc = null;
for(Party party : parties) {
if (previousDesc != null) {
assertTrue(party.getDescription().compareToIgnoreCase(previousDesc) <= 0);
}
previousDesc = party.getDescription();
}
assertNotNull("Expected to find several parties", previousDesc);
}
}

View File

@@ -0,0 +1,60 @@
package org.springframework.data.couchbase.repository;
import java.util.Date;
import org.springframework.data.couchbase.core.query.N1qlSecondaryIndexed;
import org.springframework.data.couchbase.core.query.Query;
import org.springframework.data.couchbase.core.query.View;
import org.springframework.data.couchbase.core.query.ViewIndexed;
import org.springframework.data.repository.query.Param;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @author Subhashni Balakrishnan
*/
@ViewIndexed(designDoc = "party", viewName = "all")
@N1qlSecondaryIndexed(indexName = "party")
public interface ReactivePartyRepository extends ReactiveCouchbaseRepository<Party, String> {
Flux<Party> findByAttendeesGreaterThanEqual(int minAttendees);
Flux<Party> findByEventDateIs(Date targetDate);
@View(designDocument = "party", viewName = "byDate")
Flux<Party> findFirst3ByEventDateGreaterThanEqual(Date targetDate);
Flux<Object> findAllByDescriptionNotNull();
Mono<Long> countAllByDescriptionNotNull();
@Query("SELECT MAX(attendees) FROM #{#n1ql.bucket} WHERE #{#n1ql.filter}")
Mono<Long> findMaxAttendees();
@Query("SELECT `desc` FROM #{#n1ql.bucket} WHERE #{#n1ql.filter}")
Mono<String> findSomeString();
@Query("SELECT count(*) + 5 FROM #{#n1ql.bucket} WHERE #{#n1ql.filter}")
Mono<Long> countCustomPlusFive();
@Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter}")
Mono<Long> countCustom();
@Query("SELECT 1 = 1")
Mono<Boolean> justABoolean();
@Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND `desc` LIKE '%' || $included || '%' AND attendees >= $min" +
" AND `desc` NOT LIKE '%' || $excluded || '%'")
Flux<Party> findAllWithNamedParams(@Param("excluded") String ex, @Param("included") String inc, @Param("min") long minimumAttendees);
@Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND `desc` LIKE '%' || $2 || '%' AND attendees >= $3" +
" AND `desc` NOT LIKE '%' || $1 || '%'")
Flux<Party> findAllWithPositionalParams(String ex, String inc, long minimumAttendees);
@Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND `desc` LIKE '%' || $2 || '%' AND attendees >= $3" +
" AND `desc` NOT LIKE '%' || $1 || '%' AND `desc` != \"this is \\\"$excluded\\\"\"")
Flux<Party> findAllWithPositionalParamsAndQuotedNamedParams(@Param("excluded") String ex, @Param("included") String inc, @Param("min") long min);
Flux<Party> findByDescriptionOrName(String description, String name);
}

View File

@@ -0,0 +1,9 @@
package org.springframework.data.couchbase.repository;
/**
* @author Subhashni Balakrishnan
*/
public interface ReactivePartySortingRepository extends ReactiveCouchbaseSortingRepository<Party, String> {
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository;
import org.springframework.data.annotation.Id;
/**
* @author Subhashni Balakrishnan
*/
public class ReactiveUser {
@Id
private final String key;
private final String username;
private final int age;
public ReactiveUser(String key, String username, int age) {
this.key = key;
this.username = username;
this.age = age;
}
public String getUsername() {
return username;
}
public String getKey() {
return key;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return this.key;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ReactiveUser user = (ReactiveUser) o;
return key.equals(user.key);
}
@Override
public int hashCode() {
return key.hashCode();
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository;
import java.util.List;
import com.couchbase.client.java.view.ViewQuery;
import org.springframework.data.couchbase.core.query.Query;
import org.springframework.data.couchbase.core.query.View;
import reactor.core.publisher.Flux;
/**
* @author Subhashni Balakrishnan
*/
public interface ReactiveUserRepository extends ReactiveCouchbaseRepository<ReactiveUser, String> {
@View(designDocument = "user", viewName = "all")
Flux<ReactiveUser> customViewQuery(ViewQuery query);
@Query("#{#n1ql.selectEntity} WHERE username = $1")
Flux<ReactiveUser> findByUsername(String username);
@Query("SELECT * FROM #{#n1ql.bucket} WHERE username = $1")
Flux<ReactiveUser> findByUsernameBadSelect(String username);
@Query("#{#n1ql.selectEntity} WHERE username LIKE '%-#{3 + 1}'")
Flux<ReactiveUser> findByUsernameWithSpelAndPlaceholder();
@Query
Flux<ReactiveUser> findByUsernameRegexAndUsernameIn(String regex, List<String> sample);
Flux<ReactiveUser> findByUsernameContains(String contains);
Flux<ReactiveUser> findByUsernameNear(String place);//this is to check that there's a N1QL derivation AND it fails
}

View File

@@ -0,0 +1,50 @@
package org.springframework.data.couchbase.repository;
import java.util.Collections;
import java.util.List;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.PersistTo;
import com.couchbase.client.java.ReplicateTo;
import com.couchbase.client.java.cluster.ClusterInfo;
import com.couchbase.client.java.view.DefaultView;
import com.couchbase.client.java.view.DesignDocument;
import com.couchbase.client.java.view.View;
import org.springframework.data.couchbase.config.BeanNames;
import org.springframework.data.couchbase.core.RxJavaCouchbaseTemplate;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
/**
* @author Subhashni Balakrishnan
*/
public class SimpleReactiveCouchbaseRepositoryListener extends DependencyInjectionTestExecutionListener {
@Override
public void beforeTestClass(final TestContext testContext) throws Exception {
Bucket client = (Bucket) testContext.getApplicationContext().getBean(BeanNames.COUCHBASE_BUCKET);
ClusterInfo clusterInfo = (ClusterInfo) testContext.getApplicationContext().getBean(BeanNames.COUCHBASE_CLUSTER_INFO);
populateTestData(client, clusterInfo);
createAndWaitForDesignDocs(client);
}
private void populateTestData(Bucket client, ClusterInfo clusterInfo) {
RxJavaCouchbaseTemplate template = new RxJavaCouchbaseTemplate(clusterInfo, client);
for (int i = 0; i < 100; i++) {
ReactiveUser u = new ReactiveUser("reactivetestuser-" + i, "reactiveuname-" + i, i);
template.save(u, PersistTo.MASTER, ReplicateTo.NONE).subscribe();
}
}
private void createAndWaitForDesignDocs(Bucket client) {
String mapFunction = "function (doc, meta) { if(doc._class == \"org.springframework.data.couchbase.repository." +
"ReactiveUser\") { emit(null, null); } }";
View view = DefaultView.create("all", mapFunction, "_count");
List<View> views = Collections.singletonList(view);
DesignDocument designDoc = DesignDocument.create("reactiveUser", views);
client.bucketManager().upsertDesignDocument(designDoc);
}
}

View File

@@ -0,0 +1,185 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.error.DocumentDoesNotExistException;
import com.couchbase.client.java.view.Stale;
import com.couchbase.client.java.view.ViewQuery;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.couchbase.IntegrationTestApplicationConfig;
import org.springframework.data.couchbase.ReactiveIntegrationTestApplicationConfig;
import org.springframework.data.couchbase.core.CouchbaseQueryExecutionException;
import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping;
import org.springframework.data.couchbase.repository.support.ReactiveCouchbaseRepositoryFactory;
import org.springframework.data.couchbase.repository.support.IndexManager;
import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
/**
* @author Subhashni Balakrishnan
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ReactiveIntegrationTestApplicationConfig.class)
@TestExecutionListeners(SimpleReactiveCouchbaseRepositoryListener.class)
public class SimpleReactiveCouchbaseRepositoryTests {
@Rule
public TestName testName = new TestName();
@Autowired
private Bucket client;
@Autowired
private ReactiveRepositoryOperationsMapping operationsMapping;
@Autowired
private IndexManager indexManager;
private ReactiveUserRepository repository;
@Before
public void setup() throws Exception {
ReactiveRepositoryFactorySupport factory = new ReactiveCouchbaseRepositoryFactory(operationsMapping, indexManager);
repository = factory.getRepository(ReactiveUserRepository.class);
}
private void remove(String key) {
try {
client.remove(key);
} catch (DocumentDoesNotExistException e) {
}
}
@Test
public void simpleCrud() {
String key = "my_unique_user_key";
ReactiveUser instance = new ReactiveUser(key, "foobar", 22);
repository.save(instance).block();
ReactiveUser found = repository.findOne(key).block();
assertEquals(instance.getKey(), found.getKey());
assertEquals(instance.getUsername(), found.getUsername());
assertTrue(repository.exists(key).block());
repository.delete(found).block();
assertNull(repository.findOne(key).block());
assertFalse(repository.exists(key).block());
}
@Test
/**
* This test uses/assumes a default viewName called "all" that is configured on Couchbase.
*/
public void shouldFindAll() {
// do a non-stale query to populate data for testing.
client.query(ViewQuery.from("reactiveUser", "all").stale(Stale.FALSE));
List<ReactiveUser> allUsers = repository.findAll().collectList().block();
int size = 0;
for (ReactiveUser u : allUsers) {
size++;
assertNotNull(u.getKey());
assertNotNull(u.getUsername());
}
assertEquals(100, size);
}
@Test
public void shouldCount() {
// do a non-stale query to populate data for testing.
client.query(ViewQuery.from("reactiveUser", "all").stale(Stale.FALSE));
assertEquals("100", repository.count().block().toString());
}
@Test
public void shouldFindByUsernameUsingN1ql() {
ReactiveUser user = repository.findByUsername("reactiveuname-1").single().block();
assertNotNull(user);
assertEquals("reactivetestuser-1", user.getKey());
assertEquals("reactiveuname-1", user.getUsername());
}
@Test
public void shouldFailFindByUsernameWithNoIdOrCas() {
try {
ReactiveUser user = repository.findByUsernameBadSelect("reactiveuname-1").single().block();
fail("shouldFailFindByUsernameWithNoIdOrCas");
} catch (CouchbaseQueryExecutionException e) {
assertTrue("_ID expected in exception " + e, e.getMessage().contains("_ID"));
assertTrue("_CAS expected in exception " + e, e.getMessage().contains("_CAS"));
} catch (Exception e) {
fail("CouchbaseQueryExecutionException expected");
}
}
@Test
public void shouldFindFromUsernameInlineWithSpelParsing() {
ReactiveUser user = repository.findByUsernameWithSpelAndPlaceholder().take(1).blockLast();
assertNotNull(user);
assert(user.getUsername().startsWith("reactive"));
assert(user.getUsername().startsWith("reactive"));
}
@Test
public void shouldFindFromDeriveQueryWithRegexpAndIn() {
ReactiveUser user = repository.findByUsernameRegexAndUsernameIn("reactiveuname-[123]", Arrays.asList("reactiveuname-2", "reactiveuname-4")).take(1).blockLast();
assertNotNull(user);
assertEquals("reactivetestuser-2", user.getKey());
assertEquals("reactiveuname-2", user.getUsername());
}
@Test
public void shouldFindContainsWithoutAnnotation() {
List<ReactiveUser> users = repository.findByUsernameContains("reactive").collectList().block();
assertNotNull(users);
assertFalse(users.isEmpty());
for (ReactiveUser user : users) {
assertTrue(user.getUsername().startsWith("reactive"));
}
}
@Test
public void shouldDefaultToN1qlQueryDerivation() {
try {
ReactiveUser u = repository.findByUsernameNear("london").single().block();
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
if (!e.getMessage().contains("N1QL")) {
fail(e.getMessage());
}
}
}
}

View File

@@ -34,13 +34,13 @@ public interface UserRepository extends CouchbaseRepository<User, String> {
@View(designDocument = "user", viewName = "all")
Iterable<User> customViewQuery(ViewQuery query);
@Query("#{#n1ql.selectEntity} WHERE username = $1")
@Query("#{#n1ql.selectEntity} WHERE username = $1 and #{#n1ql.filter}")
User findByUsername(String username);
@Query("SELECT * FROM #{#n1ql.bucket} WHERE username = $1")
@Query("SELECT * FROM #{#n1ql.bucket} WHERE username = $1 and #{#n1ql.filter} ")
User findByUsernameBadSelect(String username);
@Query("#{#n1ql.selectEntity} WHERE username LIKE '%-#{3 + 1}'")
@Query("#{#n1ql.selectEntity} WHERE username LIKE '%-#{3 + 1}' and #{#n1ql.filter}'")
User findByUsernameWithSpelAndPlaceholder();
@Query

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors
* Copyright 2012-2017 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.
@@ -16,17 +16,23 @@
package org.springframework.data.couchbase.config;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.Cluster;
import com.couchbase.client.java.CouchbaseCluster;
import com.couchbase.client.java.cluster.ClusterInfo;
import com.couchbase.client.java.env.CouchbaseEnvironment;
import com.couchbase.client.java.env.DefaultCouchbaseEnvironment;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.couchbase.core.*;
import org.springframework.data.couchbase.core.mapping.Document;
import org.springframework.data.couchbase.repository.CouchbaseRepository;
import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Base class for Spring Data Couchbase configuration using JavaConfig.
@@ -34,94 +40,99 @@ import org.springframework.context.annotation.Configuration;
* @author Michael Nitschinger
* @author Simon Baslé
* @author Stephane Nicoll
* @author Subhashni Balakrishnan
*/
@Configuration
public abstract class AbstractCouchbaseConfiguration
extends AbstractCouchbaseDataConfiguration implements CouchbaseConfigurer {
/**
* The list of hostnames (or IP addresses) to bootstrap from.
*
* @return the list of bootstrap hosts.
*/
protected abstract List<String> getBootstrapHosts();
/**
* The name of the bucket to connect to.
*
* @return the name of the bucket.
*/
protected abstract String getBucketName();
/**
* The password of the bucket (can be an empty string).
*
* @return the password of the bucket.
*/
protected abstract String getBucketPassword();
/**
* Is the {@link #getEnvironment()} to be destroyed by Spring?
*
* @return true if Spring should destroy the environment with the context, false otherwise.
*/
protected boolean isEnvironmentManagedBySpring() {
return true;
}
/**
* Override this method if you want a customized {@link CouchbaseEnvironment}.
* This environment will be managed by Spring, which will call its shutdown()
* method upon bean destruction, unless you override {@link #isEnvironmentManagedBySpring()}
* as well to return false.
*
* @return a customized environment, defaults to a {@link DefaultCouchbaseEnvironment}.
*/
protected CouchbaseEnvironment getEnvironment() {
return DefaultCouchbaseEnvironment.create();
}
extends CouchbaseConfigurationSupport {
@Override
protected CouchbaseConfigurer couchbaseConfigurer() {
return this;
}
/**
* Creates a {@link CouchbaseTemplate}.
*
* This uses {@link #mappingCouchbaseConverter()}, {@link #translationService()} and {@link #getDefaultConsistency()}
* for construction.
*
* Additionally, it will expect injection of a {@link ClusterInfo} and a {@link Bucket} beans from the context (most
* probably from another configuration). For a self-sufficient configuration that defines such beans, see
* {@link AbstractCouchbaseConfiguration}.
*
* @throws Exception on Bean construction failure.
*/
@Bean(name = BeanNames.COUCHBASE_TEMPLATE)
public CouchbaseTemplate couchbaseTemplate() throws Exception {
CouchbaseTemplate template = new CouchbaseTemplate(couchbaseConfigurer().couchbaseClusterInfo(),
couchbaseConfigurer().couchbaseClient(), mappingCouchbaseConverter(), translationService());
template.setDefaultConsistency(getDefaultConsistency());
return template;
}
/**
* Creates the {@link RepositoryOperationsMapping} bean which will be used by the framework to choose which
* {@link CouchbaseOperations} should back which {@link CouchbaseRepository}.
* Override {@link #configureRepositoryOperationsMapping(RepositoryOperationsMapping)} in order to customize this.
*
* @throws Exception
*/
@Bean(name = BeanNames.COUCHBASE_OPERATIONS_MAPPING)
public RepositoryOperationsMapping repositoryOperationsMapping(CouchbaseTemplate couchbaseTemplate) throws Exception {
//create a base mapping that associates all repositories to the default template
RepositoryOperationsMapping baseMapping = new RepositoryOperationsMapping(couchbaseTemplate);
//let the user tune it
configureRepositoryOperationsMapping(baseMapping);
return baseMapping;
}
/**
* In order to customize the mapping between repositories/entity types to couchbase templates,
* use the provided mapping's api (eg. in order to have different buckets backing different repositories).
*
* @param mapping the default mapping (will associate all repositories to the default template).
*/
protected void configureRepositoryOperationsMapping(RepositoryOperationsMapping mapping) {
//NO_OP
}
/**
* Scans the mapping base package for classes annotated with {@link Document}.
*
* @throws ClassNotFoundException if initial entity sets could not be loaded.
*/
@Override
@Bean(destroyMethod = "shutdown", name = BeanNames.COUCHBASE_ENV)
public CouchbaseEnvironment couchbaseEnvironment() {
CouchbaseEnvironment env = getEnvironment();
if (isEnvironmentManagedBySpring()) {
return env;
protected Set<Class<?>> getInitialEntitySet() throws ClassNotFoundException {
String basePackage = getMappingBasePackage();
Set<Class<?>> initialEntitySet = new HashSet<Class<?>>();
if (StringUtils.hasText(basePackage)) {
ClassPathScanningCandidateComponentProvider componentProvider = new ClassPathScanningCandidateComponentProvider(false);
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Document.class));
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Persistent.class));
for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) {
initialEntitySet.add(ClassUtils.forName(candidate.getBeanClassName(), AbstractCouchbaseConfiguration.class.getClassLoader()));
}
}
return new CouchbaseEnvironmentNoShutdownProxy(env);
return initialEntitySet;
}
/**
* Returns the {@link Cluster} instance to connect to.
* Return the base package to scan for mapped {@link Document}s. Will return the package name of the configuration
* class (the concrete class, not this one here) by default.
* <p/>
* <p>So if you have a {@code com.acme.AppConfig} extending {@link AbstractCouchbaseConfiguration} the base package
* will be considered {@code com.acme} unless the method is overridden to implement alternate behavior.</p>
*
* @throws Exception on Bean construction failure.
* @return the base package to scan for mapped {@link Document} classes or {@literal null} to not enable scanning for
* entities.
*/
@Override
@Bean(destroyMethod = "disconnect", name = BeanNames.COUCHBASE_CLUSTER)
public Cluster couchbaseCluster() throws Exception {
return CouchbaseCluster.create(couchbaseEnvironment(), getBootstrapHosts());
protected String getMappingBasePackage() {
return getClass().getPackage().getName();
}
@Override
@Bean(name = BeanNames.COUCHBASE_CLUSTER_INFO)
public ClusterInfo couchbaseClusterInfo() throws Exception {
return couchbaseCluster().clusterManager(getBucketName(), getBucketPassword()).info();
}
/**
* Return the {@link Bucket} instance to connect to.
*
* @throws Exception on Bean construction failure.
*/
@Override
@Bean(destroyMethod = "close", name = BeanNames.COUCHBASE_BUCKET)
public Bucket couchbaseClient() throws Exception {
//@Bean method can use another @Bean method in the same @Configuration by directly invoking it
return couchbaseCluster().openBucket(getBucketName(), getBucketPassword());
}
}

View File

@@ -1,249 +0,0 @@
/*
* Copyright 2012-2016 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.config;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.Cluster;
import com.couchbase.client.java.cluster.ClusterInfo;
import com.couchbase.client.java.env.CouchbaseEnvironment;
import com.couchbase.client.java.query.N1qlQuery;
import com.couchbase.client.java.view.ViewQuery;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.convert.CustomConversions;
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService;
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext;
import org.springframework.data.couchbase.core.mapping.Document;
import org.springframework.data.couchbase.core.query.Consistency;
import org.springframework.data.couchbase.core.query.N1qlPrimaryIndexed;
import org.springframework.data.couchbase.core.query.N1qlSecondaryIndexed;
import org.springframework.data.couchbase.core.query.ViewIndexed;
import org.springframework.data.couchbase.repository.CouchbaseRepository;
import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping;
import org.springframework.data.couchbase.repository.support.IndexManager;
import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
@Configuration
public abstract class AbstractCouchbaseDataConfiguration {
protected abstract CouchbaseConfigurer couchbaseConfigurer();
/**
* Creates a {@link CouchbaseTemplate}.
*
* This uses {@link #mappingCouchbaseConverter()}, {@link #translationService()} and {@link #getDefaultConsistency()}
* for construction.
*
* Additionally, it will expect injection of a {@link ClusterInfo} and a {@link Bucket} beans from the context (most
* probably from another configuration). For a self-sufficient configuration that defines such beans, see
* {@link AbstractCouchbaseConfiguration}.
*
* @throws Exception on Bean construction failure.
*/
@Bean(name = BeanNames.COUCHBASE_TEMPLATE)
public CouchbaseTemplate couchbaseTemplate() throws Exception {
CouchbaseTemplate template = new CouchbaseTemplate(couchbaseConfigurer().couchbaseClusterInfo(),
couchbaseConfigurer().couchbaseClient(), mappingCouchbaseConverter(), translationService());
template.setDefaultConsistency(getDefaultConsistency());
return template;
}
/**
* Creates the {@link RepositoryOperationsMapping} bean which will be used by the framework to choose which
* {@link CouchbaseOperations} should back which {@link CouchbaseRepository}.
* Override {@link #configureRepositoryOperationsMapping(RepositoryOperationsMapping)} in order to customize this.
*
* @throws Exception
*/
@Bean(name = BeanNames.COUCHBASE_OPERATIONS_MAPPING)
public RepositoryOperationsMapping repositoryOperationsMapping(CouchbaseTemplate couchbaseTemplate) throws Exception {
//create a base mapping that associates all repositories to the default template
RepositoryOperationsMapping baseMapping = new RepositoryOperationsMapping(couchbaseTemplate);
//let the user tune it
configureRepositoryOperationsMapping(baseMapping);
return baseMapping;
}
/**
* In order to customize the mapping between repositories/entity types to couchbase templates,
* use the provided mapping's api (eg. in order to have different buckets backing different repositories).
*
* @param mapping the default mapping (will associate all repositories to the default template).
*/
protected void configureRepositoryOperationsMapping(RepositoryOperationsMapping mapping) {
//NO_OP
}
/**
* Determines the name of the field that will store the type information for complex types when
* using the {@link #mappingCouchbaseConverter()}.
* Defaults to {@value MappingCouchbaseConverter#TYPEKEY_DEFAULT}.
*
* @see MappingCouchbaseConverter#TYPEKEY_DEFAULT
* @see MappingCouchbaseConverter#TYPEKEY_SYNCGATEWAY_COMPATIBLE
*/
public String typeKey() {
return MappingCouchbaseConverter.TYPEKEY_DEFAULT;
}
/**
* Creates a {@link MappingCouchbaseConverter} using the configured {@link #couchbaseMappingContext}.
*
* @throws Exception on Bean construction failure.
*/
@Bean(name = BeanNames.COUCHBASE_MAPPING_CONVERTER)
public MappingCouchbaseConverter mappingCouchbaseConverter() throws Exception {
MappingCouchbaseConverter converter = new MappingCouchbaseConverter(couchbaseMappingContext(), typeKey());
converter.setCustomConversions(customConversions());
return converter;
}
/**
* Creates a {@link TranslationService}.
*
* @return TranslationService, defaulting to JacksonTranslationService.
*/
@Bean(name = BeanNames.COUCHBASE_TRANSLATION_SERVICE)
public TranslationService translationService() {
final JacksonTranslationService jacksonTranslationService = new JacksonTranslationService();
jacksonTranslationService.afterPropertiesSet();
return jacksonTranslationService;
}
/**
* Creates a {@link CouchbaseMappingContext} equipped with entity classes scanned from the mapping base package.
*
* @throws Exception on Bean construction failure.
*/
@Bean(name = BeanNames.COUCHBASE_MAPPING_CONTEXT)
public CouchbaseMappingContext couchbaseMappingContext() throws Exception {
CouchbaseMappingContext mappingContext = new CouchbaseMappingContext();
mappingContext.setInitialEntitySet(getInitialEntitySet());
mappingContext.setSimpleTypeHolder(customConversions().getSimpleTypeHolder());
mappingContext.setFieldNamingStrategy(fieldNamingStrategy());
return mappingContext;
}
/**
* Register custom Converters in a {@link CustomConversions} object if required. These
* {@link CustomConversions} will be registered with the {@link #mappingCouchbaseConverter()} and
* {@link #couchbaseMappingContext()}. Returns an empty {@link CustomConversions} instance by default.
*
* @return must not be {@literal null}.
*/
@Bean(name = BeanNames.COUCHBASE_CUSTOM_CONVERSIONS)
public CustomConversions customConversions() {
return new CustomConversions(Collections.emptyList());
}
/**
* Register an {@link IndexManager} bean that will be used to process {@link ViewIndexed},
* {@link N1qlPrimaryIndexed} and {@link N1qlSecondaryIndexed} annotations on repositories
* to automatically create indexes. By default, since such automatic creations are discouraged in
* production envrironment, the configuration will assume the worst and will ignore these annotations.
* <p/>
* If you are sure this configuration used in a context where such automatic creations are desired (eg.
* you want automatic index creation in Dev, just not in Prod, and this configuration is the Dev one),
* override the bean and use the {@link IndexManager#IndexManager()} constructor (or
* {@link IndexManager#IndexManager(boolean, boolean, boolean)} constructor with appropriate flags set to true to
* activate).
*/
@Bean(name = BeanNames.COUCHBASE_INDEX_MANAGER)
public IndexManager indexManager() {
return new IndexManager(false, false, false); //this ignores view, N1QL primary and secondary annotations
}
/**
* Scans the mapping base package for classes annotated with {@link Document}.
*
* @throws ClassNotFoundException if initial entity sets could not be loaded.
*/
protected Set<Class<?>> getInitialEntitySet() throws ClassNotFoundException {
String basePackage = getMappingBasePackage();
Set<Class<?>> initialEntitySet = new HashSet<Class<?>>();
if (StringUtils.hasText(basePackage)) {
ClassPathScanningCandidateComponentProvider componentProvider = new ClassPathScanningCandidateComponentProvider(false);
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Document.class));
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Persistent.class));
for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) {
initialEntitySet.add(ClassUtils.forName(candidate.getBeanClassName(), AbstractCouchbaseConfiguration.class.getClassLoader()));
}
}
return initialEntitySet;
}
/**
* Return the base package to scan for mapped {@link Document}s. Will return the package name of the configuration
* class (the concrete class, not this one here) by default.
* <p/>
* <p>So if you have a {@code com.acme.AppConfig} extending {@link AbstractCouchbaseConfiguration} the base package
* will be considered {@code com.acme} unless the method is overridden to implement alternate behavior.</p>
*
* @return the base package to scan for mapped {@link Document} classes or {@literal null} to not enable scanning for
* entities.
*/
protected String getMappingBasePackage() {
return getClass().getPackage().getName();
}
/**
* Set to true if field names should be abbreviated with the {@link CamelCaseAbbreviatingFieldNamingStrategy}.
*
* @return true if field names should be abbreviated, default is false.
*/
protected boolean abbreviateFieldNames() {
return false;
}
/**
* Configures a {@link FieldNamingStrategy} on the {@link CouchbaseMappingContext} instance created.
*
* @return the naming strategy.
*/
protected FieldNamingStrategy fieldNamingStrategy() {
return abbreviateFieldNames() ? new CamelCaseAbbreviatingFieldNamingStrategy() : PropertyNameFieldNamingStrategy.INSTANCE;
}
/**
* Configures the default consistency for generated {@link ViewQuery view queries}
* and {@link N1qlQuery N1QL queries} in repositories.
*
* @return the {@link Consistency consistency} to apply by default on generated queries.
*/
protected Consistency getDefaultConsistency() {
return Consistency.DEFAULT_CONSISTENCY;
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.config;
import java.util.HashSet;
import java.util.Set;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.cluster.ClusterInfo;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.couchbase.core.RxJavaCouchbaseTemplate;
import org.springframework.data.couchbase.core.mapping.Document;
import org.springframework.data.couchbase.repository.ReactiveCouchbaseRepository;
import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Base class for Reactive Spring Data Couchbase configuration using JavaConfig.
*
* @author Subhashni Balakrishnan
*/
@Configuration
public abstract class AbstractReactiveCouchbaseConfiguration extends CouchbaseConfigurationSupport {
@Override
protected CouchbaseConfigurer couchbaseConfigurer() {
return this;
}
/**
* Creates a {@link ReactiveCouchbaseTemplate}.
*
* This uses {@link #mappingCouchbaseConverter()}, {@link #translationService()} and {@link #getDefaultConsistency()}
* for construction.
*
* Additionally, it will expect injection of a {@link ClusterInfo} and a {@link Bucket} beans from the context (most
* probably from another configuration). For a self-sufficient configuration that defines such beans, see
* {@link CouchbaseConfigurationSupport}.
*
* @throws Exception on Bean construction failure.
*/
@Bean(name = BeanNames.RXJAVA1_COUCHBASE_TEMPLATE)
public RxJavaCouchbaseTemplate reactiveCouchbaseTemplate() throws Exception {
RxJavaCouchbaseTemplate template = new RxJavaCouchbaseTemplate(couchbaseConfigurer().couchbaseClusterInfo(),
couchbaseConfigurer().couchbaseClient(), mappingCouchbaseConverter(), translationService());
template.setDefaultConsistency(getDefaultConsistency());
return template;
}
/**
* Creates the {@link ReactiveRepositoryOperationsMapping} bean which will be used by the framework to choose which
* {@link ReactiveCouchbaseOperations} should back which {@link ReactiveCouchbaseRepository}.
* Override {@link #configureReactiveRepositoryOperationsMapping} in order to customize this.
*
* @throws Exception
*/
@Bean(name = BeanNames.REACTIVE_COUCHBASE_OPERATIONS_MAPPING)
public ReactiveRepositoryOperationsMapping reactiveRepositoryOperationsMapping(RxJavaCouchbaseTemplate couchbaseTemplate) throws Exception {
//create a base mapping that associates all repositories to the default template
ReactiveRepositoryOperationsMapping baseMapping = new ReactiveRepositoryOperationsMapping(couchbaseTemplate);
//let the user tune it
configureReactiveRepositoryOperationsMapping(baseMapping);
return baseMapping;
}
/**
* In order to customize the mapping between repositories/entity types to couchbase templates,
* use the provided mapping's api (eg. in order to have different buckets backing different repositories).
*
* @param mapping the default mapping (will associate all repositories to the default template).
*/
protected void configureReactiveRepositoryOperationsMapping(ReactiveRepositoryOperationsMapping mapping) {
//NO_OP
}
/**
* Scans the mapping base package for classes annotated with {@link Document}.
*
* @throws ClassNotFoundException if initial entity sets could not be loaded.
*/
@Override
protected Set<Class<?>> getInitialEntitySet() throws ClassNotFoundException {
String basePackage = getMappingBasePackage();
Set<Class<?>> initialEntitySet = new HashSet<Class<?>>();
if (StringUtils.hasText(basePackage)) {
ClassPathScanningCandidateComponentProvider componentProvider = new ClassPathScanningCandidateComponentProvider(false);
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Document.class));
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Persistent.class));
for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) {
initialEntitySet.add(ClassUtils.forName(candidate.getBeanClassName(), AbstractReactiveCouchbaseConfiguration.class.getClassLoader()));
}
}
return initialEntitySet;
}
/**
* Return the base package to scan for mapped {@link Document}s. Will return the package name of the configuration
* class (the concrete class, not this one here) by default.
* <p/>
* <p>So if you have a {@code com.acme.AppConfig} extending {@link AbstractCouchbaseConfiguration} the base package
* will be considered {@code com.acme} unless the method is overridden to implement alternate behavior.</p>
*
* @return the base package to scan for mapped {@link Document} classes or {@literal null} to not enable scanning for
* entities.
*/
protected String getMappingBasePackage() {
return getClass().getPackage().getName();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors
* Copyright 2012-2017 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.
@@ -23,8 +23,8 @@ import com.couchbase.client.java.env.CouchbaseEnvironment;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.core.RxJavaCouchbaseOperations;
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
import org.springframework.data.support.IsNewStrategyFactory;
/**
* Contains default bean names for Couchbase beans.
@@ -90,6 +90,16 @@ public class BeanNames {
*/
public static final String COUCHBASE_OPERATIONS_MAPPING = "couchbaseRepositoryOperationsMapping";
/**
* The name for the bean that stores custom mapping between reactive repositories and their backing reactiveCouchbaseOperations.
*/
public static final String REACTIVE_COUCHBASE_OPERATIONS_MAPPING = "reactiveCouchbaseRepositoryOperationsMapping";
/**
* The name for the bean that stores custom mapping between rxjava repositories and their backing rxjavaCouchbaseOperations.
*/
public static final String RXJAVA_COUCHBASE_OPERATIONS_MAPPING = "rxJavaCouchbaseRepositoryOperationsMapping";
/**
* The name for the bean that drives how some indexes are automatically created.
*/
@@ -114,4 +124,22 @@ public class BeanNames {
* The name for the bean that will handle audit trail marking of entities.
*/
public static final String COUCHBASE_AUDITING_HANDLER = "couchbaseAuditingHandler";
/**
* The name for the default {@link ReactiveCouchbaseOperations} bean.
*
* See {@link AbstractReactiveCouchbaseConfiguration#reactiveCouchbaseTemplate()} for java config, and
* the "&lt;couchbase:template /&gt;" element for xml config.
*/
public static final String REACTIVE_COUCHBASE_TEMPLATE = "reactiveCouchbaseTemplate";
/**
* The name for the default {@link RxJavaCouchbaseOperations} bean.
*
* See {@link AbstractRxJavaCouchbaseConfiguration#rxjava1CouchbaseTemplate()} for java config, and
* the "&lt;couchbase:template /&gt;" element for xml config.
*/
public static final String RXJAVA1_COUCHBASE_TEMPLATE = "rxjava1CouchbaseTemplate";
}

View File

@@ -0,0 +1,240 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.config;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.Cluster;
import com.couchbase.client.java.CouchbaseCluster;
import com.couchbase.client.java.cluster.ClusterInfo;
import com.couchbase.client.java.env.CouchbaseEnvironment;
import com.couchbase.client.java.env.DefaultCouchbaseEnvironment;
import com.couchbase.client.java.query.N1qlQuery;
import com.couchbase.client.java.view.ViewQuery;
import org.springframework.context.annotation.Bean;
import org.springframework.data.couchbase.core.convert.CustomConversions;
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService;
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext;
import org.springframework.data.couchbase.core.query.Consistency;
import org.springframework.data.couchbase.core.query.N1qlPrimaryIndexed;
import org.springframework.data.couchbase.core.query.N1qlSecondaryIndexed;
import org.springframework.data.couchbase.core.query.ViewIndexed;
import org.springframework.data.couchbase.repository.support.IndexManager;
import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
/**
* @author Subhashni Balakrishnan
*/
public abstract class CouchbaseConfigurationSupport implements CouchbaseConfigurer {
/**
* The list of hostnames (or IP addresses) to bootstrap from.
*
* @return the list of bootstrap hosts.
*/
protected abstract List<String> getBootstrapHosts();
/**
* The name of the bucket to connect to.
*
* @return the name of the bucket.
*/
protected abstract String getBucketName();
/**
* The password of the bucket (can be an empty string).
*
* @return the password of the bucket.
*/
protected abstract String getBucketPassword();
/**
* Is the {@link #getEnvironment()} to be destroyed by Spring?
*
* @return true if Spring should destroy the environment with the context, false otherwise.
*/
protected boolean isEnvironmentManagedBySpring() {
return true;
}
/**
* Override this method if you want a customized {@link CouchbaseEnvironment}.
* This environment will be managed by Spring, which will call its shutdown()
* method upon bean destruction, unless you override {@link #isEnvironmentManagedBySpring()}
* as well to return false.
*
* @return a customized environment, defaults to a {@link DefaultCouchbaseEnvironment}.
*/
protected CouchbaseEnvironment getEnvironment() {
return DefaultCouchbaseEnvironment.create();
}
@Override
@Bean(destroyMethod = "shutdown", name = BeanNames.COUCHBASE_ENV)
public CouchbaseEnvironment couchbaseEnvironment() {
CouchbaseEnvironment env = getEnvironment();
if (isEnvironmentManagedBySpring()) {
return env;
}
return new CouchbaseEnvironmentNoShutdownProxy(env);
}
/**
* Returns the {@link Cluster} instance to connect to.
*
* @throws Exception on Bean construction failure.
*/
@Override
@Bean(destroyMethod = "disconnect", name = BeanNames.COUCHBASE_CLUSTER)
public Cluster couchbaseCluster() throws Exception {
return CouchbaseCluster.create(couchbaseEnvironment(), getBootstrapHosts());
}
@Override
@Bean(name = BeanNames.COUCHBASE_CLUSTER_INFO)
public ClusterInfo couchbaseClusterInfo() throws Exception {
return couchbaseCluster().clusterManager(getBucketName(), getBucketPassword()).info();
}
/**
* Return the {@link Bucket} instance to connect to.
*
* @throws Exception on Bean construction failure.
*/
@Override
@Bean(destroyMethod = "close", name = BeanNames.COUCHBASE_BUCKET)
public Bucket couchbaseClient() throws Exception {
//@Bean method can use another @Bean method in the same @Configuration by directly invoking it
return couchbaseCluster().openBucket(getBucketName(), getBucketPassword());
}
/**
* Determines the name of the field that will store the type information for complex types when
* using the {@link #mappingCouchbaseConverter()}.
* Defaults to {@value MappingCouchbaseConverter#TYPEKEY_DEFAULT}.
*
* @see MappingCouchbaseConverter#TYPEKEY_DEFAULT
* @see MappingCouchbaseConverter#TYPEKEY_SYNCGATEWAY_COMPATIBLE
*/
public String typeKey() {
return MappingCouchbaseConverter.TYPEKEY_DEFAULT;
}
/**
* Creates a {@link MappingCouchbaseConverter} using the configured {@link #couchbaseMappingContext}.
*
* @throws Exception on Bean construction failure.
*/
@Bean(name = BeanNames.COUCHBASE_MAPPING_CONVERTER)
public MappingCouchbaseConverter mappingCouchbaseConverter() throws Exception {
MappingCouchbaseConverter converter = new MappingCouchbaseConverter(couchbaseMappingContext(), typeKey());
converter.setCustomConversions(customConversions());
return converter;
}
/**
* Creates a {@link TranslationService}.
*
* @return TranslationService, defaulting to JacksonTranslationService.
*/
@Bean(name = BeanNames.COUCHBASE_TRANSLATION_SERVICE)
public TranslationService translationService() {
final JacksonTranslationService jacksonTranslationService = new JacksonTranslationService();
jacksonTranslationService.afterPropertiesSet();
return jacksonTranslationService;
}
/**
* Creates a {@link CouchbaseMappingContext} equipped with entity classes scanned from the mapping base package.
*
* @throws Exception on Bean construction failure.
*/
@Bean(name = BeanNames.COUCHBASE_MAPPING_CONTEXT)
public CouchbaseMappingContext couchbaseMappingContext() throws Exception {
CouchbaseMappingContext mappingContext = new CouchbaseMappingContext();
mappingContext.setInitialEntitySet(getInitialEntitySet());
mappingContext.setSimpleTypeHolder(customConversions().getSimpleTypeHolder());
mappingContext.setFieldNamingStrategy(fieldNamingStrategy());
return mappingContext;
}
/**
* Register custom Converters in a {@link CustomConversions} object if required. These
* {@link CustomConversions} will be registered with the {@link #mappingCouchbaseConverter()} and
* {@link #couchbaseMappingContext()}. Returns an empty {@link CustomConversions} instance by default.
*
* @return must not be {@literal null}.
*/
@Bean(name = BeanNames.COUCHBASE_CUSTOM_CONVERSIONS)
public CustomConversions customConversions() {
return new CustomConversions(Collections.emptyList());
}
/**
* Register an {@link IndexManager} bean that will be used to process {@link ViewIndexed},
* {@link N1qlPrimaryIndexed} and {@link N1qlSecondaryIndexed} annotations on repositories
* to automatically create indexes. By default, since such automatic creations are discouraged in
* production envrironment, the configuration will assume the worst and will ignore these annotations.
* <p/>
* If you are sure this configuration used in a context where such automatic creations are desired (eg.
* you want automatic index creation in Dev, just not in Prod, and this configuration is the Dev one),
* override the bean and use the {@link IndexManager#IndexManager()} constructor (or
* {@link IndexManager#IndexManager(boolean, boolean, boolean)} constructor with appropriate flags set to true to
* activate).
*/
@Bean(name = BeanNames.COUCHBASE_INDEX_MANAGER)
public IndexManager indexManager() {
return new IndexManager(false, false, false); //this ignores view, N1QL primary and secondary annotations
}
/**
* Set to true if field names should be abbreviated with the {@link CamelCaseAbbreviatingFieldNamingStrategy}.
*
* @return true if field names should be abbreviated, default is false.
*/
protected boolean abbreviateFieldNames() {
return false;
}
/**
* Configures a {@link FieldNamingStrategy} on the {@link CouchbaseMappingContext} instance created.
*
* @return the naming strategy.
*/
protected FieldNamingStrategy fieldNamingStrategy() {
return abbreviateFieldNames() ? new CamelCaseAbbreviatingFieldNamingStrategy() : PropertyNameFieldNamingStrategy.INSTANCE;
}
/**
* Configures the default consistency for generated {@link ViewQuery view queries}
* and {@link N1qlQuery N1QL queries} in repositories.
*
* @return the {@link Consistency consistency} to apply by default on generated queries.
*/
protected Consistency getDefaultConsistency() {
return Consistency.DEFAULT_CONSISTENCY;
}
protected abstract CouchbaseConfigurer couchbaseConfigurer();
protected abstract Set<Class<?>> getInitialEntitySet() throws ClassNotFoundException;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors
* Copyright 2012-2017 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.
@@ -37,6 +37,7 @@ import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
import org.springframework.data.couchbase.core.query.Consistency;
/**
* Defines common operations on the Couchbase data source, most commonly implemented by {@link CouchbaseTemplate}.
*
@@ -45,9 +46,6 @@ import org.springframework.data.couchbase.core.query.Consistency;
*/
public interface CouchbaseOperations {
String SELECT_ID = "_ID";
String SELECT_CAS = "_CAS";
/**
* Save the given object.
* <p/>
@@ -247,10 +245,7 @@ public interface CouchbaseOperations {
/**
* Query the N1QL Service for JSON data of type T. Enough data to construct the full
* entity is expected to be selected, including the metadata {@value #SELECT_ID} and
* {@value #SELECT_CAS} (document id and cas, obtained through N1QL's
* "{@code META(bucket).id AS} {@value #SELECT_ID}" and
* "{@code META(bucket).cas AS} {@value #SELECT_CAS}").
* entity is expected to be selected, including the metadata (document id and cas), obtained through N1QL's query.
* <p>This is done via a {@link N1qlQuery} that contains a {@link Statement} and possibly
* additional query parameters ({@link N1qlParams}) and placeholder values if the
* statement contains placeholders.

View File

@@ -16,6 +16,7 @@
package org.springframework.data.couchbase.core;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -77,6 +78,9 @@ import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_ID;
import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_CAS;
/**
* @author Michael Nitschinger
* @author Oliver Gierke

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2017 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.core;
import java.util.Collection;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.PersistTo;
import com.couchbase.client.java.ReplicateTo;
import com.couchbase.client.java.cluster.ClusterInfo;
import com.couchbase.client.java.query.AsyncN1qlQueryResult;
import com.couchbase.client.java.query.N1qlQuery;
import com.couchbase.client.java.view.AsyncSpatialViewResult;
import com.couchbase.client.java.view.AsyncViewResult;
import com.couchbase.client.java.view.SpatialViewQuery;
import com.couchbase.client.java.view.ViewQuery;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
import org.springframework.data.couchbase.core.query.Consistency;
import rx.Observable;
/**
* @author Subhashni Balakrishnan
* @since 3.0
*/
public interface RxJavaCouchbaseOperations {
<T>Observable<T> save(T objectToSave);
<T>Observable<T> save(Iterable<T> batchToSave);
<T>Observable<T> save(T objectToSave, PersistTo persistTo, ReplicateTo replicateTo);
<T>Observable<T> save(Iterable<T> batchToSave, PersistTo persistTo, ReplicateTo replicateTo);
<T>Observable<T> remove(T objectToRemove);
<T>Observable<T> remove(T objectToRemove, PersistTo persistTo, ReplicateTo replicateTo);
<T>Observable<T> remove(Iterable<T> batchToRemove);
<T>Observable<T> remove(Iterable<T> batchToRemove, PersistTo persistTo, ReplicateTo replicateTo);
Observable<Boolean> exists(String id);
<T>Observable<T> findById(String id, Class<T> entityClass);
Observable<AsyncN1qlQueryResult> queryN1QL(N1qlQuery n1ql);
Observable<AsyncViewResult> queryView(ViewQuery query);
Observable<AsyncSpatialViewResult> querySpatialView(SpatialViewQuery query);
<T>Observable<T> findByView(ViewQuery query, Class<T> entityClass);
<T>Observable<T> findByN1QL(N1qlQuery n1ql, Class<T> entityClass);
<T>Observable<T> findBySpatialView(SpatialViewQuery query, Class<T> entityClass);
<T>Observable<T> findByN1QLProjection(N1qlQuery n1ql, Class<T> fragmentClass);
Consistency getDefaultConsistency();
/**
* Returns the linked {@link Bucket} to this template.
*
* @return the client used for the template.
*/
Bucket getCouchbaseBucket();
CouchbaseConverter getConverter();
ClusterInfo getCouchbaseClusterInfo();
}

View File

@@ -0,0 +1,366 @@
/*
* Copyright 2017 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.core;
import static org.springframework.data.couchbase.core.CouchbaseTemplate.ensureNotIterable;
import java.util.Collection;
import com.couchbase.client.java.AsyncBucket;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.PersistTo;
import com.couchbase.client.java.ReplicateTo;
import com.couchbase.client.java.cluster.ClusterInfo;
import com.couchbase.client.java.document.Document;
import com.couchbase.client.java.document.RawJsonDocument;
import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.query.*;
import com.couchbase.client.java.view.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService;
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
import org.springframework.data.couchbase.core.mapping.*;
import org.springframework.data.couchbase.core.query.Consistency;
import org.springframework.data.couchbase.core.support.TemplateUtils;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import rx.Observable;
/**
* RxJavaCouchbaseTemplate implements operations using rxjava1 observables
* @author Subhashni Balakrishnan
* @since 3.0
*/
public class RxJavaCouchbaseTemplate implements RxJavaCouchbaseOperations {
private static final Logger LOGGER = LoggerFactory.getLogger(RxJavaCouchbaseTemplate.class);
private Bucket syncClient;
private AsyncBucket client;
private final ClusterInfo clusterInfo;
private final CouchbaseConverter converter;
private final TranslationService translationService;
protected final MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> mappingContext;
private Consistency configuredConsistency = Consistency.DEFAULT_CONSISTENCY;
private static final WriteResultChecking DEFAULT_WRITE_RESULT_CHECKING = WriteResultChecking.NONE;
private WriteResultChecking writeResultChecking = DEFAULT_WRITE_RESULT_CHECKING;
public <T> Observable<T> save(T objectToSave) {
return doPersist(objectToSave, PersistTo.NONE, ReplicateTo.NONE);
}
public <T> Observable<T> save(Iterable<T> batchToSave) {
return Observable.from(batchToSave)
.flatMap(object -> save(object));
}
public <T> Observable<T> save(T objectToSave, PersistTo persistTo, ReplicateTo replicateTo) {
return doPersist(objectToSave, persistTo, replicateTo);
}
public <T> Observable<T> save(Iterable<T> batchToSave, PersistTo persistTo, ReplicateTo replicateTo) {
return Observable.from(batchToSave)
.flatMap(object -> save(object, persistTo, replicateTo));
}
public <T> Observable<T> remove(T objectToRemove) {
return doRemove(objectToRemove, PersistTo.NONE, ReplicateTo.NONE);
}
public <T> Observable<T> remove(Iterable<T> batchToRemove) {
return Observable.from(batchToRemove)
.flatMap(object -> remove(object));
}
public <T> Observable<T> remove(T objectToRemove, PersistTo persistTo, ReplicateTo replicateTo) {
return doRemove(objectToRemove, persistTo, replicateTo);
}
public <T> Observable<T> remove(Iterable<T> batchToRemove, PersistTo persistTo, ReplicateTo replicateTo) {
return Observable.from(batchToRemove)
.flatMap(object -> remove(object, persistTo, replicateTo));
}
public RxJavaCouchbaseTemplate(final ClusterInfo clusterInfo, final Bucket client) {
this(clusterInfo, client, null, null);
}
public RxJavaCouchbaseTemplate(final ClusterInfo clusterInfo, final Bucket client, final TranslationService translationService) {
this(clusterInfo, client, null, translationService);
}
public void setWriteResultChecking(WriteResultChecking writeResultChecking) {
this.writeResultChecking = writeResultChecking == null ? DEFAULT_WRITE_RESULT_CHECKING : writeResultChecking;
}
public RxJavaCouchbaseTemplate(final ClusterInfo clusterInfo, final Bucket client,
final CouchbaseConverter converter,
final TranslationService translationService) {
this.syncClient = client;
this.clusterInfo = clusterInfo;
this.client = client.async();
this.converter = converter == null ? getDefaultConverter() : converter;
this.translationService = translationService == null ? getDefaultTranslationService() : translationService;
this.mappingContext = this.converter.getMappingContext();
}
private RawJsonDocument encodeAndWrap(final CouchbaseDocument source, Long version) {
String encodedContent = translationService.encode(source);
if (version == null) {
return RawJsonDocument.create(source.getId(), source.getExpiration(), encodedContent);
} else {
return RawJsonDocument.create(source.getId(), source.getExpiration(), encodedContent, version);
}
}
private TranslationService getDefaultTranslationService() {
JacksonTranslationService t = new JacksonTranslationService();
t.afterPropertiesSet();
return t;
}
private CouchbaseConverter getDefaultConverter() {
MappingCouchbaseConverter c = new MappingCouchbaseConverter(new CouchbaseMappingContext());
c.afterPropertiesSet();
return c;
}
private final ConvertingPropertyAccessor getPropertyAccessor(Object source) {
CouchbasePersistentEntity<?> entity = mappingContext.getPersistentEntity(source.getClass());
PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source);
return new ConvertingPropertyAccessor(accessor, converter.getConversionService());
}
private <T> Observable<T> doPersist(T objectToPersist, final PersistTo persistTo, final ReplicateTo replicateTo) {
ensureNotIterable(objectToPersist);
final ConvertingPropertyAccessor accessor = getPropertyAccessor(objectToPersist);
final CouchbasePersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(objectToPersist.getClass());
final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty();
final Long version = versionProperty != null ? accessor.getProperty(versionProperty, Long.class) : null;
final CouchbaseDocument converted = new CouchbaseDocument();
converter.write(objectToPersist, converted);
RawJsonDocument doc = encodeAndWrap(converted, version);
return client.upsert(doc, persistTo, replicateTo)
.flatMap(rawJsonDocument -> Observable.just(objectToPersist))
.doOnError(e -> TemplateUtils.translateError(e));
}
private <T> Observable<T> doRemove(T objectToRemove, final PersistTo persistTo, final ReplicateTo replicateTo) {
ensureNotIterable(objectToRemove);
if(objectToRemove instanceof String) {
return client.remove((String) objectToRemove, persistTo, replicateTo)
.flatMap(rawJsonDocument -> Observable.just(objectToRemove))
.doOnError(e -> TemplateUtils.translateError(e));
} else {
final ConvertingPropertyAccessor accessor = getPropertyAccessor(objectToRemove);
final CouchbasePersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(objectToRemove.getClass());
final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty();
final Long version = versionProperty != null ? accessor.getProperty(versionProperty, Long.class) : null;
final CouchbaseDocument converted = new CouchbaseDocument();
converter.write(objectToRemove, converted);
RawJsonDocument doc = encodeAndWrap(converted, version);
return client.remove(doc, persistTo, replicateTo)
.flatMap(rawJsonDocument -> Observable.just(objectToRemove))
.doOnError(e -> TemplateUtils.translateError(e));
}
}
@Override
public Observable<Boolean> exists(String id) {
return client.exists(id)
.doOnError(e -> TemplateUtils.translateError(e));
}
@Override
public Observable<AsyncN1qlQueryResult> queryN1QL(N1qlQuery query) {
return client.query(query)
.doOnError(e -> TemplateUtils.translateError(e));
}
@Override
public Observable<AsyncViewResult> queryView(ViewQuery query) {
return client.query(query)
.doOnError(e -> TemplateUtils.translateError(e));
}
@Override
public Observable<AsyncSpatialViewResult> querySpatialView(SpatialViewQuery query){
return client.query(query)
.doOnError(e -> TemplateUtils.translateError(e));
}
@Override
public <T> Observable<T> findById(String id, Class<T> entityClass) {
final CouchbasePersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass);
if (entity.isTouchOnRead()) {
return client.getAndTouch(id, entity.getExpiry(), RawJsonDocument.class)
.switchIfEmpty(Observable.just(null))
.map(doc -> mapToEntity(id, doc, entityClass))
.doOnError(e -> TemplateUtils.translateError(e));
} else {
return client.get(id, RawJsonDocument.class)
.switchIfEmpty(Observable.just(null))
.map(doc -> mapToEntity(id, doc, entityClass))
.doOnError(e -> TemplateUtils.translateError(e));
}
}
@Override
public <T>Observable<T> findByView(ViewQuery query, Class<T> entityClass) {
if (!query.isIncludeDocs() || !query.includeDocsTarget().equals(RawJsonDocument.class)) {
if (query.isOrderRetained()) {
query.includeDocsOrdered(RawJsonDocument.class);
} else {
query.includeDocs(RawJsonDocument.class);
}
}
//we'll always map the document to the entity, hence reduce never makes sense.
query.reduce(false);
return queryView(query)
.flatMap(asyncViewResult -> asyncViewResult.error()
.flatMap(error -> Observable.error(new CouchbaseQueryExecutionException("Unable to execute view query due to error:" + error.toString())))
.switchIfEmpty(asyncViewResult.rows()))
.map(row -> {
AsyncViewRow asyncViewRow = (AsyncViewRow) row;
return asyncViewRow.document(RawJsonDocument.class)
.map(doc -> mapToEntity(doc.id(), doc, entityClass)).toBlocking().single();
})
.doOnError(throwable -> Observable.error(new CouchbaseQueryExecutionException("Unable to execute view query", throwable)));
}
@Override
public <T>Observable<T> findByN1QL(N1qlQuery query, Class<T> entityClass) {
return queryN1QL(query)
.flatMap(asyncN1qlQueryResult -> asyncN1qlQueryResult.errors()
.flatMap(error -> Observable.error(new CouchbaseQueryExecutionException("Unable to execute n1ql query due to error:" + error.toString())))
.switchIfEmpty(asyncN1qlQueryResult.rows()))
.map(row -> {
JsonObject json = ((AsyncN1qlQueryRow)row).value();
String id = json.getString(TemplateUtils.SELECT_ID);
Long cas = json.getLong(TemplateUtils.SELECT_CAS);
if (id == null || cas == null) {
throw new CouchbaseQueryExecutionException("Unable to retrieve enough metadata for N1QL to entity mapping, " +
"have you selected " + TemplateUtils.SELECT_ID + " and " + TemplateUtils.SELECT_CAS + "?");
}
json = json.removeKey(TemplateUtils.SELECT_ID).removeKey(TemplateUtils.SELECT_CAS);
RawJsonDocument entityDoc = RawJsonDocument.create(id, json.toString(), cas);
T decoded = mapToEntity(id, entityDoc, entityClass);
return decoded;
})
.doOnError(throwable -> Observable.error(new CouchbaseQueryExecutionException("Unable to execute n1ql query", throwable)));
}
@Override
public <T>Observable<T> findBySpatialView(SpatialViewQuery query, Class<T> entityClass) {
return querySpatialView(query)
.flatMap(spatialViewResult -> spatialViewResult.error()
.flatMap(error -> Observable.error(new CouchbaseQueryExecutionException("Unable to execute spatial view query due to error:" + error.toString())))
.switchIfEmpty(spatialViewResult.rows()))
.map(row -> {
AsyncSpatialViewRow asyncSpatialViewRow = (AsyncSpatialViewRow) row;
return asyncSpatialViewRow.document(RawJsonDocument.class)
.map(doc -> mapToEntity(doc.id(), doc, entityClass))
.toBlocking().single();
})
.doOnError(throwable -> Observable.error(new CouchbaseQueryExecutionException("Unable to execute spatial view query", throwable)));
}
@Override
public <T>Observable<T> findByN1QLProjection(N1qlQuery query, Class<T> entityClass) {
return queryN1QL(query)
.flatMap(asyncN1qlQueryResult -> asyncN1qlQueryResult.errors()
.flatMap(error -> Observable.error(new CouchbaseQueryExecutionException("Unable to execute n1ql query due to error:" + error.toString())))
.switchIfEmpty(asyncN1qlQueryResult.rows()))
.map(row -> {
JsonObject json = ((AsyncN1qlQueryRow)row).value();
T decoded = translationService.decodeFragment(json.toString(), entityClass);
return decoded;
})
.doOnError(throwable -> Observable.error(new CouchbaseQueryExecutionException("Unable to execute n1ql query", throwable)));
}
@Override
public Consistency getDefaultConsistency() {
return configuredConsistency;
}
public void setDefaultConsistency(Consistency consistency) {
this.configuredConsistency = consistency;
}
@Override
public CouchbaseConverter getConverter() {
return this.converter;
}
private <T> T mapToEntity(String id, Document<String> data, Class<T> entityClass) {
if (data == null) {
return null;
}
final CouchbaseDocument converted = new CouchbaseDocument(id);
Object readEntity = converter.read(entityClass, (CouchbaseDocument) decodeAndUnwrap(data, converted));
final ConvertingPropertyAccessor accessor = getPropertyAccessor(readEntity);
CouchbasePersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(readEntity.getClass());
if (persistentEntity.hasVersionProperty()) {
accessor.setProperty(persistentEntity.getVersionProperty(), data.cas());
}
return (T) readEntity;
}
/**
* Decode a {@link Document Document&lt;String&gt;} containing a JSON string
* into a {@link CouchbaseStorable}
*/
private CouchbaseStorable decodeAndUnwrap(final Document<String> source, final CouchbaseStorable target) {
//TODO at some point the necessity of CouchbaseStorable should be re-evaluated
return translationService.decode(source.content(), target);
}
@Override
public Bucket getCouchbaseBucket() {
return this.syncClient;
}
@Override
public ClusterInfo getCouchbaseClusterInfo() {
return this.clusterInfo;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2017 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.core.support;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.couchbase.core.CouchbaseExceptionTranslator;
import org.springframework.data.couchbase.core.OperationInterruptedException;
import rx.Observable;
/**
* @author Subhashni Balakrishnan
* @since 3.0
*/
public class TemplateUtils {
public static final String SELECT_ID = "_ID";
public static final String SELECT_CAS = "_CAS";
private static PersistenceExceptionTranslator exceptionTranslator = new CouchbaseExceptionTranslator();
public static Observable translateError(Throwable e) {
if (e instanceof RuntimeException) {
return Observable.error(exceptionTranslator.translateExceptionIfPossible((RuntimeException) e));
}
else if(e instanceof TimeoutException) {
return Observable.error(new QueryTimeoutException(e.getMessage(), e));
}
else if(e instanceof InterruptedException) {
return Observable.error(new OperationInterruptedException(e.getMessage(), e));
}
else if(e instanceof ExecutionException) {
return Observable.error(new OperationInterruptedException(e.getMessage(), e));
} else {
return Observable.error(e);
}
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository;
import java.io.Serializable;
import org.springframework.data.couchbase.core.RxJavaCouchbaseOperations;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
/**
* @author Subhashni Balakrishnan
* @since 3.0
*/
public interface ReactiveCouchbaseRepository<T, ID extends Serializable> extends ReactiveCrudRepository<T, ID> {
/**
* @return a reference to the underlying {@link RxJavaCouchbaseOperations operation template}.
*/
RxJavaCouchbaseOperations getCouchbaseOperations();
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository;
import java.io.Serializable;
import org.springframework.data.repository.reactive.ReactiveSortingRepository;
/**
* Couchbase specific {@link org.springframework.data.repository.Repository} interface that is
* a {@link ReactiveSortingRepository}.
*
* @author Subhashni Balakrishnan
*/
public interface ReactiveCouchbaseSortingRepository<T, ID extends Serializable>
extends ReactiveCouchbaseRepository<T, ID>, ReactiveSortingRepository<T, ID> {
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository.cdi;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import java.lang.annotation.Annotation;
import java.util.Set;
import org.springframework.data.couchbase.core.RxJavaCouchbaseOperations;
import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping;
import org.springframework.data.couchbase.repository.support.IndexManager;
import org.springframework.data.couchbase.repository.support.ReactiveCouchbaseRepositoryFactory;
import org.springframework.data.repository.cdi.CdiRepositoryBean;
import org.springframework.data.repository.config.CustomRepositoryImplementationDetector;
import org.springframework.util.Assert;
/**
* A bean which represents a Couchbase repository.
* @author Subhashni Balakrishnan
*/
public class ReactiveCouchbaseRepositoryBean<T> extends CdiRepositoryBean<T> {
private final Bean<RxJavaCouchbaseOperations> reactiveCouchbaseOperationsBean;
/**
* Creates a new {@link ReactiveCouchbaseRepositoryBean}.
*
* @param reactiveOperations must not be {@literal null}.
* @param qualifiers must not be {@literal null}.
* @param repositoryType must not be {@literal null}.
* @param beanManager must not be {@literal null}.
* @param detector detector for the custom {@link org.springframework.data.repository.Repository} implementations
* {@link org.springframework.data.repository.config.CustomRepositoryImplementationDetector}, can be {@literal null}.
*/
public ReactiveCouchbaseRepositoryBean(Bean<RxJavaCouchbaseOperations> reactiveOperations, Set<Annotation> qualifiers, Class<T> repositoryType,
BeanManager beanManager, CustomRepositoryImplementationDetector detector) {
super(qualifiers, repositoryType, beanManager, detector);
Assert.notNull(reactiveOperations, "Cannot create repository with 'null' for ReactiveCouchbaseOperations.");
this.reactiveCouchbaseOperationsBean = reactiveOperations;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.cdi.CdiRepositoryBean#create(javax.enterprise.context.spi.CreationalContext, java.lang.Class, java.lang.Object)
*/
@Override
protected T create(CreationalContext<T> creationalContext, Class<T> repositoryType, Object customImplementation) {
RxJavaCouchbaseOperations reactiveCouchbaseOperations = getDependencyInstance(reactiveCouchbaseOperationsBean, RxJavaCouchbaseOperations.class);
ReactiveRepositoryOperationsMapping reactiveCouchbaseOperationsMapping = new ReactiveRepositoryOperationsMapping(reactiveCouchbaseOperations);
IndexManager indexManager = new IndexManager();
return new ReactiveCouchbaseRepositoryFactory(reactiveCouchbaseOperationsMapping, indexManager).getRepository(repositoryType, customImplementation);
}
@Override
public Class<? extends Annotation> getScope() {
return reactiveCouchbaseOperationsBean.getScope();
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository.cdi;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.UnsatisfiedResolutionException;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.ProcessBean;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.core.RxJavaCouchbaseOperations;
import org.springframework.data.repository.cdi.CdiRepositoryBean;
import org.springframework.data.repository.cdi.CdiRepositoryExtensionSupport;
/**
* A portable CDI extension which registers beans for Spring Data Couchbase repositories.
* @author Mark Paluch
*/
public class ReactiveCouchbaseRepositoryExtension extends CdiRepositoryExtensionSupport{
private final Map<Set<Annotation>, Bean<RxJavaCouchbaseOperations>> reactiveCouchbaseOperationsMap = new HashMap<Set<Annotation>, Bean<RxJavaCouchbaseOperations>>();
/**
* Implementation of a an observer which checks for CouchbaseOperations beans and stores them in {@link #reactiveCouchbaseOperationsMap} for
* later association with corresponding repository beans.
*
* @param <T> The type.
* @param processBean The annotated type as defined by CDI.
*/
@SuppressWarnings("unchecked")
<T> void processBean(@Observes ProcessBean<T> processBean) {
Bean<T> bean = processBean.getBean();
for (Type type : bean.getTypes()) {
if (type instanceof Class<?> && CouchbaseOperations.class.isAssignableFrom((Class<?>) type)) {
reactiveCouchbaseOperationsMap.put(bean.getQualifiers(), ((Bean<RxJavaCouchbaseOperations>) bean));
}
}
}
/**
* Implementation of a an observer which registers beans to the CDI container for the detected Spring Data
* repositories.
* <p>
* The repository beans are associated to the CouchbaseOperations using their qualifiers.
*
* @param beanManager The BeanManager instance.
*/
void afterBeanDiscovery(@Observes AfterBeanDiscovery afterBeanDiscovery, BeanManager beanManager) {
for (Map.Entry<Class<?>, Set<Annotation>> entry : getRepositoryTypes()) {
Class<?> repositoryType = entry.getKey();
Set<Annotation> qualifiers = entry.getValue();
CdiRepositoryBean<?> repositoryBean = createRepositoryBean(repositoryType, qualifiers, beanManager);
afterBeanDiscovery.addBean(repositoryBean);
registerBean(repositoryBean);
}
}
/**
* Creates a {@link Bean}.
*
* @param <T> The type of the repository.
* @param repositoryType The class representing the repository.
* @param beanManager The BeanManager instance.
* @return The bean.
*/
private <T> CdiRepositoryBean<T> createRepositoryBean(Class<T> repositoryType, Set<Annotation> qualifiers, BeanManager beanManager) {
Bean<RxJavaCouchbaseOperations> reactiveCouchbaseOperationsBean = this.reactiveCouchbaseOperationsMap.get(qualifiers);
if (reactiveCouchbaseOperationsBean == null) {
throw new UnsatisfiedResolutionException(String.format("Unable to resolve a bean for '%s' with qualifiers %s.",
CouchbaseOperations.class.getName(), qualifiers));
}
return new ReactiveCouchbaseRepositoryBean<T>(reactiveCouchbaseOperationsBean, qualifiers, repositoryType, beanManager, getCustomImplementationDetector());
}
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository.config;
import java.lang.annotation.*;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.data.couchbase.config.BeanNames;
import org.springframework.data.couchbase.repository.support.ReactiveCouchbaseRepositoryFactoryBean;
import org.springframework.data.repository.config.DefaultRepositoryBaseClass;
/**
* Annotation to activate reactive couchbase repositories. If no base package is configured through either {@link #value()},
* {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of annotated class.
*
* @author Subhashni Balakrishnan
* @since 3.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(ReactiveCouchbaseRepositoriesRegistrar.class)
public @interface EnableReactiveCouchbaseRepositories {
/**
* Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.:
* {@code @EnableCouchbaseRepositories("org.my.pkg")} instead of {@code @EnableCouchbaseRepositories(basePackages="org.my.pkg")}.
*/
String[] value() default {};
/**
* Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with) this
* attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names.
*/
String[] basePackages() default {};
/**
* Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components. The
* package of each class specified will be scanned. Consider creating a special no-op marker class or interface in
* each package that serves no purpose other than being referenced by this attribute.
*/
Class<?>[] basePackageClasses() default {};
/**
* Specifies which types are eligible for component scanning. Further narrows the set of candidate components from
* everything in {@link #basePackages()} to everything in the base packages that matches the given filter or filters.
*/
ComponentScan.Filter[] includeFilters() default {};
/**
* Specifies which types are not eligible for component scanning.
*/
ComponentScan.Filter[] excludeFilters() default {};
/**
* Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So
* for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning
* for {@code PersonRepositoryImpl}.
*
* @return
*/
String repositoryImplementationPostfix() default "";
/**
* Configures the location of where to find the Spring Data named queries properties file. Will default to
* {@code META-INFO/couchbase-named-queries.properties}.
*
* @return
*/
String namedQueriesLocation() default "";
/**
* Configure the repository base class to be used to create repository proxies for this particular configuration.
*
* @return
*/
Class<?> repositoryBaseClass() default DefaultRepositoryBaseClass.class;
/**
* Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to
* {@link ReactiveCouchbaseRepositoryFactoryBean}.
*
* @return
*/
Class<?> repositoryFactoryBeanClass() default ReactiveCouchbaseRepositoryFactoryBean.class;
/**
* Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the
* repositories infrastructure.
*/
boolean considerNestedRepositories() default false;
/**
* Configures the name of the {@link ReactiveCouchbaseTemplate} bean to be used by default with the repositories detected.
*
* @return
*/
String couchbaseTemplateRef() default BeanNames.REACTIVE_COUCHBASE_TEMPLATE;
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository.config;
import java.lang.annotation.Annotation;
import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport;
import org.springframework.data.repository.config.RepositoryConfigurationExtension;
/**
* @author Subhashni Balakrishnan
* @since 3.0
*/
public class ReactiveCouchbaseRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport {
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getAnnotation()
*/
@Override
protected Class<? extends Annotation> getAnnotation() {
return EnableReactiveCouchbaseRepositories.class;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getExtension()
*/
@Override
protected RepositoryConfigurationExtension getExtension() {
return new ReactiveCouchbaseRepositoryConfigurationExtension();
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository.config;
import org.springframework.data.couchbase.repository.support.ReactiveCouchbaseRepositoryFactoryBean;
import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
import org.springframework.data.repository.config.XmlRepositoryConfigurationSource;
import org.w3c.dom.Element;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.data.config.ParsingUtils;
import org.springframework.data.couchbase.config.BeanNames;
import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource;
/**
* @author Subhashni Balakrishnan
* @since 3.0
*/
public class ReactiveCouchbaseRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport {
/** The reference property to use in xml configuration to specify the template to use with a repository. */
private static final String REACTIVE_COUCHBASE_TEMPLATE_REF = "reactive-couchbase-template-ref";
/** The reference property to use in xml configuration to specify the index manager bean to use with a repository. */
private static final String COUCHBASE_INDEX_MANAGER_REF = "couchbase-index-manager-ref";
@Override
protected String getModulePrefix() {
return "reactive-couchbase";
}
public String getRepositoryFactoryClassName() {
return ReactiveCouchbaseRepositoryFactoryBean.class.getName();
}
@Override
public void postProcess(final BeanDefinitionBuilder builder, final XmlRepositoryConfigurationSource config) {
Element element = config.getElement();
ParsingUtils.setPropertyReference(builder, element, REACTIVE_COUCHBASE_TEMPLATE_REF, "reactiveCouchbaseOperations");
ParsingUtils.setPropertyReference(builder, element, COUCHBASE_INDEX_MANAGER_REF, "indexManager");
}
@Override
public void postProcess(final BeanDefinitionBuilder builder, final AnnotationRepositoryConfigurationSource config) {
builder.addDependsOn(BeanNames.REACTIVE_COUCHBASE_OPERATIONS_MAPPING);
builder.addDependsOn(BeanNames.COUCHBASE_INDEX_MANAGER);
builder.addPropertyReference("couchbaseOperationsMapping", BeanNames.REACTIVE_COUCHBASE_OPERATIONS_MAPPING);
builder.addPropertyReference("indexManager", BeanNames.COUCHBASE_INDEX_MANAGER);
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository.config;
import java.util.HashMap;
import java.util.Map;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.core.RxJavaCouchbaseOperations;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.util.Assert;
/**
* @author Subhashni Balakrishnan
* @since 3.0
*/
public class ReactiveRepositoryOperationsMapping {
private RxJavaCouchbaseOperations defaultOperations;
private Map<String, RxJavaCouchbaseOperations> byRepository = new HashMap<String, RxJavaCouchbaseOperations>();
private Map<String, RxJavaCouchbaseOperations> byEntity = new HashMap<String, RxJavaCouchbaseOperations>();
/**
* Creates a new mapping, setting the default fallback to use by otherwise non mapped repositories.
*
* @param defaultOperations the default fallback reactive couchbase operations.
*/
public ReactiveRepositoryOperationsMapping(RxJavaCouchbaseOperations defaultOperations) {
Assert.notNull(defaultOperations);
this.defaultOperations = defaultOperations;
}
/**
* Change the default reactive couchbase operations in an existing mapping.
*
* @param aDefault the new default couchbase operations.
* @return the mapping, for chaining.
*/
public ReactiveRepositoryOperationsMapping setDefault(RxJavaCouchbaseOperations aDefault) {
Assert.notNull(aDefault);
this.defaultOperations = aDefault;
return this;
}
/**
* Add a highest priority mapping that will associate a specific repository interface with a given
* {@link RxJavaCouchbaseOperations}.
*
* @param repositoryInterface the repository interface {@link Class}.
* @param operations the ReactiveCouchbaseOperations to use.
* @return the mapping, for chaining.
*/
public ReactiveRepositoryOperationsMapping map(Class<?> repositoryInterface, RxJavaCouchbaseOperations operations) {
byRepository.put(repositoryInterface.getName(), operations);
return this;
}
/**
* Add a middle priority mapping that will associate any un-mapped repository that deals with the given domain type
* Class with a given {@link CouchbaseOperations}.
*
* @param entityClass the domain type's {@link Class}.
* @param operations the CouchbaseOperations to use.
* @return the mapping, for chaining.
*/
public ReactiveRepositoryOperationsMapping mapEntity(Class<?> entityClass, RxJavaCouchbaseOperations operations) {
byEntity.put(entityClass.getName(), operations);
return this;
}
/**
* @return the configured default {@link RxJavaCouchbaseOperations}.
*/
public RxJavaCouchbaseOperations getDefault() {
return defaultOperations;
}
/**
* Get the {@link MappingContext} to use in repositories. It is extracted from the default {@link RxJavaCouchbaseOperations}.
*
* @return the mapping context.
*/
public MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> getMappingContext() {
return defaultOperations.getConverter().getMappingContext();
}
/**
* Given a repository interface and its domain type, resolves which {@link RxJavaCouchbaseOperations} it should be backed with.
*
* Starts by looking for a direct mapping to the interface, then a common mapping for the domain type, then falls back
* to the default CouchbaseOperations.
*
* @param repositoryInterface the repository's interface.
* @param domainType the repository's domain type / entity.
* @return the CouchbaseOperations to back the repository.
*/
public RxJavaCouchbaseOperations resolve(Class<?> repositoryInterface, Class<?> domainType) {
RxJavaCouchbaseOperations result = byRepository.get(repositoryInterface.getName());
if (result != null) {
return result;
} else {
result = byEntity.get(domainType.getName());
if (result != null) {
return result;
} else {
return defaultOperations;
}
}
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository.query;
import java.util.Map;
import com.couchbase.client.java.document.json.JsonValue;
import com.couchbase.client.java.query.N1qlQuery;
import com.couchbase.client.java.query.Statement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.couchbase.core.RxJavaCouchbaseOperations;
import org.springframework.data.couchbase.repository.query.support.N1qlUtils;
import org.springframework.data.repository.query.*;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import reactor.core.publisher.Flux;
/**
* @author Subhashni Balakrishnan
* @since 3.0
*/
public abstract class ReactiveAbstractN1qlBasedQuery implements RepositoryQuery {
private static final Logger LOG = LoggerFactory.getLogger(ReactiveAbstractN1qlBasedQuery.class);
protected final CouchbaseQueryMethod queryMethod;
private final RxJavaCouchbaseOperations couchbaseOperations;
protected ReactiveAbstractN1qlBasedQuery(CouchbaseQueryMethod method, RxJavaCouchbaseOperations operations) {
this.queryMethod = method;
this.couchbaseOperations = operations;
}
protected abstract Statement getStatement(ParameterAccessor accessor, Object[] runtimeParameters, ReturnedType returnedType);
protected abstract JsonValue getPlaceholderValues(ParameterAccessor accessor);
@Override
public Object execute(Object[] parameters) {
ReactiveCouchbaseParameterAccessor accessor = new ReactiveCouchbaseParameterAccessor(queryMethod, parameters);
ResultProcessor processor = this.queryMethod.getResultProcessor().withDynamicProjection(accessor);
ReturnedType returnedType = processor.getReturnedType();
Class<?> typeToRead = returnedType.getTypeToRead();
typeToRead = typeToRead == null ? returnedType.getDomainType() : typeToRead;
Statement statement = getStatement(accessor, parameters, returnedType);
JsonValue queryPlaceholderValues = getPlaceholderValues(accessor);
//prepare the final query
N1qlQuery query = N1qlUtils.buildQuery(statement, queryPlaceholderValues,
getCouchbaseOperations().getDefaultConsistency().n1qlConsistency());
return ReactiveWrapperConverters.toWrapper(
processor.processResult(executeDependingOnType(query, queryMethod, typeToRead)), Flux.class);
}
protected Object executeDependingOnType(N1qlQuery query,
QueryMethod queryMethod,
Class<?> typeToRead) {
if (queryMethod.isModifyingQuery()) {
throw new UnsupportedOperationException("Modifying queries not yet supported");
}
if (queryMethod.isQueryForEntity()) {
return execute(query, typeToRead);
} else {
return executeSingleProjection(query);
}
}
private void logIfNecessary(N1qlQuery query) {
if (LOG.isDebugEnabled()) {
LOG.debug("Executing N1QL query: " + query.n1ql());
}
}
protected Object execute(N1qlQuery query, Class<?> typeToRead) {
logIfNecessary(query);
return couchbaseOperations.findByN1QL(query, typeToRead);
}
protected Object executeSingleProjection(N1qlQuery query) {
logIfNecessary(query);
return couchbaseOperations.findByN1QLProjection(query, Map.class);
}
@Override
public CouchbaseQueryMethod getQueryMethod() {
return this.queryMethod;
}
protected RxJavaCouchbaseOperations getCouchbaseOperations() {
return this.couchbaseOperations;
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository.query;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.data.repository.util.ReactiveWrappers;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
/**
* Reactive {@link org.springframework.data.repository.query.ParametersParameterAccessor} implementation that subscribes
* to reactive parameter wrapper types upon creation. This class performs synchronization when accessing parameters.
*
* @author Subhashni Balakrishnan
* @since 3.0
*/
public class ReactiveCouchbaseParameterAccessor extends ParametersParameterAccessor {
private final Object[] values;
private final List<MonoProcessor<?>> subscriptions;
public ReactiveCouchbaseParameterAccessor(CouchbaseQueryMethod method, Object[] values) {
super(method.getParameters(), values);
this.values = values;
this.subscriptions = new ArrayList<>(values.length);
for (int i = 0; i < values.length; i++) {
Object value = values[i];
if (value == null || !ReactiveWrappers.supports(value.getClass())) {
subscriptions.add(null);
continue;
}
if (ReactiveWrappers.isSingleValueType(value.getClass())) {
subscriptions.add(ReactiveWrapperConverters.toWrapper(value, Mono.class).subscribe());
} else {
subscriptions.add(ReactiveWrapperConverters.toWrapper(value, Flux.class).collectList().subscribe());
}
}
}
/* (non-Javadoc)
* @see org.springframework.data.repository.query.ParametersParameterAccessor#getValue(int)
*/
@SuppressWarnings("unchecked")
@Override
protected <T> T getValue(int index) {
if (subscriptions.get(index) != null) {
return (T) subscriptions.get(index).block();
}
return super.getValue(index);
}
/* (non-Javadoc)
* @see org.springframework.data.repository.query.ParametersParameterAccessor#getBindableValue(int)
*/
public Object getBindableValue(int index) {
return getValue(getParameters().getBindableParameter(index).getIndex());
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository.query;
import static com.couchbase.client.java.query.Select.select;
import static com.couchbase.client.java.query.dsl.functions.AggregateFunctions.count;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.document.json.JsonValue;
import com.couchbase.client.java.query.Statement;
import com.couchbase.client.java.query.dsl.Expression;
import com.couchbase.client.java.query.dsl.path.FromPath;
import com.couchbase.client.java.query.dsl.path.LimitPath;
import com.couchbase.client.java.query.dsl.path.WherePath;
import org.springframework.data.couchbase.core.RxJavaCouchbaseOperations;
import org.springframework.data.couchbase.repository.query.support.N1qlUtils;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.parser.PartTree;
/**
* A reactive {@link RepositoryQuery} for Couchbase, based on query derivation
*
* @author Subhashni Balakrishnan
* @since 3.0
*/
public class ReactivePartTreeN1qlBasedQuery extends ReactiveAbstractN1qlBasedQuery {
private final PartTree partTree;
public ReactivePartTreeN1qlBasedQuery(CouchbaseQueryMethod queryMethod, RxJavaCouchbaseOperations operations) {
super(queryMethod, operations);
this.partTree = new PartTree(queryMethod.getName(), queryMethod.getEntityInformation().getJavaType());
}
@Override
protected JsonValue getPlaceholderValues(ParameterAccessor accessor) {
return JsonArray.empty();
}
@Override
protected Statement getStatement(ParameterAccessor accessor, Object[] runtimeParameters, ReturnedType returnedType) {
String bucketName = getCouchbaseOperations().getCouchbaseBucket().name();
Expression bucket = N1qlUtils.escapedBucket(bucketName);
FromPath select;
if (partTree.isCountProjection()) {
select = select(count("*"));
} else {
select = N1qlUtils.createSelectClauseForEntity(bucketName, returnedType, this.getCouchbaseOperations().getConverter());
}
WherePath selectFrom = select.from(bucket);
N1qlQueryCreator queryCreator = new N1qlQueryCreator(partTree, accessor, selectFrom,
getCouchbaseOperations().getConverter(), getQueryMethod());
LimitPath selectFromWhereOrderBy = queryCreator.createQuery();
if (partTree.isLimiting()) {
return selectFromWhereOrderBy.limit(partTree.getMaxResults());
} else {
return selectFromWhereOrderBy;
}
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository.query;
import com.couchbase.client.java.view.SpatialViewQuery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.couchbase.core.RxJavaCouchbaseOperations;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import reactor.core.publisher.Flux;
/**
* A reactive {@link RepositoryQuery} for Couchbase, for spatial queries
*
* @author Subhashni Balakrishnan
* @since 3.0
*/
public class ReactiveSpatialViewBasedQuery implements RepositoryQuery {
private static final Logger LOG = LoggerFactory.getLogger(ReactiveSpatialViewBasedQuery.class);
private final CouchbaseQueryMethod method;
private final RxJavaCouchbaseOperations operations;
public ReactiveSpatialViewBasedQuery(CouchbaseQueryMethod method, RxJavaCouchbaseOperations operations) {
this.method = method;
this.operations = operations;
}
@Override
public Object execute(Object[] runtimeParams) {
String designDoc = method.getDimensionalAnnotation().designDocument();
String viewName = method.getDimensionalAnnotation().spatialViewName();
int dimensions = method.getDimensionalAnnotation().dimensions();
PartTree tree = new PartTree(method.getName(), method.getEntityInformation().getJavaType());
//prepare a spatial view query to be used as a base for the query creator
SpatialViewQuery baseSpatialQuery = SpatialViewQuery.from(designDoc, viewName)
.stale(operations.getDefaultConsistency().viewConsistency());
//use the SpatialViewQueryCreator to complete it
SpatialViewQueryCreator creator = new SpatialViewQueryCreator(dimensions,
tree, new ReactiveCouchbaseParameterAccessor(method, runtimeParams),
baseSpatialQuery, operations.getConverter());
SpatialViewQueryCreator.SpatialViewQueryWrapper finalQuery = creator.createQuery();
//execute the spatial query
return execute(finalQuery);
}
protected Object execute(SpatialViewQueryCreator.SpatialViewQueryWrapper query) {
if (LOG.isDebugEnabled()) {
LOG.debug("Executing spatial view query: " + query.getQuery().toString());
}
//TODO: eliminate false positives in geo query
return ReactiveWrapperConverters.toWrapper(operations.findBySpatialView(query.getQuery(),
method.getEntityInformation().getJavaType()), Flux.class);
}
@Override
public CouchbaseQueryMethod getQueryMethod() {
return method;
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository.query;
import com.couchbase.client.java.document.json.JsonValue;
import com.couchbase.client.java.query.N1qlQuery;
import com.couchbase.client.java.query.Statement;
import org.springframework.data.couchbase.core.RxJavaCouchbaseOperations;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.data.repository.query.EvaluationContextProvider;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ReturnedType;
/**
* A reactive StringN1qlBasedQuery {@link RepositoryQuery} for Couchbase, based on N1QL and a String statement.
* <p/>
* The statement can contain positional placeholders (eg. <code>name = $1</code>) that will map to the
* method's parameters, in the same order.
* <p/>
* The statement can also contain SpEL expressions enclosed in <code>#{</code> and <code>}</code>.
* <p/>
*
* @author Subhashni Balakrishnan
* @since 3.0
*/
public class ReactiveStringN1qlBasedQuery extends ReactiveAbstractN1qlBasedQuery {
private final StringBasedN1qlQueryParser queryParser;
private final SpelExpressionParser parser;
private final EvaluationContextProvider evaluationContextProvider;
protected String getTypeField() {
return getCouchbaseOperations().getConverter().getTypeKey();
}
protected Class<?> getTypeValue() {
return getQueryMethod().getEntityInformation().getJavaType();
}
public ReactiveStringN1qlBasedQuery(String statement,
CouchbaseQueryMethod queryMethod,
RxJavaCouchbaseOperations couchbaseOperations,
SpelExpressionParser spelParser,
final EvaluationContextProvider evaluationContextProvider) {
super(queryMethod, couchbaseOperations);
this.queryParser = new StringBasedN1qlQueryParser(statement, queryMethod,
getCouchbaseOperations().getCouchbaseBucket().name(), getTypeField(), getTypeValue());
this.parser = spelParser;
this.evaluationContextProvider = evaluationContextProvider;
}
@Override
protected JsonValue getPlaceholderValues(ParameterAccessor accessor) {
return this.queryParser.getPlaceholderValues(accessor);
}
@Override
public Statement getStatement(ParameterAccessor accessor, Object[] runtimeParameters, ReturnedType returnedType) {
EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(getQueryMethod().getParameters(), runtimeParameters);
String parsedStatement = queryParser.doParse(parser, evaluationContext, false);
return N1qlQuery.simple(parsedStatement).statement();
}
}

View File

@@ -0,0 +1,158 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository.query;
import com.couchbase.client.java.view.AsyncViewRow;
import com.couchbase.client.java.view.ViewQuery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.couchbase.core.CouchbaseQueryExecutionException;
import org.springframework.data.couchbase.core.RxJavaCouchbaseOperations;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import rx.Observable;
/**
* Execute a reactive repository query through the View mechanism.
*
* @author Subhashni Balakrishnan
* @since 3.0
*/
public class ReactiveViewBasedCouchbaseQuery implements RepositoryQuery {
private static final Logger LOG = LoggerFactory.getLogger(ReactiveViewBasedCouchbaseQuery.class);
private final CouchbaseQueryMethod method;
private final RxJavaCouchbaseOperations operations;
public ReactiveViewBasedCouchbaseQuery(CouchbaseQueryMethod method, RxJavaCouchbaseOperations operations) {
this.method = method;
this.operations = operations;
}
@Override
public Object execute(Object[] runtimeParams) {
if (method.hasViewName()) { //only allow derivation on @View explicitly defining a viewName
return deriveAndExecute(runtimeParams);
} else {
return guessViewAndExecute();
}
}
protected Object guessViewAndExecute() {
String designDoc = designDocName(method);
String methodName = method.getName();
boolean isExplicitReduce = method.hasViewAnnotation() && method.getViewAnnotation().reduce();
boolean isReduce = methodName.startsWith("count") || isExplicitReduce;
String viewName = StringUtils.uncapitalize(methodName.replaceFirst("find|count", ""));
ViewQuery simpleQuery = ViewQuery.from(designDoc, viewName)
.stale(operations.getDefaultConsistency().viewConsistency());
if (isReduce) {
simpleQuery.reduce();
return executeReduce(simpleQuery, designDoc, viewName);
} else {
return execute(simpleQuery);
}
}
protected Object deriveAndExecute(Object[] runtimeParams) {
String designDoc = designDocName(method);
String viewName = method.getViewAnnotation().viewName();
//prepare a ViewQuery to be used as a base for the ViewQueryCreator
ViewQuery baseQuery = ViewQuery.from(designDoc, viewName)
.stale(operations.getDefaultConsistency().viewConsistency());
try {
PartTree tree = new PartTree(method.getName(), method.getEntityInformation().getJavaType());
//use a ViewQueryCreator to complete the base query
ViewQueryCreator creator = new ViewQueryCreator(tree, new ReactiveCouchbaseParameterAccessor(method, runtimeParams),
method.getViewAnnotation(), baseQuery, operations.getConverter());
ViewQueryCreator.DerivedViewQuery result = creator.createQuery();
if (result.isReduce) {
return executeReduce(result.builtQuery, designDoc, viewName);
} else {
//otherwise just execute the query
return execute(result.builtQuery);
}
} catch (PropertyReferenceException e) {
/*
For views, not including an attribute name in the method will result in returning
the whole set of results from the view.
This is detected by looking for PropertyReferenceExceptions that seem to complain
about a missing property that corresponds to the method name
*/
if (e.getPropertyName().equals(method.getName())) {
return execute(baseQuery);
}
throw e;
}
}
protected Object execute(ViewQuery query) {
if (LOG.isDebugEnabled()) {
LOG.debug("Executing view query: " + query.toString());
}
return ReactiveWrapperConverters.toWrapper(operations.findByView(query, method.getEntityInformation().getJavaType()),
Flux.class);
}
protected Object executeReduce(ViewQuery query, String designDoc, String viewName) {
if (LOG.isDebugEnabled()) {
LOG.debug("Executing view reduced query: " + query.toString());
}
return ReactiveWrapperConverters.toWrapper(operations.queryView(query)
.flatMap(asyncViewResult -> asyncViewResult.error()
.flatMap(error -> Observable.error(new CouchbaseQueryExecutionException("Unable to execute reducing view "
+ viewName +" in design document " + designDoc +
"due to error:" + error.toString())))
.switchIfEmpty(asyncViewResult.rows()))
.map(row -> {
AsyncViewRow asyncViewRow = (AsyncViewRow) row;
return asyncViewRow.value();
}).take(1), Flux.class);
}
@Override
public QueryMethod getQueryMethod() {
return method;
}
/**
* Returns the best-guess design document name.
*
* @return the design document name.
*/
private static String designDocName(CouchbaseQueryMethod method) {
if (method.hasViewSpecification()) {
return method.getViewAnnotation().designDocument();
} else if (method.hasViewAnnotation()) {
return StringUtils.uncapitalize(method.getEntityInformation().getJavaType().getSimpleName());
} else {
throw new IllegalStateException("View-based query should only happen on a method with @View annotation");
}
}
}

View File

@@ -16,26 +16,13 @@
package org.springframework.data.couchbase.repository.query;
import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.List;
import com.couchbase.client.java.view.SpatialViewQuery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.couchbase.core.query.Dimensional;
import org.springframework.data.couchbase.repository.query.support.GeoUtils;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;
/**

View File

@@ -0,0 +1,266 @@
package org.springframework.data.couchbase.repository.query;
import static org.springframework.data.couchbase.core.support.TemplateUtils.*;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.document.json.JsonValue;
import org.slf4j.LoggerFactory;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
/**
* @author Subhashni Balakrishnan
*/
public class StringBasedN1qlQueryParser {
private static final Logger LOGGER = LoggerFactory.getLogger(StringBasedN1qlQueryParser.class);
public static final String SPEL_PREFIX = "n1ql";
/**
* Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.query.Query @Query}
* annotation's inline statement. This will be replaced by the correct <code>SELECT x FROM y</code> clause needed
* for entity mapping. Eg. <code>"#{{@value SPEL_SELECT_FROM_CLAUSE}} WHERE test = true"</code>.
* Note this only makes sense once, as the beginning of the statement.
*/
public static final String SPEL_SELECT_FROM_CLAUSE = "#" + SPEL_PREFIX + ".selectEntity";
/**
* Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.query.Query @Query}
* annotation's inline statement. This will be replaced by the (escaped) bucket name corresponding to the repository's
* entity. Eg. <code>"SELECT * FROM #{{@value SPEL_BUCKET}} LIMIT 3"</code>.
*/
public static final String SPEL_BUCKET = "#" + SPEL_PREFIX + ".bucket";
/**
* Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.query.Query @Query}
* annotation's inline statement. This will be replaced by the fields allowing to construct the repository's entity
* (SELECT clause). Eg. <code>"SELECT #{{@value SPEL_ENTITY}} FROM test"</code>.
*/
public static final String SPEL_ENTITY = "#" + SPEL_PREFIX + ".fields";
/**
* Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.query.Query @Query}
* annotation's inline statement WHERE clause. This will be replaced by the expression allowing to only select
* documents matching the entity's class. Eg. <code>"SELECT * FROM test WHERE test = true AND #{{@value SPEL_FILTER}}"</code>.
*/
public static final String SPEL_FILTER = "#" + SPEL_PREFIX + ".filter";
/** regexp that detect $named placeholder (starts with a letter, then alphanum chars) */
public static final Pattern NAMED_PLACEHOLDER_PATTERN = Pattern.compile("\\W(\\$\\p{Alpha}\\p{Alnum}*)\\b");
/** regexp that detect positional placeholder ($ followed by digits only) */
public static final Pattern POSITIONAL_PLACEHOLDER_PATTERN = Pattern.compile("\\W(\\$\\p{Digit}+)\\b");
/** regexp that detects " and ' quote boundaries, ignoring escaped quotes */
public static final Pattern QUOTE_DETECTION_PATTERN = Pattern.compile("[\"'](?:[^\"'\\\\]*(?:\\\\.)?)*[\"']");
/** enumeration of all the combinations of placeholder types that could be found in a N1QL statement */
private enum PlaceholderType {
NAMED, POSITIONAL, NONE
}
private final String statement;
private final QueryMethod queryMethod;
private final PlaceholderType placeHolderType;
private final N1qlSpelValues statementContext;
private final N1qlSpelValues countContext;
public StringBasedN1qlQueryParser(String statement,
QueryMethod queryMethod,
String bucketName,
String typeField,
Class<?> typeValue) {
this.statement = statement;
this.queryMethod = queryMethod;
this.placeHolderType = checkPlaceholders(statement);
this.statementContext = createN1qlSpelValues(bucketName, typeField, typeValue, false);
this.countContext = createN1qlSpelValues(bucketName, typeField, typeValue, true);
}
public static N1qlSpelValues createN1qlSpelValues(String bucketName, String typeField, Class<?> typeValue, boolean isCount) {
String b = "`" + bucketName + "`";
String entity = "META(" + b + ").id AS " + SELECT_ID +
", META(" + b + ").cas AS " + SELECT_CAS;
String count = "COUNT(*) AS " + CountFragment.COUNT_ALIAS;
String selectEntity;
if (isCount) {
selectEntity = "SELECT " + count + " FROM " + b;
} else {
selectEntity = "SELECT " + entity + ", " + b + ".* FROM " + b;
}
String typeSelection = "`" + typeField + "` = \"" + typeValue.getName() + "\"";
return new N1qlSpelValues(selectEntity, entity, b, typeSelection);
}
//this static method can be used to test the parsing behavior for Couchbase specific spel variables
//in isolation from the rest of the spel parser initialization chain.
public String doParse(SpelExpressionParser parser, EvaluationContext evaluationContext, boolean isCountQuery) {
org.springframework.expression.Expression parsedExpression = parser.parseExpression(this.getStatement(), new TemplateParserContext());
if (isCountQuery) {
evaluationContext.setVariable(SPEL_PREFIX, this.getCountContext());
} else {
evaluationContext.setVariable(SPEL_PREFIX, this.getStatementContext());
}
return parsedExpression.getValue(evaluationContext, String.class);
}
private PlaceholderType checkPlaceholders(String statement) {
Matcher quoteMatcher = QUOTE_DETECTION_PATTERN.matcher(statement);
Matcher positionMatcher = POSITIONAL_PLACEHOLDER_PATTERN.matcher(statement);
Matcher namedMatcher = NAMED_PLACEHOLDER_PATTERN.matcher(statement);
List<int[]> quotes = new ArrayList<int[]>();
while(quoteMatcher.find()) {
quotes.add(new int[] { quoteMatcher.start(), quoteMatcher.end() });
}
int posCount = 0;
int namedCount = 0;
while(positionMatcher.find()) {
String placeholder = positionMatcher.group(1);
//check not in quoted
if (checkNotQuoted(placeholder, positionMatcher.start(), positionMatcher.end(), quotes)) {
LOGGER.trace("{}: Found positional placeholder {}", this.queryMethod.getName(), placeholder);
posCount++;
}
}
while(namedMatcher.find()) {
String placeholder = namedMatcher.group(1);
//check not in quoted
if (checkNotQuoted(placeholder, namedMatcher.start(), namedMatcher.end(), quotes)) {
LOGGER.trace("{}: Found named placeholder {}", this.queryMethod.getName(), placeholder);
namedCount++;
}
}
if (posCount > 0 && namedCount > 0) {
throw new IllegalArgumentException("Using both named (" + namedCount + ") and positional (" + posCount +
") placeholders is not supported, please choose one over the other in " + this.queryMethod.getName());
} else if (posCount > 0) {
return PlaceholderType.POSITIONAL;
} else if (namedCount > 0) {
return PlaceholderType.NAMED;
} else {
return PlaceholderType.NONE;
}
}
private boolean checkNotQuoted(String item, int start, int end, List<int[]> quotes) {
for (int[] quote : quotes) {
if (quote[0] <= start && quote[1] >= end) {
LOGGER.trace("{}: potential placeholder {} is inside quotes, ignored", this.queryMethod.getName(), item);
return false;
}
}
return true;
}
private JsonValue getPositionalPlaceholderValues(ParameterAccessor accessor) {
JsonArray posValues = JsonArray.create();
for (Parameter parameter : this.queryMethod.getParameters().getBindableParameters()) {
posValues.add(accessor.getBindableValue(parameter.getIndex()));
}
return posValues;
}
private JsonObject getNamedPlaceholderValues(ParameterAccessor accessor) {
JsonObject namedValues = JsonObject.create();
for (Parameter parameter : this.queryMethod.getParameters().getBindableParameters()) {
String placeholder = parameter.getPlaceholder();
Object value = accessor.getBindableValue(parameter.getIndex());
if (placeholder != null && placeholder.charAt(0) == ':') {
placeholder = placeholder.replaceFirst(":", "");
namedValues.put(placeholder, value);
} else {
namedValues.put(parameter.getName(), value);
}
}
return namedValues;
}
protected JsonValue getPlaceholderValues(ParameterAccessor accessor) {
switch (this.placeHolderType) {
case NAMED:
return getNamedPlaceholderValues(accessor);
case POSITIONAL:
return getPositionalPlaceholderValues(accessor);
case NONE:
default:
return JsonArray.empty();
}
}
protected boolean useGeneratedCountQuery() {
return this.statement.contains(SPEL_SELECT_FROM_CLAUSE);
}
public N1qlSpelValues getCountContext() {
return this.countContext;
}
public N1qlSpelValues getStatementContext() {
return this.statementContext;
}
public String getStatement() {
return this.statement;
}
/**
* This class is exposed to SpEL parsing through the variable <code>#{@value SPEL_PREFIX}</code>.
* Use the attributes in your SpEL expressions: {@link #selectEntity}, {@link #fields}, {@link #bucket} and {@link #filter}.
*/
public static final class N1qlSpelValues {
/**
* <code>#{{@value SPEL_SELECT_FROM_CLAUSE}.
* selectEntity</code> will be replaced by the SELECT-FROM clause corresponding to the entity. Use once at the beginning.
*/
public final String selectEntity;
/**
* <code>#{{@value SPEL_ENTITY}.
* fields</code> will be replaced by the list of N1QL fields allowing to reconstruct the entity.
*/
public final String fields;
/**
* <code>#{{@value SPEL_BUCKET}.
* bucket</code> will be replaced by (escaped) bucket name in which the entity is stored.
*/
public final String bucket;
/**
* <code>#{{@value SPEL_FILTER}.
* filter</code> will be replaced by an expression allowing to select only entries matching the entity in a WHERE clause.
*/
public final String filter;
public N1qlSpelValues(String selectClause, String entityFields, String bucket, String filter) {
this.selectEntity = selectClause;
this.fields = entityFields;
this.bucket = bucket;
this.filter = filter;
}
}
}

View File

@@ -16,28 +16,17 @@
package org.springframework.data.couchbase.repository.query;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.document.json.JsonValue;
import com.couchbase.client.java.query.N1qlQuery;
import com.couchbase.client.java.query.Statement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.repository.query.EvaluationContextProvider;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
/**
@@ -48,68 +37,18 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
* <p/>
* The statement can also contain SpEL expressions enclosed in <code>#{</code> and <code>}</code>.
* <p/>
* There are couchbase-provided variables included for the {@link #SPEL_BUCKET bucket namespace},
* the {@link #SPEL_ENTITY ID and CAS fields} necessary for entity reconstruction
* or a shortcut that covers {@link #SPEL_SELECT_FROM_CLAUSE SELECT AND FROM clauses},
* along with a variable for {@link #SPEL_FILTER WHERE clause filtering} of the correct entity.
* There are couchbase-provided variables included for the {@link StringBasedN1qlQueryParser#SPEL_BUCKET bucket namespace},
* the {@link StringBasedN1qlQueryParser#SPEL_ENTITY ID and CAS fields} necessary for entity reconstruction
* or a shortcut that covers {@link StringBasedN1qlQueryParser#SPEL_SELECT_FROM_CLAUSE SELECT AND FROM clauses},
* along with a variable for {@link StringBasedN1qlQueryParser#SPEL_FILTER WHERE clause filtering} of the correct entity.
*
* @author Simon Baslé
* @author Subhashni Balakrishnan
*/
public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery {
private static final Logger LOGGER = LoggerFactory.getLogger(StringN1qlBasedQuery.class);
public static final String SPEL_PREFIX = "n1ql";
/**
* Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.query.Query @Query}
* annotation's inline statement. This will be replaced by the correct <code>SELECT x FROM y</code> clause needed
* for entity mapping. Eg. <code>"#{{@value StringN1qlBasedQuery#SPEL_SELECT_FROM_CLAUSE}} WHERE test = true"</code>.
* Note this only makes sense once, as the beginning of the statement.
*/
public static final String SPEL_SELECT_FROM_CLAUSE = "#" + SPEL_PREFIX + ".selectEntity";
/**
* Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.query.Query @Query}
* annotation's inline statement. This will be replaced by the (escaped) bucket name corresponding to the repository's
* entity. Eg. <code>"SELECT * FROM #{{@value StringN1qlBasedQuery#SPEL_BUCKET}} LIMIT 3"</code>.
*/
public static final String SPEL_BUCKET = "#" + SPEL_PREFIX + ".bucket";
/**
* Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.query.Query @Query}
* annotation's inline statement. This will be replaced by the fields allowing to construct the repository's entity
* (SELECT clause). Eg. <code>"SELECT #{{@value StringN1qlBasedQuery#SPEL_ENTITY}} FROM test"</code>.
*/
public static final String SPEL_ENTITY = "#" + SPEL_PREFIX + ".fields";
/**
* Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.query.Query @Query}
* annotation's inline statement WHERE clause. This will be replaced by the expression allowing to only select
* documents matching the entity's class. Eg. <code>"SELECT * FROM test WHERE test = true AND #{{@value StringN1qlBasedQuery#SPEL_FILTER}}"</code>.
*/
public static final String SPEL_FILTER = "#" + SPEL_PREFIX + ".filter";
/** regexp that detect $named placeholder (starts with a letter, then alphanum chars) */
private static final Pattern NAMED_PLACEHOLDER_PATTERN = Pattern.compile("\\W(\\$\\p{Alpha}\\p{Alnum}*)\\b");
/** regexp that detect positional placeholder ($ followed by digits only) */
private static final Pattern POSITIONAL_PLACEHOLDER_PATTERN = Pattern.compile("\\W(\\$\\p{Digit}+)\\b");
/** regexp that detects " and ' quote boundaries, ignoring escaped quotes */
private static final Pattern QUOTE_DETECTION_PATTERN = Pattern.compile("[\"'](?:[^\"'\\\\]*(?:\\\\.)?)*[\"']");
/** enumeration of all the combinations of placeholder types that could be found in a N1QL statement */
private enum PlaceholderType {
NAMED, POSITIONAL, NONE
}
private final String originalStatement;
private final PlaceholderType placeHolderType;
private final SpelExpressionParser parser;
private final EvaluationContextProvider evaluationContextProvider;
private final N1qlSpelValues countContext;
private final N1qlSpelValues statementContext;
private final StringBasedN1qlQueryParser queryParser;
protected String getTypeField() {
return getCouchbaseOperations().getConverter().getTypeKey();
@@ -122,201 +61,34 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery {
public StringN1qlBasedQuery(String statement, CouchbaseQueryMethod queryMethod, CouchbaseOperations couchbaseOperations,
SpelExpressionParser spelParser, final EvaluationContextProvider evaluationContextProvider) {
super(queryMethod, couchbaseOperations);
this.originalStatement = statement;
this.placeHolderType = checkPlaceholders(statement);
this.queryParser = new StringBasedN1qlQueryParser(statement, queryMethod,
getCouchbaseOperations().getCouchbaseBucket().name(), getTypeField(), getTypeValue());
this.parser = spelParser;
this.evaluationContextProvider = evaluationContextProvider;
this.statementContext = createN1qlSpelValues(getCouchbaseOperations().getCouchbaseBucket().name(), getTypeField(), getTypeValue(), false);
this.countContext = createN1qlSpelValues(getCouchbaseOperations().getCouchbaseBucket().name(), getTypeField(), getTypeValue(), true);
}
private PlaceholderType checkPlaceholders(String statement) {
Matcher quoteMatcher = QUOTE_DETECTION_PATTERN.matcher(statement);
Matcher positionMatcher = POSITIONAL_PLACEHOLDER_PATTERN.matcher(statement);
Matcher namedMatcher = NAMED_PLACEHOLDER_PATTERN.matcher(statement);
List<int[]> quotes = new ArrayList<int[]>();
while(quoteMatcher.find()) {
quotes.add(new int[] { quoteMatcher.start(), quoteMatcher.end() });
}
int posCount = 0;
int namedCount = 0;
while(positionMatcher.find()) {
String placeholder = positionMatcher.group(1);
//check not in quoted
if (checkNotQuoted(placeholder, positionMatcher.start(), positionMatcher.end(), quotes)) {
LOGGER.trace("{}: Found positional placeholder {}", getQueryMethod().getName(), placeholder);
posCount++;
}
}
while(namedMatcher.find()) {
String placeholder = namedMatcher.group(1);
//check not in quoted
if (checkNotQuoted(placeholder, namedMatcher.start(), namedMatcher.end(), quotes)) {
LOGGER.trace("{}: Found named placeholder {}", getQueryMethod().getName(), placeholder);
namedCount++;
}
}
if (posCount > 0 && namedCount > 0) {
throw new IllegalArgumentException("Using both named (" + namedCount + ") and positional (" + posCount +
") placeholders is not supported, please choose one over the other in " + queryMethod.getName());
} else if (posCount > 0) {
return PlaceholderType.POSITIONAL;
} else if (namedCount > 0) {
return PlaceholderType.NAMED;
} else {
return PlaceholderType.NONE;
}
}
private boolean checkNotQuoted(String item, int start, int end, List<int[]> quotes) {
for (int[] quote : quotes) {
if (quote[0] <= start && quote[1] >= end) {
LOGGER.trace("{}: potential placeholder {} is inside quotes, ignored", queryMethod.getName(), item);
return false;
}
}
return true;
}
public static N1qlSpelValues createN1qlSpelValues(String bucketName, String typeField, Class<?> typeValue, boolean isCount) {
String b = "`" + bucketName + "`";
String entity = "META(" + b + ").id AS " + CouchbaseOperations.SELECT_ID +
", META(" + b + ").cas AS " + CouchbaseOperations.SELECT_CAS;
String count = "COUNT(*) AS " + CountFragment.COUNT_ALIAS;
String selectEntity;
if (isCount) {
selectEntity = "SELECT " + count + " FROM " + b;
} else {
selectEntity = "SELECT " + entity + ", " + b + ".* FROM " + b;
}
String typeSelection = "`" + typeField + "` = \"" + typeValue.getName() + "\"";
return new N1qlSpelValues(selectEntity, entity, b, typeSelection);
}
/**
* Parse the statement to detect SPEL blocks (delimited by <code>#{</code> and <code>}</code>)
* and replace said expression blocks with their corresponding values.
*
* @param statement the full statement into which SpEL expressions should be parsed and replaced.
* @param runtimeParameters the parameters passed into the method at runtime.
* @return the statement with the SpEL interpreted and replaced by its values.
*/
protected String parseSpel(String statement, boolean isCount, Object[] runtimeParameters) {
EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(getQueryMethod().getParameters(), runtimeParameters);
N1qlSpelValues n1qlSpelValues = this.statementContext;
if (isCount) {
n1qlSpelValues = this.countContext;
}
return doParse(statement, parser, evaluationContext, n1qlSpelValues);
}
//this static method can be used to test the parsing behavior for Couchbase specific spel variables
//in isolation from the rest of the spel parser initialization chain.
protected static String doParse(String statement, SpelExpressionParser parser, EvaluationContext evaluationContext, N1qlSpelValues n1qlSpelValues) {
Expression parsedExpression = parser.parseExpression(statement, new TemplateParserContext());
evaluationContext.setVariable(SPEL_PREFIX, n1qlSpelValues);
return parsedExpression.getValue(evaluationContext, String.class);
}
@Override
protected JsonValue getPlaceholderValues(ParameterAccessor accessor) {
switch (this.placeHolderType) {
case NAMED:
return getNamedPlaceholderValues(accessor);
case POSITIONAL:
return getPositionalPlaceholderValues(accessor);
case NONE:
default:
return JsonArray.empty();
}
}
private JsonValue getPositionalPlaceholderValues(ParameterAccessor accessor) {
JsonArray posValues = JsonArray.create();
for (Parameter parameter : getQueryMethod().getParameters().getBindableParameters()) {
posValues.add(accessor.getBindableValue(parameter.getIndex()));
}
return posValues;
}
private JsonObject getNamedPlaceholderValues(ParameterAccessor accessor) {
JsonObject namedValues = JsonObject.create();
for (Parameter parameter : getQueryMethod().getParameters().getBindableParameters()) {
String placeholder = parameter.getPlaceholder();
Object value = accessor.getBindableValue(parameter.getIndex());
if (placeholder != null && placeholder.charAt(0) == ':') {
placeholder = placeholder.replaceFirst(":", "");
namedValues.put(placeholder, value);
} else {
namedValues.put(parameter.getName(), value);
}
}
return namedValues;
return this.queryParser.getPlaceholderValues(accessor);
}
@Override
public Statement getStatement(ParameterAccessor accessor, Object[] runtimeParameters, ReturnedType returnedType) {
String parsedStatement = parseSpel(this.originalStatement, false, runtimeParameters);
EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(getQueryMethod().getParameters(), runtimeParameters);
String parsedStatement = this.queryParser.doParse(parser, evaluationContext, false);
return N1qlQuery.simple(parsedStatement).statement();
}
@Override
protected Statement getCount(ParameterAccessor accessor, Object[] runtimeParameters) {
String parsedCountStatement = parseSpel(this.originalStatement, true, runtimeParameters);
return N1qlQuery.simple(parsedCountStatement).statement();
EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(getQueryMethod().getParameters(), runtimeParameters);
String parsedStatement = this.queryParser.doParse(parser, evaluationContext, true);
return N1qlQuery.simple(parsedStatement).statement();
}
@Override
protected boolean useGeneratedCountQuery() {
return this.originalStatement.contains(SPEL_SELECT_FROM_CLAUSE);
return this.queryParser.useGeneratedCountQuery();
}
/**
* This class is exposed to SpEL parsing through the variable <code>#{@value StringN1qlBasedQuery#SPEL_PREFIX}</code>.
* Use the attributes in your SpEL expressions: {@link #selectEntity}, {@link #fields}, {@link #bucket} and {@link #filter}.
*/
public static final class N1qlSpelValues {
/**
* <code>#{{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
* selectEntity</code> will be replaced by the SELECT-FROM clause corresponding to the entity. Use once at the beginning.
*/
public final String selectEntity;
/**
* <code>#{{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
* fields</code> will be replaced by the list of N1QL fields allowing to reconstruct the entity.
*/
public final String fields;
/**
* <code>#{{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
* bucket</code> will be replaced by (escaped) bucket name in which the entity is stored.
*/
public final String bucket;
/**
* <code>#{{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
* filter</code> will be replaced by an expression allowing to select only entries matching the entity in a WHERE clause.
*/
public final String filter;
public N1qlSpelValues(String selectClause, String entityFields, String bucket, String filter) {
this.selectEntity = selectClause;
this.fields = entityFields;
this.bucket = bucket;
this.filter = filter;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors
* Copyright 2012-2017 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.
@@ -24,11 +24,19 @@ import static com.couchbase.client.java.query.dsl.Expression.x;
import static com.couchbase.client.java.query.dsl.functions.AggregateFunctions.count;
import static com.couchbase.client.java.query.dsl.functions.MetaFunctions.meta;
import static com.couchbase.client.java.query.dsl.functions.StringFunctions.lower;
import static org.springframework.data.couchbase.core.support.TemplateUtils.*;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.document.json.JsonValue;
import com.couchbase.client.java.query.N1qlParams;
import com.couchbase.client.java.query.N1qlQuery;
import com.couchbase.client.java.query.Statement;
import com.couchbase.client.java.query.consistency.ScanConsistency;
import com.couchbase.client.java.query.dsl.Expression;
import com.couchbase.client.java.query.dsl.functions.TypeFunctions;
import com.couchbase.client.java.query.dsl.path.FromPath;
@@ -41,11 +49,15 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation;
import org.springframework.data.couchbase.repository.query.CountFragment;
import org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.context.PersistentPropertyPath;
import org.springframework.data.repository.core.EntityMetadata;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
/**
* Utility class to deal with constructing well formed N1QL queries around Spring Data entities, so that
@@ -87,8 +99,8 @@ public class N1qlUtils {
*/
public static FromPath createSelectClauseForEntity(String bucketName, ReturnedType returnedType, CouchbaseConverter converter) {
Expression bucket = escapedBucket(bucketName);
Expression metaId = path(meta(bucket), "id").as(CouchbaseOperations.SELECT_ID);
Expression metaCas = path(meta(bucket), "cas").as(CouchbaseOperations.SELECT_CAS);
Expression metaId = path(meta(bucket), "id").as(SELECT_ID);
Expression metaCas = path(meta(bucket), "cas").as(SELECT_CAS);
List<Expression> expList = new ArrayList<Expression>();
expList.add(metaId);
expList.add(metaCas);
@@ -208,4 +220,28 @@ public class N1qlUtils {
public static <T> Statement createCountQueryForEntity(String bucketName, CouchbaseConverter converter, CouchbaseEntityInformation<T, String> entityInformation) {
return select(count("*").as(CountFragment.COUNT_ALIAS)).from(escapedBucket(bucketName)).where(createWhereFilterForEntity(null, converter, entityInformation));
}
/**
* Creates N1QLQuery object from the statement, query placeholder values and scan consistency
*
* @param statement
* @param queryPlaceholderValues
* @param scanConsistency
* @return
*/
public static N1qlQuery buildQuery(Statement statement, JsonValue queryPlaceholderValues, ScanConsistency scanConsistency) {
N1qlParams n1qlParams = N1qlParams.build().consistency(scanConsistency);
N1qlQuery query;
if (queryPlaceholderValues instanceof JsonObject && !((JsonObject) queryPlaceholderValues).isEmpty()) {
query = N1qlQuery.parameterized(statement, (JsonObject) queryPlaceholderValues, n1qlParams);
} else if (queryPlaceholderValues instanceof JsonArray && !((JsonArray) queryPlaceholderValues).isEmpty()) {
query = N1qlQuery.parameterized(statement, (JsonArray) queryPlaceholderValues, n1qlParams);
} else {
query = N1qlQuery.simple(statement, n1qlParams);
}
return query;
}
}

View File

@@ -21,6 +21,7 @@ import static com.couchbase.client.java.query.dsl.Expression.x;
import java.util.Collections;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.bucket.BucketManager;
import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.query.AsyncN1qlQueryResult;
@@ -31,6 +32,7 @@ import com.couchbase.client.java.view.DefaultView;
import com.couchbase.client.java.view.DesignDocument;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.couchbase.core.RxJavaCouchbaseOperations;
import rx.Observable;
import rx.exceptions.CompositeException;
import rx.functions.Action1;
@@ -134,15 +136,15 @@ public class IndexManager {
Observable<Void> n1qlSecondaryAsync = Observable.empty();
if (viewIndexed != null && !ignoreViews) {
viewAsync = buildAllView(viewIndexed, metadata, couchbaseOperations);
viewAsync = buildAllView(viewIndexed, metadata, couchbaseOperations.getCouchbaseBucket(), couchbaseOperations.getConverter().getTypeKey());
}
if (n1qlPrimaryIndexed != null && !ignoreN1qlPrimary) {
n1qlPrimaryAsync = buildN1qlPrimary(metadata, couchbaseOperations);
n1qlPrimaryAsync = buildN1qlPrimary(metadata, couchbaseOperations.getCouchbaseBucket());
}
if (n1qlSecondaryIndexed != null && !ignoreN1qlSecondary) {
n1qlSecondaryAsync = buildN1qlSecondary(n1qlSecondaryIndexed, metadata, couchbaseOperations);
n1qlSecondaryAsync = buildN1qlSecondary(n1qlSecondaryIndexed, metadata, couchbaseOperations.getCouchbaseBucket(), couchbaseOperations.getConverter().getTypeKey());
}
//trigger the builds, wait for the last one, throw CompositeException if errors
@@ -151,14 +153,54 @@ public class IndexManager {
.lastOrDefault(null);
}
private Observable<Void> buildN1qlPrimary(final RepositoryInformation metadata, CouchbaseOperations couchbaseOperations) {
final String bucketName = couchbaseOperations.getCouchbaseBucket().name();
/**
* Build the relevant indexes according to the provided annotation and repository metadata, in parallel but blocking
* until all relevant indexes are created. Existing indexes will be detected and skipped.
* <p/>
* Note that this IndexManager could be configured to ignore some of the annotation types.
* In case of multiple errors, a {@link CompositeException} can be raised with up to 3 causes (one per type of index).
*
* @param metadata the repository's metadata (allowing to find out the type of entity stored, the key under which type
* information is stored, etc...).
* @param viewIndexed the annotation for creation of a View-based index.
* @param n1qlPrimaryIndexed the annotation for creation of a N1QL-based primary index (generic).
* @param n1qlSecondaryIndexed the annotation for creation of a N1QL-based secondary index (specific to the repository
* stored entity).
* @param rxjava1CouchbaseOperations the template to use for index creation.
* @throws CompositeException when several errors (for multiple index types) have been raised.
*/
public void buildIndexes(RepositoryInformation metadata, ViewIndexed viewIndexed, N1qlPrimaryIndexed n1qlPrimaryIndexed,
N1qlSecondaryIndexed n1qlSecondaryIndexed, RxJavaCouchbaseOperations rxjava1CouchbaseOperations) {
Observable<Void> viewAsync = Observable.empty();
Observable<Void> n1qlPrimaryAsync = Observable.empty();
Observable<Void> n1qlSecondaryAsync = Observable.empty();
if (viewIndexed != null && !ignoreViews) {
viewAsync = buildAllView(viewIndexed, metadata, rxjava1CouchbaseOperations.getCouchbaseBucket(), rxjava1CouchbaseOperations.getConverter().getTypeKey());
}
if (n1qlPrimaryIndexed != null && !ignoreN1qlPrimary) {
n1qlPrimaryAsync = buildN1qlPrimary(metadata, rxjava1CouchbaseOperations.getCouchbaseBucket());
}
if (n1qlSecondaryIndexed != null && !ignoreN1qlSecondary) {
n1qlSecondaryAsync = buildN1qlSecondary(n1qlSecondaryIndexed, metadata, rxjava1CouchbaseOperations.getCouchbaseBucket(), rxjava1CouchbaseOperations.getConverter().getTypeKey());
}
//trigger the builds, wait for the last one, throw CompositeException if errors
Observable.mergeDelayError(viewAsync, n1qlPrimaryAsync, n1qlSecondaryAsync)
.toBlocking()
.lastOrDefault(null);
}
private Observable<Void> buildN1qlPrimary(final RepositoryInformation metadata, Bucket bucket) {
final String bucketName = bucket.name();
Statement createPrimary = Index.createPrimaryIndex()
.on(bucketName)
.using(IndexType.GSI);
LOGGER.debug("Creating N1QL primary index for repository {}", metadata.getRepositoryInterface().getSimpleName());
return couchbaseOperations.getCouchbaseBucket().async().query(createPrimary)
return bucket.async().query(createPrimary)
.flatMap(new Func1<AsyncN1qlQueryResult, Observable<JsonObject>>() {
@Override
public Observable<JsonObject> call(AsyncN1qlQueryResult asyncN1qlQueryResult) {
@@ -184,10 +226,9 @@ public class IndexManager {
});
}
private Observable<Void> buildN1qlSecondary(N1qlSecondaryIndexed config, final RepositoryInformation metadata, CouchbaseOperations couchbaseOperations) {
final String bucketName = couchbaseOperations.getCouchbaseBucket().name();
private Observable<Void> buildN1qlSecondary(N1qlSecondaryIndexed config, final RepositoryInformation metadata, Bucket bucket, String typeKey) {
final String bucketName = bucket.name();
final String indexName = config.indexName();
String typeKey = couchbaseOperations.getConverter().getTypeKey();
final String type = metadata.getDomainType().getName();
Statement createIndex = Index.createIndex(indexName)
@@ -196,7 +237,7 @@ public class IndexManager {
.using(IndexType.GSI);
LOGGER.debug("Creating N1QL secondary index for repository {}", metadata.getRepositoryInterface().getSimpleName());
return couchbaseOperations.getCouchbaseBucket().async().query(createIndex)
return bucket.async().query(createIndex)
.flatMap(new Func1<AsyncN1qlQueryResult, Observable<JsonObject>>() {
@Override
public Observable<JsonObject> call(AsyncN1qlQueryResult asyncN1qlQueryResult) {
@@ -222,15 +263,14 @@ public class IndexManager {
});
}
private Observable<Void> buildAllView(ViewIndexed config, final RepositoryInformation metadata, CouchbaseOperations couchbaseOperations) {
private Observable<Void> buildAllView(ViewIndexed config, final RepositoryInformation metadata, Bucket bucket, String typeKey) {
if (config == null) return Observable.empty();
LOGGER.debug("Creating View index index for repository {}", metadata.getRepositoryInterface().getSimpleName());
BucketManager manager = couchbaseOperations.getCouchbaseBucket().bucketManager();
BucketManager manager = bucket.bucketManager();
String viewName = config.viewName();
String mapFunction = config.mapFunction();
if (mapFunction.isEmpty()) {
String typeKey = couchbaseOperations.getConverter().getTypeKey();
String type = metadata.getDomainType().getName();
mapFunction = String.format(TEMPLATE_MAP_FUNCTION, typeKey, type);

View File

@@ -0,0 +1,237 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository.support;
import java.io.Serializable;
import java.lang.reflect.Method;
import com.couchbase.client.java.util.features.CouchbaseFeature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.couchbase.core.RxJavaCouchbaseOperations;
import org.springframework.data.couchbase.core.UnsupportedCouchbaseFeatureException;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.couchbase.core.query.*;
import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping;
import org.springframework.data.couchbase.repository.query.*;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport;
import org.springframework.data.repository.query.EvaluationContextProvider;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
/**
* @Subhashni Balakrishnan
* @since 3.0
*/
public class ReactiveCouchbaseRepositoryFactory extends ReactiveRepositoryFactorySupport {
private static final SpelExpressionParser SPEL_PARSER = new SpelExpressionParser();
/**
* Holds the reference to the template.
*/
private final ReactiveRepositoryOperationsMapping couchbaseOperationsMapping;
/**
* Holds the reference to the {@link IndexManager}.
*/
private final IndexManager indexManager;
/**
* Holds the mapping context.
*/
private final MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> mappingContext;
/**
* Holds a custom ViewPostProcessor..
*/
private final ViewPostProcessor viewPostProcessor;
/**
* Create a new factory.
*
* @param couchbaseOperationsMapping the template for the underlying actions.
*/
public ReactiveCouchbaseRepositoryFactory(final ReactiveRepositoryOperationsMapping couchbaseOperationsMapping,
final IndexManager indexManager) {
Assert.notNull(couchbaseOperationsMapping);
Assert.notNull(indexManager);
this.couchbaseOperationsMapping = couchbaseOperationsMapping;
this.indexManager = indexManager;
mappingContext = this.couchbaseOperationsMapping.getMappingContext();
viewPostProcessor = ViewPostProcessor.INSTANCE;
addRepositoryProxyPostProcessor(viewPostProcessor);
}
/**
* Returns entity information based on the domain class.
*
* @param domainClass the class for the entity.
* @param <T> the value type
* @param <ID> the id type.
*
* @return entity information for that domain class.
*/
@Override
public <T, ID extends Serializable> CouchbaseEntityInformation<T, ID> getEntityInformation(final Class<T> domainClass) {
CouchbasePersistentEntity<?> entity = mappingContext.getPersistentEntity(domainClass);
if (entity == null) {
throw new MappingException(String.format("Could not lookup mapping metadata for domain class %s!", domainClass.getName()));
}
return new MappingCouchbaseEntityInformation<T, ID>((CouchbasePersistentEntity<T>) entity);
}
/**
* Returns a new Repository based on the metadata. Two categories of repositories can be instantiated:
* {@link SimpleReactiveCouchbaseRepository} and {@link ReactiveN1qlCouchbaseRepository}.
*
* This method performs feature checks to decide which of the two categories can be instantiated (eg. is N1QL available?).
* Instantiation is done via reflection, see {@link #getRepositoryBaseClass(RepositoryMetadata)}.
*
* @param metadata the repository metadata.
*
* @return a new created repository.
*/
@Override
protected final Object getTargetRepository(final RepositoryInformation metadata) {
RxJavaCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve(metadata.getRepositoryInterface(),
metadata.getDomainType());
boolean isN1qlAvailable = couchbaseOperations.getCouchbaseClusterInfo().checkAvailable(CouchbaseFeature.N1QL);
ViewIndexed viewIndexed = AnnotationUtils.findAnnotation(metadata.getRepositoryInterface(), ViewIndexed.class);
N1qlPrimaryIndexed n1qlPrimaryIndexed = AnnotationUtils.findAnnotation(metadata.getRepositoryInterface(), N1qlPrimaryIndexed.class);
N1qlSecondaryIndexed n1qlSecondaryIndexed = AnnotationUtils.findAnnotation(metadata.getRepositoryInterface(), N1qlSecondaryIndexed.class);
checkFeatures(metadata, isN1qlAvailable, n1qlPrimaryIndexed, n1qlSecondaryIndexed);
indexManager.buildIndexes(metadata, viewIndexed, n1qlPrimaryIndexed, n1qlSecondaryIndexed, couchbaseOperations);
CouchbaseEntityInformation<?, Serializable> entityInformation = getEntityInformation(metadata.getDomainType());
SimpleReactiveCouchbaseRepository repo = getTargetRepositoryViaReflection(metadata, entityInformation, couchbaseOperations);
repo.setViewMetadataProvider(viewPostProcessor.getViewMetadataProvider());
return repo;
}
private void checkFeatures(RepositoryInformation metadata, boolean isN1qlAvailable,
N1qlPrimaryIndexed n1qlPrimaryIndexed, N1qlSecondaryIndexed n1qlSecondaryIndexed) {
//paging repo will always need N1QL, also check if the repository requires a N1QL index
boolean needsN1ql = metadata.isPagingRepository() || n1qlPrimaryIndexed != null || n1qlSecondaryIndexed != null;
//for other repos, they might also need N1QL if they don't have only @View methods
if (!needsN1ql) {
for (Method method : metadata.getQueryMethods()) {
boolean hasN1ql = AnnotationUtils.findAnnotation(method, Query.class) != null;
boolean hasView = AnnotationUtils.findAnnotation(method, View.class) != null;
if (hasN1ql || !hasView) {
needsN1ql = true;
break;
}
}
}
if (needsN1ql && !isN1qlAvailable) {
throw new UnsupportedCouchbaseFeatureException("Repository uses N1QL", CouchbaseFeature.N1QL);
}
}
/**
* Returns the base class for the repository being constructed. Two categories of repositories can be produced by
* this factory: {@link SimpleReactiveCouchbaseRepository} and {@link ReactiveN1qlCouchbaseRepository}. This method checks if N1QL
* is available to choose between the two, but the actual concrete class is determined respectively by
* {@link #getSimpleBaseClass(RepositoryMetadata)} and {@link #getN1qlBaseClass(RepositoryMetadata)}.
*
* Override these methods if you want to change the base class for all your repositories.
*
* @param repositoryMetadata metadata for the repository.
*
* @return the base class.
*/
@Override
protected final Class<?> getRepositoryBaseClass(final RepositoryMetadata repositoryMetadata) {
RxJavaCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve(repositoryMetadata.getRepositoryInterface(),
repositoryMetadata.getDomainType());
boolean isN1qlAvailable = couchbaseOperations.getCouchbaseClusterInfo().checkAvailable(CouchbaseFeature.N1QL);
if (isN1qlAvailable) {
return getN1qlBaseClass(repositoryMetadata);
}
return getSimpleBaseClass(repositoryMetadata);
}
protected Class<? extends ReactiveN1qlCouchbaseRepository> getN1qlBaseClass(final RepositoryMetadata repositoryMetadata) {
return ReactiveN1qlCouchbaseRepository.class;
}
protected Class<? extends SimpleReactiveCouchbaseRepository> getSimpleBaseClass(final RepositoryMetadata repositoryMetadata) {
return SimpleReactiveCouchbaseRepository.class;
}
@Override
protected QueryLookupStrategy getQueryLookupStrategy(QueryLookupStrategy.Key key, EvaluationContextProvider contextProvider) {
return new ReactiveCouchbaseRepositoryFactory.CouchbaseQueryLookupStrategy(contextProvider);
}
/**
* Strategy to lookup Couchbase queries implementation to be used.
*/
private class CouchbaseQueryLookupStrategy implements QueryLookupStrategy {
private final EvaluationContextProvider evaluationContextProvider;
public CouchbaseQueryLookupStrategy(EvaluationContextProvider evaluationContextProvider) {
this.evaluationContextProvider = evaluationContextProvider;
}
@Override
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) {
RxJavaCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve(metadata.getRepositoryInterface(),
metadata.getDomainType());
CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, metadata, factory, mappingContext);
String namedQueryName = queryMethod.getNamedQueryName();
if (queryMethod.hasDimensionalAnnotation()) {
return new ReactiveSpatialViewBasedQuery(queryMethod, couchbaseOperations);
} else if (queryMethod.hasViewAnnotation()) {
return new ReactiveViewBasedCouchbaseQuery(queryMethod, couchbaseOperations);
} else if (queryMethod.hasN1qlAnnotation()) {
if (queryMethod.hasInlineN1qlQuery()) {
return new ReactiveStringN1qlBasedQuery(queryMethod.getInlineN1qlQuery(), queryMethod, couchbaseOperations,
SPEL_PARSER, evaluationContextProvider);
} else if (namedQueries.hasQuery(namedQueryName)) {
String namedQuery = namedQueries.getQuery(namedQueryName);
return new ReactiveStringN1qlBasedQuery(namedQuery, queryMethod, couchbaseOperations,
SPEL_PARSER, evaluationContextProvider);
} //otherwise will do default, queryDerivation
}
return new ReactivePartTreeN1qlBasedQuery(queryMethod, couchbaseOperations);
}
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository.support;
import java.io.Serializable;
import org.springframework.data.couchbase.core.RxJavaCouchbaseOperations;
import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.util.Assert;
/**
* @author Subhashni Balakrishnan
* @since 3.0
*/
public class ReactiveCouchbaseRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable>
extends RepositoryFactoryBeanSupport<T, S, ID> {
/**
* Contains the reference to the template.
*/
private ReactiveRepositoryOperationsMapping couchbaseOperationsMapping;
/**
* Contains the reference to the IndexManager.
*/
private IndexManager indexManager;
/**
* Creates a new {@link CouchbaseRepositoryFactoryBean} for the given repository interface.
*
* @param repositoryInterface must not be {@literal null}.
*/
public ReactiveCouchbaseRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
super(repositoryInterface);
}
/**
* Set the template reference.
*
* @param couchbaseOperationsMapping the reference to the operations template.
*/
public void setCouchbaseOperations(final RxJavaCouchbaseOperations couchbaseOperationsMapping) {
setCouchbaseOperationsMapping(new ReactiveRepositoryOperationsMapping(couchbaseOperationsMapping));
}
public void setCouchbaseOperationsMapping(final ReactiveRepositoryOperationsMapping couchbaseOperationsMapping) {
this.couchbaseOperationsMapping = couchbaseOperationsMapping;
setMappingContext(couchbaseOperationsMapping.getMappingContext());
}
/**
* Set the IndexManager reference.
*
* @param indexManager the IndexManager to use.
*/
public void setIndexManager(final IndexManager indexManager) {
this.indexManager = indexManager;
}
/**
* Returns a factory instance.
*
* @return the factory instance.
*/
@Override
protected RepositoryFactorySupport createRepositoryFactory() {
return getFactoryInstance(couchbaseOperationsMapping, indexManager);
}
/**
* Get the factory instance for the operations.
*
* @param couchbaseOperationsMapping the reference to the template.
* @param indexManager the reference to the {@link IndexManager}.
* @return the factory instance.
*/
protected ReactiveCouchbaseRepositoryFactory getFactoryInstance(final ReactiveRepositoryOperationsMapping couchbaseOperationsMapping,
IndexManager indexManager) {
return new ReactiveCouchbaseRepositoryFactory(couchbaseOperationsMapping, indexManager);
}
/**
* Make sure that the dependencies are set and not null.
*/
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
Assert.notNull(couchbaseOperationsMapping, "operationsMapping must not be null!");
Assert.notNull(indexManager, "indexManager must not be null!");
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository.support;
import java.io.Serializable;
import com.couchbase.client.java.query.N1qlParams;
import com.couchbase.client.java.query.N1qlQuery;
import com.couchbase.client.java.query.Statement;
import com.couchbase.client.java.query.consistency.ScanConsistency;
import com.couchbase.client.java.query.dsl.Expression;
import com.couchbase.client.java.query.dsl.path.WherePath;
import org.springframework.data.couchbase.core.RxJavaCouchbaseOperations;
import org.springframework.data.couchbase.repository.ReactiveCouchbaseSortingRepository;
import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation;
import org.springframework.data.couchbase.repository.query.support.N1qlUtils;
import org.springframework.data.domain.Sort;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
/**
* @author Subhashni Balakrishnan
* @since 3.0
*/
public class ReactiveN1qlCouchbaseRepository<T, ID extends Serializable>
extends SimpleReactiveCouchbaseRepository<T, ID>
implements ReactiveCouchbaseSortingRepository<T, ID> {
public ReactiveN1qlCouchbaseRepository(CouchbaseEntityInformation<T, String> metadata, RxJavaCouchbaseOperations operations) {
super(metadata, operations);
}
@SuppressWarnings("unchecked")
@Override
public Flux<T> findAll(Sort sort) {
Assert.notNull(sort);
//prepare elements of the query
WherePath selectFrom = N1qlUtils.createSelectFromForEntity(getCouchbaseOperations().getCouchbaseBucket().name());
Expression whereCriteria = N1qlUtils.createWhereFilterForEntity(null, getCouchbaseOperations().getConverter(),
getEntityInformation());
//apply the sort
com.couchbase.client.java.query.dsl.Sort[] orderings = N1qlUtils.createSort(sort, getCouchbaseOperations().getConverter());
Statement st = selectFrom.where(whereCriteria).orderBy(orderings);
//fire the query
ScanConsistency consistency = getCouchbaseOperations().getDefaultConsistency().n1qlConsistency();
N1qlQuery query = N1qlQuery.simple(st, N1qlParams.build().consistency(consistency));
return mapFlux(getCouchbaseOperations().findByN1QL(query, getEntityInformation().getJavaType()));
}
}

View File

@@ -0,0 +1,313 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository.support;
import java.io.Serializable;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.error.DocumentDoesNotExistException;
import com.couchbase.client.java.view.AsyncViewResult;
import com.couchbase.client.java.view.AsyncViewRow;
import com.couchbase.client.java.view.ViewQuery;
import org.reactivestreams.Publisher;
import org.springframework.data.couchbase.core.RxJavaCouchbaseOperations;
import org.springframework.data.couchbase.core.query.View;
import org.springframework.data.couchbase.repository.ReactiveCouchbaseRepository;
import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import rx.Single;
import rx.Observable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Reactive repository base implementation for Couchbase.
*
* @author Subhashni Balakrishnan
* @since 3.0
*/
public class SimpleReactiveCouchbaseRepository<T, ID extends Serializable> implements ReactiveCouchbaseRepository<T, ID> {
/**
* Holds the reference to the {@link org.springframework.data.couchbase.core.RxJavaCouchbaseTemplate}.
*/
private final RxJavaCouchbaseOperations operations;
/**
* Contains information about the entity being used in this repository.
*/
private final CouchbaseEntityInformation<T, String> entityInformation;
/**
* Custom ViewMetadataProvider.
*/
private ViewMetadataProvider viewMetadataProvider;
/**
* Create a new Repository.
*
* @param metadata the Metadata for the entity.
* @param operations the reference to the reactive template used.
*/
public SimpleReactiveCouchbaseRepository(final CouchbaseEntityInformation<T, String> metadata,
final RxJavaCouchbaseOperations operations) {
Assert.notNull(operations);
Assert.notNull(metadata);
this.entityInformation = metadata;
this.operations = operations;
}
/**
* Configures a custom {@link ViewMetadataProvider} to be used to detect {@link View}s to be applied to queries.
*
* @param viewMetadataProvider that is used to lookup any annotated View on a query method.
*/
public void setViewMetadataProvider(final ViewMetadataProvider viewMetadataProvider) {
this.viewMetadataProvider = viewMetadataProvider;
}
protected Mono mapMono(Single single) {
return ReactiveWrapperConverters.toWrapper(single , Mono.class);
}
protected Flux mapFlux(Observable observable) {
return ReactiveWrapperConverters.toWrapper(observable, Flux.class);
}
@SuppressWarnings("unchecked")
public <S extends T> Mono<S> save(S entity) {
Assert.notNull(entity, "Entity must not be null!");
return mapMono(operations.save(entity).toSingle());
}
@SuppressWarnings("unchecked")
@Override
public <S extends T> Flux<S> save(Iterable<S> entities) {
Assert.notNull(entities, "The given Iterable of entities must not be null!");
return mapFlux(operations.save(entities));
}
@SuppressWarnings("unchecked")
@Override
public <S extends T> Flux<S> save(Publisher<S> entityStream) {
Assert.notNull(entityStream, "The given Iterable of entities must not be null!");
return Flux.from(entityStream)
.flatMap(object -> save(object));
}
@SuppressWarnings("unchecked")
@Override
public Mono<T> findOne(ID id) {
Assert.notNull(id, "The given id must not be null!");
return mapMono(operations.findById(id.toString(), entityInformation.getJavaType()).toSingle())
.otherwise(throwable -> {
//reactive streams adapter doesn't work with null
if(throwable instanceof NullPointerException) {
return Mono.empty();
}
return Mono.just(throwable);
});
}
@SuppressWarnings("unchecked")
@Override
public Mono<T> findOne(Mono<ID> mono) {
Assert.notNull(mono, "The given mono must not be null!");
return mono.then(
id -> findOne(id));
}
@SuppressWarnings("unchecked")
@Override
public Mono<Boolean> exists(ID id) {
Assert.notNull(id, "The given id must not be null!");
return mapMono(operations.exists(id.toString()).toSingle());
}
@SuppressWarnings("unchecked")
@Override
public Mono<Boolean> exists(Mono<ID> mono) {
return mono.then(
id -> exists(id));
}
@SuppressWarnings("unchecked")
@Override
public Flux<T> findAll() {
final ResolvedView resolvedView = determineView();
ViewQuery query = ViewQuery.from(resolvedView.getDesignDocument(), resolvedView.getViewName());
query.reduce(false);
query.stale(operations.getDefaultConsistency().viewConsistency());
return mapFlux(operations.findByView(query, entityInformation.getJavaType()));
}
@SuppressWarnings("unchecked")
@Override
public Flux<T> findAll(final Iterable<ID> ids) {
final ResolvedView resolvedView = determineView();
ViewQuery query = ViewQuery.from(resolvedView.getDesignDocument(), resolvedView.getViewName());
query.reduce(false);
query.stale(operations.getDefaultConsistency().viewConsistency());
JsonArray keys = JsonArray.create();
for (ID id : ids) {
keys.add(id);
}
query.keys(keys);
return mapFlux(operations.findByView(query, entityInformation.getJavaType()));
}
@SuppressWarnings("unchecked")
@Override
public Flux<T> findAll(Publisher<ID> entityStream) {
Assert.notNull(entityStream, "The given entityStream must not be null!");
return Flux.from(entityStream)
.flatMap(entity -> findOne(entity));
}
@SuppressWarnings("unchecked")
@Override
public Mono<Void> delete(ID id) {
Assert.notNull(id, "The given id must not be null!");
return mapMono(operations.remove(id.toString()).map(res -> Observable.<Void>empty()).toSingle());
}
@SuppressWarnings("unchecked")
@Override
public Mono<Void> delete(T entity) {
Assert.notNull(entity, "The given id must not be null!");
return mapMono(operations.remove(entity).map(res -> Observable.<Void>empty()).toSingle());
}
@SuppressWarnings("unchecked")
@Override
public Mono<Void> delete(Iterable<? extends T> entities) {
Assert.notNull(entities, "The given Iterable of entities must not be null!");
return mapMono(operations.remove(entities).map(res -> Observable.<Void>empty()).toSingle());
}
@Override
public Mono<Void> delete(Publisher<? extends T> entityStream) {
Assert.notNull(entityStream, "The given publisher of entities must not be null!");
return Flux.from(entityStream)
.flatMap(entity -> delete(entity)).single();
}
@SuppressWarnings("unchecked")
@Override
public Mono<Long> count() {
final ResolvedView resolvedView = determineView();
ViewQuery query = ViewQuery.from(resolvedView.getDesignDocument(), resolvedView.getViewName());
query.reduce(true);
query.stale(operations.getDefaultConsistency().viewConsistency());
return mapMono(operations
.queryView(query)
.flatMap(AsyncViewResult::rows)
.map(asyncViewRow ->
Long.valueOf(asyncViewRow.value().toString())).toSingle());
}
@SuppressWarnings("unchecked")
@Override
public Mono<Void> deleteAll() {
final ResolvedView resolvedView = determineView();
ViewQuery query = ViewQuery.from(resolvedView.getDesignDocument(), resolvedView.getViewName());
query.reduce(false);
query.stale(operations.getDefaultConsistency().viewConsistency());
return mapMono(operations.queryView(query)
.map(AsyncViewResult::rows)
.flatMap(row -> {
AsyncViewRow asyncViewRow = (AsyncViewRow) row;
return operations.remove(asyncViewRow.id())
.onErrorResumeNext(throwable -> {
if (throwable instanceof DocumentDoesNotExistException) {
return Observable.empty();
}
return Observable.error(throwable);
});
})
.toList()
.map(list -> Observable.<Void>empty())
.toSingle());
}
/**
* Returns the information for the underlying template.
*
* @return the underlying entity information.
*/
protected CouchbaseEntityInformation<T, String> getEntityInformation() {
return entityInformation;
}
/**
* Resolve a View based upon:
* <p/>
* 1. Any @View annotation that is present
* 2. If none are found, default designDocument to be the entity name (lowercase) and viewName to be "all".
*
* @return ResolvedView containing the designDocument and viewName.
*/
private ResolvedView determineView() {
String designDocument = StringUtils.uncapitalize(entityInformation.getJavaType().getSimpleName());
String viewName = "all";
final View view = viewMetadataProvider.getView();
if (view != null) {
designDocument = view.designDocument();
viewName = view.viewName();
}
return new ResolvedView(designDocument, viewName);
}
@Override
public RxJavaCouchbaseOperations getCouchbaseOperations(){
return operations;
}
/**
* Simple holder to allow an easier exchange of information.
*/
private final class ResolvedView {
private final String designDocument;
private final String viewName;
public ResolvedView(final String designDocument, final String viewName) {
this.designDocument = designDocument;
this.viewName = viewName;
}
private String getDesignDocument() {
return designDocument;
}
private String getViewName() {
return viewName;
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.core;
import org.springframework.data.annotation.Id;
import com.couchbase.client.java.repository.annotation.Field;
/**
* Test class for persisting and loading from {@link ReactiveCouchbaseTemplate}.
*
* @author Subhashni Balakrishnan
*/
public class ReactiveBeer {
@Id
private final String id;
private String name;
@Field("is_active")
private boolean active = true;
@Field("desc")
private String description;
public ReactiveBeer(String id, String name, Boolean active, String description) {
this.id = id;
this.name = name;
this.active = active;
this.description = description;
}
@Override
public String toString() {
return "Beer [id=" + id + ", name=" + name + ", active=" + active + ", description=" + description + "]";
}
public ReactiveBeer setName(String name) {
this.name = name;
return this;
}
public String getName() {
return name;
}
public ReactiveBeer setActive(boolean active) {
this.active = active;
return this;
}
public boolean getActive() {
return active;
}
public ReactiveBeer setDescription(String description) {
this.description = description;
return this;
}
public String getDescription() {
return description;
}
public String getId() {
return id;
}
}

View File

@@ -76,7 +76,7 @@ public class PartTreeN1qBasedQueryTest {
PartTreeN1qlBasedQuery query = new PartTreeN1qlBasedQuery(queryMethod, couchbaseOperations);
Statement statement = query.getCount(accessor, new Object[] { "value", pr });
assertEquals("SELECT COUNT(*) AS count FROM `default` WHERE name = \"value\" "
assertEquals("SELECT COUNT(*) AS count FROM `default` WHERE (name = \"value\") "
+ "AND `_class` = \"org.springframework.data.couchbase.core.Beer\"", statement.toString());
}
@@ -119,7 +119,7 @@ public class PartTreeN1qBasedQueryTest {
PartTreeN1qlBasedQuery query = new PartTreeN1qlBasedQuery(queryMethod, couchbaseOperations);
Statement statement = query.getCount(accessor, new Object[] { "value", pr });
assertEquals("SELECT COUNT(*) AS count FROM `default` WHERE name = \"value\" "
assertEquals("SELECT COUNT(*) AS count FROM `default` WHERE (name = \"value\") "
+ "AND `_class` = \"org.springframework.data.couchbase.core.Beer\"", statement.toString());
}

View File

@@ -1,58 +1,17 @@
package org.springframework.data.couchbase.repository.query;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery.*;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import static org.springframework.data.couchbase.repository.query.StringBasedN1qlQueryParser.*;
public class StringN1QlBasedQueryTest {
private static final SpelExpressionParser SPEL_PARSER = new SpelExpressionParser();
private static final EvaluationContext SPEL_EVALUATION_CONTEXT = new StandardEvaluationContext();
private StringN1qlBasedQuery mockStringUnderscoreClass;
private StringN1qlBasedQuery mockStringAtClass;
@Before
public void initMock() {
N1qlSpelValues contextStringUnderscoreClass = StringN1qlBasedQuery.createN1qlSpelValues("B", "_class", String.class, false);
N1qlSpelValues contextCountStringUnderscoreClass = StringN1qlBasedQuery.createN1qlSpelValues("B", "_class", String.class, true);
N1qlSpelValues contextStringAtClass = StringN1qlBasedQuery.createN1qlSpelValues("B", "@class", String.class, false);
N1qlSpelValues contextCountStringAtClass = StringN1qlBasedQuery.createN1qlSpelValues("B", "@class", String.class, true);
mockStringUnderscoreClass = mock(StringN1qlBasedQuery.class);
when(mockStringUnderscoreClass.parseSpel(anyString(), anyBoolean(), any(Object[].class)))
.thenAnswer(mockSpelEvaluation(contextStringUnderscoreClass, contextCountStringUnderscoreClass));
mockStringAtClass = mock(StringN1qlBasedQuery.class);
when(mockStringAtClass.parseSpel(anyString(),anyBoolean(), any(Object[].class)))
.thenAnswer(mockSpelEvaluation(contextStringAtClass, contextCountStringAtClass));
}
private static Answer<?> mockSpelEvaluation(final N1qlSpelValues spelValues, final N1qlSpelValues countSpelValues) {
return new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
String statement = (String) invocation.getArguments()[0];
boolean isCount = (Boolean) invocation.getArguments()[1];
if (isCount)
return doParse(statement, SPEL_PARSER, SPEL_EVALUATION_CONTEXT, countSpelValues);
else
return doParse(statement, SPEL_PARSER, SPEL_EVALUATION_CONTEXT, spelValues);
}
};
}
private static String spel(String expression) {
return "#{" + expression + "}";
}
@@ -60,7 +19,8 @@ public class StringN1QlBasedQueryTest {
@Test
public void testReplaceAllFullSelectPlaceholder() throws Exception {
String statement = spel(SPEL_SELECT_FROM_CLAUSE) + " where " + spel(SPEL_SELECT_FROM_CLAUSE);
String parsed = mockStringUnderscoreClass.parseSpel(statement, false, new Object[0]);
String parsed = new StringBasedN1qlQueryParser(statement, null, "B", "_class", String.class)
.doParse(SPEL_PARSER, SPEL_EVALUATION_CONTEXT, false);
assertEquals("SELECT META(`B`).id AS _ID, META(`B`).cas AS _CAS, `B`.* FROM `B` where "
+ "SELECT META(`B`).id AS _ID, META(`B`).cas AS _CAS, `B`.* FROM `B`", parsed);
@@ -69,7 +29,8 @@ public class StringN1QlBasedQueryTest {
@Test
public void testReplaceAllBucketPlaceholder() throws Exception {
String statement = "SELECT * FROM " + spel(SPEL_BUCKET) + " WHERE " + spel(SPEL_BUCKET) + ".test = 1";
String parsed = mockStringUnderscoreClass.parseSpel(statement, false, new Object[0]);
String parsed = new StringBasedN1qlQueryParser(statement, null, "B", "_class", String.class)
.doParse(SPEL_PARSER, SPEL_EVALUATION_CONTEXT, false);
assertEquals("SELECT * FROM `B` WHERE `B`.test = 1", parsed);
}
@@ -77,7 +38,8 @@ public class StringN1QlBasedQueryTest {
@Test
public void testReplaceAllEntityPlaceholder() throws Exception {
String statement = "SELECT " + spel(SPEL_ENTITY) + " FROM a where a.test = 1 and " + spel(SPEL_ENTITY);
String parsed = mockStringUnderscoreClass.parseSpel(statement, false, new Object[0]);
String parsed = new StringBasedN1qlQueryParser(statement, null, "B", "_class", String.class)
.doParse(SPEL_PARSER, SPEL_EVALUATION_CONTEXT, false);
assertEquals("SELECT META(`B`).id AS _ID, META(`B`).cas AS _CAS FROM a where a.test = 1 and "
+ "META(`B`).id AS _ID, META(`B`).cas AS _CAS", parsed);
@@ -86,7 +48,8 @@ public class StringN1QlBasedQueryTest {
@Test
public void testReplaceTypePlaceholder() throws Exception {
String statement = "SELECT " + spel(SPEL_ENTITY) + " FROM a WHERE a.test = 1 AND " + spel(SPEL_FILTER);
String parsed = mockStringAtClass.parseSpel(statement, false, new Object[0]);
String parsed = new StringBasedN1qlQueryParser(statement, null, "B", "@class", String.class)
.doParse(SPEL_PARSER, SPEL_EVALUATION_CONTEXT, false);
assertEquals("SELECT META(`B`).id AS _ID, META(`B`).cas AS _CAS FROM a WHERE a.test = 1 AND `@class` = "
+ "\"java.lang.String\"", parsed);
@@ -95,7 +58,8 @@ public class StringN1QlBasedQueryTest {
@Test
public void testReplaceSelectFromPlaceholderWithCountIfCountTrue() {
String statement = spel(SPEL_SELECT_FROM_CLAUSE) + " WHERE true";
String parsed = mockStringUnderscoreClass.parseSpel(statement, true, new Object[0]);
String parsed = new StringBasedN1qlQueryParser(statement, null, "B", "_class", String.class)
.doParse(SPEL_PARSER, SPEL_EVALUATION_CONTEXT, true);
assertEquals("SELECT COUNT(*) AS " + CountFragment.COUNT_ALIAS + " FROM `B` WHERE true", parsed);
}