DATACOUCH-144 - Detect N1QL dependency and fail fast if not available.

Detect N1QL is not available at configuration time for the repositories and at runtime for the template. This throws a UnsupportedCouchbaseFeatureException.

The ClusterInfo is captured at bootstrap and used to determine if the feature is available. The xml configuration will need the ClusterInfo-related credentials to be provided explicitly.
This commit is contained in:
Simon Baslé
2015-07-17 18:49:34 +02:00
parent 93405c5da8
commit 712bc66006
31 changed files with 657 additions and 79 deletions

View File

@@ -51,7 +51,7 @@ public class CouchbaseTemplateParserIntegrationTests {
reader.loadBeanDefinitions(new ClassPathResource("configurations/couchbase-template-bean.xml"));
BeanDefinition definition = factory.getBeanDefinition("couchbaseTemplate");
assertEquals(1, definition.getConstructorArgumentValues().getArgumentCount());
assertEquals(2, definition.getConstructorArgumentValues().getArgumentCount());
factory.getBean("couchbaseTemplate");
}
@@ -61,7 +61,7 @@ public class CouchbaseTemplateParserIntegrationTests {
reader.loadBeanDefinitions(new ClassPathResource("configurations/couchbase-template-with-translation-service-bean.xml"));
BeanDefinition definition = factory.getBeanDefinition("couchbaseTemplate");
assertEquals(2, definition.getConstructorArgumentValues().getArgumentCount());
assertEquals(3, definition.getConstructorArgumentValues().getArgumentCount());
factory.getBean("couchbaseTemplate");
}

View File

@@ -19,6 +19,7 @@ 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.view.DefaultView;
import com.couchbase.client.java.view.DesignDocument;
import com.couchbase.client.java.view.View;
@@ -31,28 +32,29 @@ import org.springframework.test.context.support.DependencyInjectionTestExecution
*/
public class CouchbaseTemplateViewListener extends DependencyInjectionTestExecutionListener {
@Override
public void beforeTestClass(final TestContext testContext) throws Exception {
Bucket client = (Bucket) testContext.getApplicationContext().getBean("couchbaseBucket");
populateTestData(client);
createAndWaitForDesignDocs(client);
}
@Override
public void beforeTestClass(final TestContext testContext) throws Exception {
Bucket client = (Bucket) testContext.getApplicationContext().getBean("couchbaseBucket");
ClusterInfo clusterInfo = (ClusterInfo) testContext.getApplicationContext().getBean("couchbaseClusterInfo");
populateTestData(client, clusterInfo);
createAndWaitForDesignDocs(client);
}
private void populateTestData(Bucket client) {
CouchbaseTemplate template = new CouchbaseTemplate(client);
private void populateTestData(Bucket client, ClusterInfo clusterInfo) {
CouchbaseTemplate template = new CouchbaseTemplate(clusterInfo, client);
for (int i = 0; i < 100; i++) {
Beer b = new Beer("testbeer-" + i).setName("MyBeer " + i).setActive(true);
template.save(b);
}
}
for (int i = 0; i < 100; i++) {
Beer b = new Beer("testbeer-" + i).setName("MyBeer " + i).setActive(true);
template.save(b);
}
}
private void createAndWaitForDesignDocs(Bucket client) {
String mapFunction = "function (doc, meta) { if(doc._class == "
+ "\"org.springframework.data.couchbase.core.Beer\") { emit(doc.name, null); } }";
View view = DefaultView.create("by_name", mapFunction);
DesignDocument designDoc = DesignDocument.create("test_beers", Collections.singletonList(view));
client.bucketManager().upsertDesignDocument(designDoc);
}
private void createAndWaitForDesignDocs(Bucket client) {
String mapFunction = "function (doc, meta) { if(doc._class == "
+ "\"org.springframework.data.couchbase.core.Beer\") { emit(doc.name, null); } }";
View view = DefaultView.create("by_name", mapFunction);
DesignDocument designDoc = DesignDocument.create("test_beers", Collections.singletonList(view));
client.bucketManager().upsertDesignDocument(designDoc);
}
}

View File

