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

@@ -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 "<couchbase:clusterInfo>" 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.
*