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:
21
pom.xml
21
pom.xml
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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, "");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.springframework.data.couchbase.repository;
|
||||
|
||||
|
||||
/**
|
||||
* @author Subhashni Balakrishnan
|
||||
*/
|
||||
public interface ReactivePartySortingRepository extends ReactiveCouchbaseSortingRepository<Party, String> {
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 "<couchbase:template />" 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 "<couchbase:template />" element for xml config.
|
||||
*/
|
||||
public static final String RXJAVA1_COUCHBASE_TEMPLATE = "rxjava1CouchbaseTemplate";
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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<String>} 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user