@@ -23,6 +23,7 @@ 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;
@@ -40,12 +41,13 @@ public class CouchbaseRepositoryViewListener extends DependencyInjectionTestExec
@Override
public void beforeTestClass(final TestContext testContext) throws Exception {
Bucket client = (Bucket) testContext.getApplicationContext().getBean("couchbaseBucket");
populateTestData(client);
ClusterInfo clusterInfo = (ClusterInfo) testContext.getApplicationContext().getBean("couchbaseClusterInfo");
populateTestData(client, clusterInfo);
createAndWaitForDesignDocs(client);
}
private void populateTestData(final Bucket client) {
CouchbaseTemplate template = new CouchbaseTemplate(client);
private void populateTestData(final Bucket client, ClusterInfo clusterInfo) {
CouchbaseTemplate template = new CouchbaseTemplate(clusterInfo, client);
for (int i = 0; i < 100; i++) {
template.save(new User("testuser-" + i, "uname-" + i), PersistTo.MASTER, ReplicateTo.NONE);
}

View File

@@ -2,12 +2,12 @@ package org.springframework.data.couchbase.repository;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
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;
@@ -24,12 +24,13 @@ public class QueryDerivationConversionListener extends DependencyInjectionTestEx
@Override
public void beforeTestClass(final TestContext testContext) throws Exception {
Bucket client = (Bucket) testContext.getApplicationContext().getBean("couchbaseBucket");
populateTestData(client);
ClusterInfo clusterInfo = (ClusterInfo) testContext.getApplicationContext().getBean("couchbaseClusterInfo");
populateTestData(client, clusterInfo);
createAndWaitForDesignDocs(client);
}
private void populateTestData(Bucket client) {
CouchbaseTemplate template = new CouchbaseTemplate(client);
private void populateTestData(Bucket client, ClusterInfo clusterInfo) {
CouchbaseTemplate template = new CouchbaseTemplate(clusterInfo, client);
Calendar cal = Calendar.getInstance();
cal.clear();

View File

@@ -22,6 +22,7 @@ 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;
@@ -38,12 +39,13 @@ public class SimpleCouchbaseRepositoryListener extends DependencyInjectionTestEx
@Override
public void beforeTestClass(final TestContext testContext) throws Exception {
Bucket client = (Bucket) testContext.getApplicationContext().getBean("couchbaseBucket");
populateTestData(client);
ClusterInfo clusterInfo = (ClusterInfo) testContext.getApplicationContext().getBean("couchbaseClusterInfo");
populateTestData(client, clusterInfo);
createAndWaitForDesignDocs(client);
}
private void populateTestData(Bucket client) {
CouchbaseTemplate template = new CouchbaseTemplate(client);
private void populateTestData(Bucket client, ClusterInfo clusterInfo) {
CouchbaseTemplate template = new CouchbaseTemplate(clusterInfo, client);
for (int i = 0; i < 100; i++) {
User u = new User("testuser-" + i, "uname-" + i);

View File

@@ -0,0 +1,42 @@
/*
* 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.inject.Produces;
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 org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
/**
* Produces a {@link ClusterInfo} instance for test usage.
*
* @author Simon Baslé
*/
class CouchbaseClusterInfoProducer {
@Produces
public ClusterInfo createClusterInfo() throws Exception {
Cluster cluster = CouchbaseCluster.create();
return cluster.clusterManager("default", "").info();
}
}

View File

@@ -19,22 +19,24 @@ package org.springframework.data.couchbase.repository.cdi;
import javax.enterprise.inject.Produces;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.cluster.ClusterInfo;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
/**
* Produces a {@link CouchbaseOperations} instance for test usage.
*
* @author Mark Paluch
*/
class CouchbaseOperationsProducer {
@Produces
public CouchbaseOperations createCouchbaseOperations(Bucket couchbaseClient) throws Exception {
@Produces
public CouchbaseOperations createCouchbaseOperations(Bucket couchbaseClient, ClusterInfo clusterInfo) throws Exception {
CouchbaseTemplate couchbaseTemplate = new CouchbaseTemplate(couchbaseClient);
CouchbaseTemplate couchbaseTemplate = new CouchbaseTemplate(clusterInfo, couchbaseClient);
return couchbaseTemplate;
}
return couchbaseTemplate;
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2013 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.feature;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import com.couchbase.client.java.cluster.ClusterInfo;
import com.couchbase.client.java.query.Query;
import com.couchbase.client.java.util.features.CouchbaseFeature;
import com.couchbase.client.java.util.features.Version;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.UnsupportedCouchbaseFeatureException;
import org.springframework.data.couchbase.repository.User;
import org.springframework.data.couchbase.repository.UserRepository;
import org.springframework.data.couchbase.repository.support.CouchbaseRepositoryFactory;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* An integration test that validates feature checking with Java Config.
*
* @author Simon Baslé
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = FeatureDetectionTestApplicationConfig.class)
public class FeatureDetectionRepositoryTests {
@Autowired
private CouchbaseTemplate template;
@Autowired
private ClusterInfo clusterInfo;
@Before
public void checkClusterInfo() {
Assume.assumeTrue(clusterInfo.getMinVersion() == Version.NO_VERSION);
}
@Test
public void testN1qlIncompatibleClusterFailsFastForN1qlBasedRepository() throws Exception {
RepositoryFactorySupport factory = new CouchbaseRepositoryFactory(template);
try {
factory.getRepository(UserRepository.class);
fail("expected UnsupportedCouchbaseFeatureException");
} catch (UnsupportedCouchbaseFeatureException e) {
assertEquals(CouchbaseFeature.N1QL, e.getFeature());
}
}
@Test
public void testN1qlIncompatibleClusterDoesntFailForViewBasedRepository() throws Exception {
RepositoryFactorySupport factory = new CouchbaseRepositoryFactory(template);
ViewOnlyUserRepository repository = factory.getRepository(ViewOnlyUserRepository.class);
assertNotNull(repository);
}
@Test
public void testN1qlIncompatibleClusterTemplateFails() {
Query query = Query.simple("SELECT * FROM `" + template.getCouchbaseBucket().name() + "`");
try {
template.findByN1QL(query, User.class);
fail("expected findByN1QL to fail with UnsupportedCouchbaseFeatureException");
} catch (UnsupportedCouchbaseFeatureException e) {
assertEquals(CouchbaseFeature.N1QL, e.getFeature());
}
try {
template.findByN1QLProjection(query, User.class);
fail("expected findByN1QLProjection to fail with UnsupportedCouchbaseFeatureException");
} catch (UnsupportedCouchbaseFeatureException e) {
assertEquals(CouchbaseFeature.N1QL, e.getFeature());
}
try {
template.queryN1QL(query);
fail("expected queryN1QL to fail with UnsupportedCouchbaseFeatureException");
} catch (UnsupportedCouchbaseFeatureException e) {
assertEquals(CouchbaseFeature.N1QL, e.getFeature());
}
}
}

View File

@@ -0,0 +1,79 @@
package org.springframework.data.couchbase.repository.feature;
import java.util.Collections;
import java.util.List;
import com.couchbase.client.java.cluster.ClusterInfo;
import com.couchbase.client.java.cluster.DefaultClusterInfo;
import com.couchbase.client.java.document.json.JsonObject;
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.AbstractCouchbaseConfiguration;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.WriteResultChecking;
@Configuration
public class FeatureDetectionTestApplicationConfig extends AbstractCouchbaseConfiguration {
@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 CouchbaseTemplate couchbaseTemplate() throws Exception {
CouchbaseTemplate template = super.couchbaseTemplate();
template.setWriteResultChecking(WriteResultChecking.LOG);
return template;
}
@Override
public ClusterInfo couchbaseClusterInfo() throws Exception {
return new DefaultClusterInfo(JsonObject.empty());
}
//change the name of the field that will hold type information
@Override
public String typeKey() {
return "javaClass";
}
}

View File

@@ -0,0 +1,7 @@
package org.springframework.data.couchbase.repository.feature;
import org.springframework.data.couchbase.repository.User;
import org.springframework.data.repository.CrudRepository;
public interface ViewOnlyUserRepository extends CrudRepository<User, String> {
}

View File

@@ -8,6 +8,7 @@
<couchbase:env/>
<couchbase:cluster/>
<couchbase:clusterInfo/>
<couchbase:bucket id="cb-first"/>
<couchbase:bucket id="cb-second"/>

View File

@@ -7,6 +7,7 @@
<couchbase:env/>
<couchbase:cluster/>
<couchbase:clusterInfo/>
<couchbase:bucket/>
<couchbase:template/>

View File

@@ -7,6 +7,7 @@
<couchbase:env/>
<couchbase:cluster/>
<couchbase:clusterInfo/>
<couchbase:bucket/>
<couchbase:template translation-service-ref="myCustomTranslationService"/>

View File

@@ -7,6 +7,7 @@
<couchbase:env/>
<couchbase:cluster/>
<couchbase:clusterInfo/>
<couchbase:bucket/>
<!--note that the template needs both converter and translation service if you want to customize converter-->

View File

@@ -111,6 +111,10 @@ The library provides a custom namespace that you can use in your XML configurati
<couchbase:node>127.0.0.1</couchbase:node>
</couchbase:cluster>
<!-- This is needed to probe the server for N1QL support -->
<!-- Can be either cluster credentials or a bucket credentials -->
<couchbase:clusterInfo login="beer-sample" password=""/>
<couchbase:bucket bucketName="beer-sample" bucketPassword=""/>
</beans:beans>
----

View File

@@ -102,6 +102,8 @@ As of version `4.0`, Couchbase Server ships with a new query language called `N1
Prerequisite is to have a N1QL-compatible cluster and to have created a PRIMARY INDEX on the bucket where the entities will be stored.
WARNING: If it is detected at configuration time that the cluster doesn't support N1QL while there are @N1QL annotated methods or non-annotated methods in your repository interface, a `UnsupportedCouchbaseFeatureException` will be thrown.
Here is an example:
.An extended User repository with N1QL queries

View File

@@ -16,6 +16,8 @@ Removing documents through the `remove` methods works exactly the same.
If you want to load documents, you can do that through the `findById` method, which is the fastest and if possible your tool of choice. The find methods for views are `findByView` which converts it into the target entity, but also `queryView` which exposes lower level semantics. Similarly, find methods using N1QL are provided in `findByN1QL` and `queryN1QL`. Additionally, since N1QL allows you to select specific fields in documents (or even across documents using joins), `findByN1QLProjection` will allow you to skip full `Document` conversion and map these fields to an ad-hoc class.
WARNING: If it is detected at runtime that the cluster doesn't support N1QL, these methods will throw a `UnsupportedCouchbaseFeatureException`.
If you really need low-level semantics, the `couchbaseBucket` is also always in scope through `getCouchbaseBucket()`.
[[couchbase.template.xml]]
@@ -35,6 +37,7 @@ The template can be configured via xml, including setting a custom `TranslationS
<couchbase:env/>
<couchbase:cluster/>
<couchbase:clusterInfo/>
<couchbase:bucket/>
<couchbase:template translation-service-ref="myCustomTranslationService"/>
@@ -45,3 +48,5 @@ The template can be configured via xml, including setting a custom `TranslationS
----
====
NOTE: In the example above most tags assume their default values, that is a localhost cluster and bucket "default". In production you would have to also provide specifics to these tags.

View File

@@ -24,6 +24,7 @@ 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;
@@ -116,6 +117,11 @@ public abstract class AbstractCouchbaseConfiguration {
return CouchbaseCluster.create(couchbaseEnvironment(), getBootstrapHosts());
}
@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.
*
@@ -134,7 +140,7 @@ public abstract class AbstractCouchbaseConfiguration {
*/
@Bean(name = BeanNames.COUCHBASE_TEMPLATE)
public CouchbaseTemplate couchbaseTemplate() throws Exception {
return new CouchbaseTemplate(couchbaseClient(), mappingCouchbaseConverter(), translationService());
return new CouchbaseTemplate(couchbaseClusterInfo(), couchbaseClient(), mappingCouchbaseConverter(), translationService());
}
/**

View File

@@ -48,4 +48,8 @@ class BeanNames {
*/
static final String TRANSLATION_SERVICE = "couchbaseTranslationService";
/**
* Refers to the "&lt;couchbase:clusterInfo&gt;" bean
*/
static final String COUCHBASE_CLUSTER_INFO = "couchbaseClusterInfo";
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2012-2015 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 com.couchbase.client.java.Cluster;
import com.couchbase.client.java.cluster.ClusterInfo;
import org.springframework.beans.factory.config.AbstractFactoryBean;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.couchbase.core.CouchbaseExceptionTranslator;
/**
* The Factory Bean to help {@link CouchbaseClusterInfoParser} constructing a {@link ClusterInfo} from a given
* {@link Cluster} reference.
*
* @author Simon Baslé
*/
public class CouchbaseClusterInfoFactoryBean extends AbstractFactoryBean<ClusterInfo> implements PersistenceExceptionTranslator {
private final Cluster cluster;
private final String login;
private final String password;
private final PersistenceExceptionTranslator exceptionTranslator = new CouchbaseExceptionTranslator();
public CouchbaseClusterInfoFactoryBean(Cluster cluster, String login, String password) {
this.cluster = cluster;
this.login = login;
this.password = password;
}
@Override
public Class<?> getObjectType() {
return ClusterInfo.class;
}
@Override
protected ClusterInfo createInstance() throws Exception {
return cluster.clusterManager(login, password).info();
}
@Override
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
return exceptionTranslator.translateExceptionIfPossible(ex);
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright 2012-2015 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 com.couchbase.client.java.Cluster;
import com.couchbase.client.java.cluster.ClusterInfo;
import org.w3c.dom.Element;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.StringUtils;
/**
* The parser for XML definition of a {@link ClusterInfo}, to be constructed from a {@link Cluster} reference.
* If no reference is given, the default reference <code>{@value BeanNames#COUCHBASE_CLUSTER_INFO}</code> is used.
* <p/>
* See attributes {@link #CLUSTER_REF_ATTR}, {@link #LOGIN_ATTR} and {@link #PASSWORD_ATTR}.
*
* @author Simon Baslé
*/
public class CouchbaseClusterInfoParser extends AbstractSingleBeanDefinitionParser {
/**
* The <code>cluster-ref</code> attribute in a cluster info definition defines the cluster to build from.
*/
public static final String CLUSTER_REF_ATTR = "cluster-ref";
/**
* The <code>login</code> attribute in a cluster info definition defines the credential to use (can also be a
* bucket level credential).
*/
public static final String LOGIN_ATTR = "login";
/**
* The <code>password</code> attribute in a cluster info definition defines the credential's password.
*/
public static final String PASSWORD_ATTR = "password";
/**
* Resolve the bean ID and assign a default if not set.
*
* @param element the XML element which contains the attributes.
* @param definition the bean definition to work with.
* @param parserContext encapsulates the parsing state and configuration.
* @return the ID to work with.
*/
@Override
protected String resolveId(final Element element, final AbstractBeanDefinition definition, final ParserContext parserContext) {
String id = super.resolveId(element, definition, parserContext);
return StringUtils.hasText(id) ? id : BeanNames.COUCHBASE_CLUSTER_INFO;
}
/**
* Defines the bean class that will be constructed.
*
* @param element the XML element which contains the attributes.
* @return the class type to instantiate.
*/
@Override
protected Class getBeanClass(final Element element) {
return CouchbaseClusterInfoFactoryBean.class;
}
/**
* Parse the bean definition and build up the bean.
*
* @param element the XML element which contains the attributes.
* @param builder the builder which builds the bean.
*/
@Override
protected void doParse(final Element element, final BeanDefinitionBuilder builder) {
String clusterRef = element.getAttribute(CLUSTER_REF_ATTR);
if (!StringUtils.hasText(clusterRef)) {
clusterRef = BeanNames.COUCHBASE_CLUSTER;
}
builder.addConstructorArgReference(clusterRef);
String login = element.getAttribute(LOGIN_ATTR);
if (!StringUtils.hasText(login)) {
login = "default";
}
builder.addConstructorArgValue(login);
String password = element.getAttribute(PASSWORD_ATTR);
if (!StringUtils.hasText(password)) {
password = "";
}
builder.addConstructorArgValue(password);
}
}

View File

@@ -39,6 +39,7 @@ public class CouchbaseNamespaceHandler extends NamespaceHandlerSupport {
registerBeanDefinitionParser("repositories", new RepositoryBeanDefinitionParser(extension));
registerBeanDefinitionParser("env", new CouchbaseEnvironmentParser());
registerBeanDefinitionParser("cluster", new CouchbaseClusterParser());
registerBeanDefinitionParser("clusterInfo", new CouchbaseClusterInfoParser());
registerBeanDefinitionParser("bucket", new CouchbaseBucketParser());
registerBeanDefinitionParser("jmx", new CouchbaseJmxParser());
registerBeanDefinitionParser("template", new CouchbaseTemplateParser());

View File

@@ -67,10 +67,12 @@ public class CouchbaseTemplateParser extends AbstractSingleBeanDefinitionParser
*/
@Override
protected void doParse(final Element element, final BeanDefinitionBuilder bean) {
String clusterInfoRef = element.getAttribute("clusterInfo-ref");
String bucketRef = element.getAttribute("bucket-ref");
String converterRef = element.getAttribute("converter-ref");
String translationServiceRef = element.getAttribute("translation-service-ref");
bean.addConstructorArgReference(StringUtils.hasText(clusterInfoRef) ? clusterInfoRef : BeanNames.COUCHBASE_CLUSTER_INFO);
bean.addConstructorArgReference(StringUtils.hasText(bucketRef) ? bucketRef : BeanNames.COUCHBASE_BUCKET);
if (StringUtils.hasText(converterRef)) {

View File

@@ -23,6 +23,7 @@ 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.query.Query;
import com.couchbase.client.java.query.QueryParams;
import com.couchbase.client.java.query.QueryResult;
@@ -328,6 +329,13 @@ public interface CouchbaseOperations {
*/
Bucket getCouchbaseBucket();
/**
* Returns the {@link ClusterInfo} about the cluster linked to this template.
*
* @return the info about the cluster the template connects to.
*/
ClusterInfo getCouchbaseClusterInfo();
/**
* Returns the underlying {@link CouchbaseConverter}.
*

View File

@@ -29,6 +29,7 @@ import java.util.concurrent.TimeoutException;
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;
@@ -37,6 +38,7 @@ import com.couchbase.client.java.error.TranscodingException;
import com.couchbase.client.java.query.Query;
import com.couchbase.client.java.query.QueryResult;
import com.couchbase.client.java.query.QueryRow;
import com.couchbase.client.java.util.features.CouchbaseFeature;
import com.couchbase.client.java.view.ViewQuery;
import com.couchbase.client.java.view.ViewResult;
import com.couchbase.client.java.view.ViewRow;
@@ -88,6 +90,7 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP
private final Bucket client;
private final CouchbaseConverter converter;
private final TranslationService translationService;
private final ClusterInfo clusterInfo;
private ApplicationEventPublisher eventPublisher;
@@ -96,16 +99,18 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP
protected final MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> mappingContext;
public CouchbaseTemplate(final Bucket client) {
this(client, null, null);
public CouchbaseTemplate(final ClusterInfo clusterInfo, final Bucket client) {
this(clusterInfo, client, null, null);
}
public CouchbaseTemplate(final Bucket client, final TranslationService translationService) {
this(client, null, translationService);
public CouchbaseTemplate(final ClusterInfo clusterInfo, final Bucket client, final TranslationService translationService) {
this(clusterInfo, client, null, translationService);
}
public CouchbaseTemplate(final Bucket client, final CouchbaseConverter converter,
public CouchbaseTemplate(final ClusterInfo clusterInfo, final Bucket client,
final CouchbaseConverter converter,
final TranslationService translationService) {
this.clusterInfo = clusterInfo;
this.client = client;
this.converter = converter == null ? getDefaultConverter() : converter;
this.translationService = translationService == null ? getDefaultTranslationService() : translationService;
@@ -315,6 +320,7 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP
@Override
public <T> List<T> findByN1QL(Query n1ql, Class<T> entityClass) {
checkN1ql();
try {
QueryResult queryResult = queryN1QL(n1ql);
@@ -351,6 +357,7 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP
@Override
public <T> List<T> findByN1QLProjection(Query n1ql, Class<T> entityClass) {
checkN1ql();
try {
QueryResult queryResult = queryN1QL(n1ql);
@@ -379,6 +386,7 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP
@Override
public QueryResult queryN1QL(final Query query) {
checkN1ql();
return execute(new BucketCallback<QueryResult>() {
@Override
public QueryResult doInBucket() throws TimeoutException, ExecutionException, InterruptedException {
@@ -538,11 +546,23 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP
return (T) readEntity;
}
private void checkN1ql() {
if (!getCouchbaseClusterInfo().checkAvailable(CouchbaseFeature.N1QL)) {
throw new UnsupportedCouchbaseFeatureException("Detected usage of N1QL in template, which is unsupported on this cluster",
CouchbaseFeature.N1QL);
}
}
@Override
public Bucket getCouchbaseBucket() {
return this.client;
}
@Override
public ClusterInfo getCouchbaseClusterInfo() {
return this.clusterInfo;
}
@Override
public CouchbaseConverter getConverter() {
return this.converter;

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2012-2015 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 com.couchbase.client.java.util.features.CouchbaseFeature;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.NonTransientDataAccessException;
/**
* A {@link NonTransientDataAccessException} that denotes that a particular feature is expected
* on the server side but is not available.
*/
public class UnsupportedCouchbaseFeatureException extends InvalidDataAccessApiUsageException {
private final CouchbaseFeature feature;
public UnsupportedCouchbaseFeatureException(String msg, CouchbaseFeature feature) {
super(msg);
this.feature = feature;
}
public UnsupportedCouchbaseFeatureException(String msg, CouchbaseFeature feature, Throwable cause) {
super(msg, cause);
this.feature = feature;
}
/**
* @return the {@link CouchbaseFeature} that was missing (could be null if not
* a registered CouchbaseFeature, in which case see {@link #getMessage()}).
*/
public CouchbaseFeature getFeature() {
return feature;
}
}

View File

@@ -19,6 +19,7 @@ package org.springframework.data.couchbase.repository.query;
import java.util.List;
import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.view.Stale;
import com.couchbase.client.java.view.ViewQuery;
import com.couchbase.client.java.view.ViewResult;
import com.couchbase.client.java.view.ViewRow;

View File

@@ -19,9 +19,15 @@ 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.CouchbaseOperations;
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.view.Query;
import org.springframework.data.couchbase.core.view.View;
import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation;
import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod;
import org.springframework.data.couchbase.repository.query.PartTreeN1qlBasedQuery;
@@ -104,12 +110,31 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport {
*/
@Override
protected Object getTargetRepository(final RepositoryInformation metadata) {
checkFeatures(metadata);
CouchbaseEntityInformation<?, Serializable> entityInformation = getEntityInformation(metadata.getDomainType());
final SimpleCouchbaseRepository simpleCouchbaseRepository = new SimpleCouchbaseRepository(entityInformation, couchbaseOperations);
simpleCouchbaseRepository.setViewMetadataProvider(viewPostProcessor.getViewMetadataProvider());
return simpleCouchbaseRepository;
}
private void checkFeatures(RepositoryInformation metadata) {
boolean needsN1ql = false;
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 && !couchbaseOperations.getCouchbaseClusterInfo().checkAvailable(CouchbaseFeature.N1QL)) {
throw new UnsupportedCouchbaseFeatureException("Repository uses N1QL", CouchbaseFeature.N1QL);
}
}
/**
* The base class for this repository.
*

View File

@@ -16,14 +16,14 @@
package org.springframework.data.couchbase.repository.support;
import java.io.Serializable;
import org.springframework.data.couchbase.core.CouchbaseOperations;
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;
import java.io.Serializable;
/**
* The factory bean to create repositories.
*

View File

@@ -42,6 +42,19 @@
</xsd:complexType>
</xsd:element>
<xsd:element name="clusterInfo">
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="beans:identifiedType">
<xsd:attribute name="cluster-ref" type="xsd:string" use="optional"/>
<!-- note that login/password can be a bucketName and bucketPassword -->
<xsd:attribute name="login" type="xsd:string" use="optional"/>
<xsd:attribute name="password" type="xsd:string" use="optional"/>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
<xsd:element name="template">
<xsd:complexType>
<xsd:attribute name="id" type="xsd:string" use="optional">
@@ -50,6 +63,18 @@
The id of the couchbase definition (by default "couchbaseFactory").]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="clusterInfo-ref" type="xsd:string" use="optional">
<xsd:annotation>
<xsd:documentation>
The reference to a ClusterInfo object giving information about the cluster this template connects to.
</xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:assignable-to type="com.couchbase.client.java.cluster.ClusterInfo"/>
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="bucket-ref" type="xsd:string" use="optional">
<xsd:annotation>
<xsd:documentation>

View File

@@ -1,5 +1,7 @@
package org.springframework.data.couchbase;
import static org.mockito.Mockito.when;
import java.util.Collections;
import java.util.List;
@@ -7,6 +9,10 @@ import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.Cluster;
import com.couchbase.client.java.CouchbaseBucket;
import com.couchbase.client.java.CouchbaseCluster;
import com.couchbase.client.java.cluster.ClusterInfo;
import com.couchbase.client.java.cluster.DefaultClusterInfo;
import com.couchbase.client.java.util.features.CouchbaseFeature;
import com.couchbase.client.java.util.features.Version;
import org.mockito.Mockito;
import org.springframework.context.annotation.Bean;
@@ -18,45 +24,53 @@ import org.springframework.data.couchbase.core.WriteResultChecking;
@Configuration
public class UnitTestApplicationConfig extends AbstractCouchbaseConfiguration {
@Bean
public String couchbaseAdminUser() {
return "someLogin";
}
@Bean
public String couchbaseAdminUser() {
return "someLogin";
}
@Bean
public String couchbaseAdminPassword() {
return "somePassword";
}
@Bean
public String couchbaseAdminPassword() {
return "somePassword";
}
@Override
protected List<String> getBootstrapHosts() {
return Collections.singletonList("192.1.2.3");
}
@Override
protected List<String> getBootstrapHosts() {
return Collections.singletonList("192.1.2.3");
}
@Override
protected String getBucketName() {
return "someBucket";
}
@Override
protected String getBucketName() {
return "someBucket";
}
@Override
protected String getBucketPassword() {
return "someBucketPassword";
}
@Override
protected String getBucketPassword() {
return "someBucketPassword";
}
@Override
public Cluster couchbaseCluster() throws Exception {
return Mockito.mock(CouchbaseCluster.class);
}
@Override
public Cluster couchbaseCluster() throws Exception {
return Mockito.mock(CouchbaseCluster.class);
}
@Override
public Bucket couchbaseClient() throws Exception {
return Mockito.mock(CouchbaseBucket.class);
}
@Override
public ClusterInfo couchbaseClusterInfo() {
DefaultClusterInfo mock = Mockito.mock(DefaultClusterInfo.class);
when(mock.checkAvailable(CouchbaseFeature.N1QL)).thenReturn(true);
when(mock.getMinVersion()).thenReturn(new Version(4, 0, 0));
return mock;
}
@Override
public CouchbaseTemplate couchbaseTemplate() throws Exception {
CouchbaseTemplate template = super.couchbaseTemplate();
template.setWriteResultChecking(WriteResultChecking.LOG);
return template;
}
@Override
public Bucket couchbaseClient() throws Exception {
return Mockito.mock(CouchbaseBucket.class);
}
@Override
public CouchbaseTemplate couchbaseTemplate() throws Exception {
CouchbaseTemplate template = super.couchbaseTemplate();
template.setWriteResultChecking(WriteResultChecking.LOG);
return template;
}
}