DATACOUCH-165 - Add geospatial support
Add support for geo queries through the use of Spatial Views. This needs the query method to be annotated with @Dimensional (or a meta-annotation of it that is runtime retained). See SpatialViewQueryCreator javadoc for supported part types and corresponding expected arguments (to be documented). In a nutshell WITHIN Box (other shapes supported but approximated to bounding box), NEAR Point+Distance (approximated to bounding box), plus a few simple operators for dimensions beyond 2. When approximating, a DEBUG log is made along the logging of the query. Conversion methods from Shapes to bounding boxes are done in a GeoUtils class. Unit and Integration test of the feature. Added documentation for @Dimensional query derivation.
This commit is contained in:
@@ -28,6 +28,8 @@ import com.couchbase.client.java.query.N1qlQuery;
|
||||
import com.couchbase.client.java.query.N1qlQueryResult;
|
||||
import com.couchbase.client.java.query.N1qlParams;
|
||||
import com.couchbase.client.java.query.Statement;
|
||||
import com.couchbase.client.java.view.SpatialViewQuery;
|
||||
import com.couchbase.client.java.view.SpatialViewResult;
|
||||
import com.couchbase.client.java.view.ViewQuery;
|
||||
import com.couchbase.client.java.view.ViewResult;
|
||||
|
||||
@@ -213,6 +215,28 @@ public interface CouchbaseOperations {
|
||||
*/
|
||||
ViewResult queryView(ViewQuery query);
|
||||
|
||||
/**
|
||||
* Query a Spatial View for a list of documents of type T.
|
||||
* <p/>
|
||||
* <p>It is valid to pass in a empty constructed {@link SpatialViewQuery} object.</p>
|
||||
* <p/>
|
||||
*
|
||||
* @param query the SpatialViewQuery object (also specifying view design document and view name).
|
||||
* @param entityClass the entity to map to.
|
||||
* @return the converted collection
|
||||
*/
|
||||
<T> List<T> findBySpatialView(SpatialViewQuery query, Class<T> entityClass);
|
||||
|
||||
/**
|
||||
* Query a Spatial View with direct access to the {@link SpatialViewResult}.
|
||||
* <p>This method is available to ease the working with spatial views by still wrapping exceptions into the Spring
|
||||
* infrastructure.</p>
|
||||
*
|
||||
* @param query the SpatialViewQuery object (also specifying view design document and view name).
|
||||
* @return SpatialViewResult containing the results of the query.
|
||||
*/
|
||||
SpatialViewResult querySpatialView(SpatialViewQuery query);
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -349,5 +373,4 @@ public interface CouchbaseOperations {
|
||||
* @return the consistency to use for generated repository queries.
|
||||
*/
|
||||
Consistency getDefaultConsistency();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,9 @@ import com.couchbase.client.java.query.N1qlQueryRow;
|
||||
import com.couchbase.client.java.query.N1qlParams;
|
||||
import com.couchbase.client.java.query.consistency.ScanConsistency;
|
||||
import com.couchbase.client.java.util.features.CouchbaseFeature;
|
||||
import com.couchbase.client.java.view.SpatialViewQuery;
|
||||
import com.couchbase.client.java.view.SpatialViewResult;
|
||||
import com.couchbase.client.java.view.SpatialViewRow;
|
||||
import com.couchbase.client.java.view.ViewQuery;
|
||||
import com.couchbase.client.java.view.ViewResult;
|
||||
import com.couchbase.client.java.view.ViewRow;
|
||||
@@ -292,7 +295,7 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP
|
||||
|
||||
@Override
|
||||
public <T> List<T> findByView(ViewQuery query, Class<T> entityClass) {
|
||||
query.includeDocs(false);
|
||||
query.includeDocs(false); //FIXME the doc says it is set to true...
|
||||
query.reduce(false);
|
||||
|
||||
try {
|
||||
@@ -326,6 +329,41 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> findBySpatialView(SpatialViewQuery query, Class<T> entityClass) {
|
||||
//note: don't make any assumption about includeDocs and let the user decide
|
||||
|
||||
try {
|
||||
final SpatialViewResult response = querySpatialView(query);
|
||||
if (response.error() != null) {
|
||||
throw new CouchbaseQueryExecutionException("Unable to execute spatial view query due to the following view error: " +
|
||||
response.error().toString());
|
||||
}
|
||||
|
||||
List<SpatialViewRow> allRows = response.allRows();
|
||||
|
||||
final List<T> result = new ArrayList<T>(allRows.size());
|
||||
for (final SpatialViewRow row : allRows) {
|
||||
result.add(mapToEntity(row.id(), row.document(RawJsonDocument.class), entityClass));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (TranscodingException e) {
|
||||
throw new CouchbaseQueryExecutionException("Unable to execute view query", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpatialViewResult querySpatialView(final SpatialViewQuery query) {
|
||||
return execute(new BucketCallback<SpatialViewResult>() {
|
||||
@Override
|
||||
public SpatialViewResult doInBucket() throws TimeoutException, ExecutionException, InterruptedException {
|
||||
return client.query(query);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> findByN1QL(N1qlQuery n1ql, Class<T> entityClass) {
|
||||
checkN1ql();
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.view;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.data.annotation.QueryAnnotation;
|
||||
|
||||
/**
|
||||
* An annotation to mark a repository method as querying using a Couchbase Spatial View.
|
||||
* The attributes will be used to resolve the actual Spatial View to be used and to determine
|
||||
* the expected number of dimensions in the Spatial View keys. It can also be used as meta-annotation.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
|
||||
@QueryAnnotation
|
||||
public @interface Dimensional {
|
||||
/** The name of the design document to be queried */
|
||||
String designDocument();
|
||||
/** The name of the spatial view to be queried */
|
||||
String spatialViewName();
|
||||
/** The number of dimensions in the spatial view, defaults to 2 */
|
||||
int dimensions() default 2;
|
||||
}
|
||||
@@ -49,6 +49,10 @@ import org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery;
|
||||
* {@value StringN1qlBasedQuery#PLACEHOLDER_ENTITY}
|
||||
* (see {@link StringN1qlBasedQuery#PLACEHOLDER_ENTITY})
|
||||
* </li>
|
||||
* <li>
|
||||
* {@value StringN1qlBasedQuery#PLACEHOLDER_FILTER_TYPE}
|
||||
* (see {@link StringN1qlBasedQuery#PLACEHOLDER_FILTER_TYPE})
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Simon Baslé.
|
||||
|
||||
@@ -22,6 +22,8 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.data.annotation.QueryAnnotation;
|
||||
|
||||
/**
|
||||
* Annotation to support the use of Views with Couchbase.
|
||||
*
|
||||
@@ -31,6 +33,7 @@ import java.lang.annotation.Target;
|
||||
@Documented
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
//don't set @QueryAnnotation, as it causes problems with reduce and replacing count() method, the reduce detection needs to be improved
|
||||
public @interface View {
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.lang.reflect.Method;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
|
||||
import org.springframework.data.couchbase.core.view.Dimensional;
|
||||
import org.springframework.data.couchbase.core.view.Query;
|
||||
import org.springframework.data.couchbase.core.view.View;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
@@ -101,6 +102,21 @@ public class CouchbaseQueryMethod extends QueryMethod {
|
||||
return method.getAnnotation(View.class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return true if the method has a @Dimensional annotation, false otherwise.
|
||||
*/
|
||||
public boolean hasDimensionalAnnotation() {
|
||||
return getDimensionalAnnotation() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the @Dimensional annotation if set, null otherwise.
|
||||
*/
|
||||
public Dimensional getDimensionalAnnotation() {
|
||||
return AnnotationUtils.findAnnotation(method, Dimensional.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the method has a @Query annotation.
|
||||
*
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProper
|
||||
import org.springframework.data.couchbase.repository.query.support.N1qlUtils;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.geo.GeoResult;
|
||||
import org.springframework.data.mapping.context.PersistentPropertyPath;
|
||||
import org.springframework.data.repository.query.ParameterAccessor;
|
||||
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2013-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.repository.query;
|
||||
|
||||
import com.couchbase.client.java.view.SpatialViewQuery;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.data.couchbase.core.CouchbaseOperations;
|
||||
import org.springframework.data.couchbase.core.view.Dimensional;
|
||||
import org.springframework.data.repository.query.ParametersParameterAccessor;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
|
||||
/**
|
||||
* Execute a {@link Dimensional} repository query through the Spatial View mechanism.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
public class SpatialViewBasedQuery implements RepositoryQuery {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SpatialViewBasedQuery.class);
|
||||
|
||||
private final CouchbaseQueryMethod method;
|
||||
private final CouchbaseOperations operations;
|
||||
|
||||
public SpatialViewBasedQuery(CouchbaseQueryMethod method, CouchbaseOperations 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();
|
||||
|
||||
/*
|
||||
here contrary to the classical view query we don't support not including an attribute of
|
||||
the entity in the method name, those are mandatory and will result in a PropertyReferenceException
|
||||
if not used...
|
||||
*/
|
||||
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 ParametersParameterAccessor(method.getParameters(), runtimeParams),
|
||||
baseSpatialQuery, operations.getConverter());
|
||||
SpatialViewQuery finalQuery = creator.createQuery();
|
||||
|
||||
//execute the spatial query
|
||||
return execute(finalQuery);
|
||||
}
|
||||
|
||||
protected Object execute(SpatialViewQuery query) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Executing spatial view query: " + query.toString());
|
||||
}
|
||||
return operations.findBySpatialView(query, method.getEntityInformation().getJavaType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CouchbaseQueryMethod getQueryMethod() {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* 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.repository.query;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
import com.couchbase.client.java.document.json.JsonArray;
|
||||
import com.couchbase.client.java.view.SpatialViewQuery;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
|
||||
import org.springframework.data.couchbase.core.view.Dimensional;
|
||||
import org.springframework.data.couchbase.repository.query.support.GeoUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.geo.Box;
|
||||
import org.springframework.data.geo.Circle;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.geo.Polygon;
|
||||
import org.springframework.data.geo.Shape;
|
||||
import org.springframework.data.repository.query.ParameterAccessor;
|
||||
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
|
||||
import org.springframework.data.repository.query.parser.Part;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
|
||||
/**
|
||||
* A QueryCreator that will enrich a {@link SpatialViewQuery} using query derivation mechanisms
|
||||
* and the parsed {@link PartTree}.
|
||||
* <p>
|
||||
* Support for query derivation keywords is limited, it is triggered by having a {@link Dimensional} annotation
|
||||
* on the query method.
|
||||
* <br/>
|
||||
* Here are the {@link Part.Type} supported:
|
||||
* <ul>
|
||||
* <li><b>WITHIN:</b> finds elements contained in the provided {@link Shape}, {@link Point Point[] polygon},
|
||||
* pair of {@link Point}s bounding box (lower left+upper right) or pair of raw {@link JsonArray} (discouraged as it
|
||||
* leaks Couchbase specific class in your method signature, needs to be numerical data)</li>
|
||||
* <li><b>NEAR:</b> finds elements near a provided {@link Point}, within the provided {@link Distance}</li>
|
||||
* <li><b>GREATER_THAN, AFTER, GREATER_THAN_EQUALS</b>: adds a numerical element to the startRange and null to the endRange
|
||||
* (useful for non geographic additional dimensions)</li>
|
||||
* <li><b>LESS_THAN, BEFORE, LESS_THAN_EQUALS</b>: adds null to the startRange and a numerical element to the endRange
|
||||
* (useful for non geographic additional dimensions)</li>
|
||||
* <li><b>SIMPLE_PROPERTY (Is, Equals)</b>: adds a numerical element to both the startRange and the endRange
|
||||
* (useful for non geographic additional dimensions)</li>
|
||||
* <li><b>BETWEEN</b>: adds a numerical element to the startRange and a second numerical element to the endRange
|
||||
* (useful for non geographic additional dimensions)</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
* Additionally, {@link PartTree#isLimiting()} will trigger usage of {@link SpatialViewQuery#limit(int) limit}.
|
||||
*/
|
||||
public class SpatialViewQueryCreator extends AbstractQueryCreator<SpatialViewQuery, SpatialViewQuery> {
|
||||
|
||||
private final SpatialViewQuery query;
|
||||
private final PartTree tree;
|
||||
private final CouchbaseConverter converter;
|
||||
|
||||
private final int dimensions;
|
||||
private final JsonArray startRange;
|
||||
private final JsonArray endRange;
|
||||
|
||||
public SpatialViewQueryCreator(int dimensions, PartTree tree, ParameterAccessor parameters, SpatialViewQuery query,
|
||||
CouchbaseConverter converter) {
|
||||
super(tree, parameters);
|
||||
this.query = query;
|
||||
this.tree = tree;
|
||||
this.converter = converter;
|
||||
this.dimensions = dimensions;
|
||||
this.startRange = JsonArray.create();
|
||||
this.endRange = JsonArray.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SpatialViewQuery create(Part part, Iterator<Object> objectIterator) {
|
||||
ConvertingIterator iterator = new ConvertingIterator(objectIterator, converter);
|
||||
|
||||
switch (part.getType()) {
|
||||
case WITHIN:
|
||||
applyWithin(startRange, endRange, iterator);
|
||||
break;
|
||||
case NEAR:
|
||||
applyNear(startRange, endRange, iterator);
|
||||
break;
|
||||
case GREATER_THAN:
|
||||
case GREATER_THAN_EQUAL:
|
||||
case AFTER:
|
||||
startRange.add(checkedNext(iterator, Object.class, null));
|
||||
endRange.addNull();
|
||||
break;
|
||||
case LESS_THAN:
|
||||
case LESS_THAN_EQUAL:
|
||||
case BEFORE:
|
||||
startRange.addNull();
|
||||
endRange.add(checkedNext(iterator, Object.class, null));
|
||||
break;
|
||||
case SIMPLE_PROPERTY:
|
||||
Object equals = checkedNext(iterator, Object.class, null);
|
||||
startRange.add(equals);
|
||||
endRange.add(equals);
|
||||
break;
|
||||
case BETWEEN:
|
||||
startRange.add(checkedNext(iterator, Object.class, null));
|
||||
endRange.add(checkedNext(iterator, Object.class, null));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported keyword in Spatial View query derivation: " + part.toString());
|
||||
}
|
||||
|
||||
//will complete the ranges in complete step (if not enough elements for the ranges to match dimension count)
|
||||
return query;
|
||||
}
|
||||
|
||||
private static void completeRangeIfNeeded(JsonArray range, int dimensions) {
|
||||
for (int i = range.size(); i < dimensions; i++) {
|
||||
range.addNull();
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyWithin(JsonArray startRange, JsonArray endRange, Iterator<Object> iterator) {
|
||||
if (!iterator.hasNext()) {
|
||||
throw new IllegalArgumentException("Not enough parameters for within");
|
||||
}
|
||||
|
||||
Object next = iterator.next();
|
||||
if (next instanceof Shape) {
|
||||
GeoUtils.convertShapeTo2DRanges(startRange, endRange, (Shape) next);
|
||||
} else if (next instanceof Point) {
|
||||
//expect another point for the other corner of the bounding box
|
||||
Point northwest = (Point) next;
|
||||
Point southeast = checkedNext(iterator, Point.class, "Cannot compute a bounding box for within, 2 Point needed");
|
||||
GeoUtils.convertPointsTo2DRanges(startRange, endRange, true, northwest, southeast);
|
||||
} else if (next instanceof Point[]) {
|
||||
GeoUtils.convertPointsTo2DRanges(startRange, endRange, false, (Point[]) next);
|
||||
} else if (next instanceof JsonArray) { //discouraged, leaks Couchbase classes into signatures
|
||||
JsonArray first = (JsonArray) next;
|
||||
for (Object o : first) {
|
||||
startRange.add(o);
|
||||
}
|
||||
|
||||
JsonArray second = checkedNext(iterator, JsonArray.class, "2 JsonArray required for within: startRange and endRange");
|
||||
for (Object o : second) {
|
||||
endRange.add(o);
|
||||
}
|
||||
} else {
|
||||
//TODO Collection<Point>: same as polygon
|
||||
//TODO Collection<Double/Number>: one collection per range
|
||||
//TODO Number[]: one array per range
|
||||
throw new IllegalArgumentException("Unsupported parameter type for within: " + next.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyNear(JsonArray startRange, JsonArray endRange, Iterator<Object> iterator) {
|
||||
if (!iterator.hasNext()) {
|
||||
throw new IllegalArgumentException("Not enough parameters for near");
|
||||
}
|
||||
|
||||
Point near = checkedNext(iterator, Point.class, "Near queries need a Point as first argument");
|
||||
Distance distance = checkedNext(iterator, Distance.class, "Near queries need a maximum Distance as second argument");
|
||||
|
||||
double[] boundingBox = GeoUtils.getBoundingBoxForNear(near, distance);
|
||||
|
||||
startRange.add(boundingBox[0]).add(boundingBox[1]);
|
||||
endRange.add(boundingBox[2]).add(boundingBox[3]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve next value in the iterator assuming it is of the specified type (otherwise throw
|
||||
* an {@link IllegalArgumentException}.
|
||||
*
|
||||
* @param iterator the iterator to peek into.
|
||||
* @param clazz the expected type of the next value in the iterator.
|
||||
* @param errorMsg the error message prefix to use in the exception (will append a short message
|
||||
* describing if the iterator has no value or if the type found was different than expected).
|
||||
* @param <T> the desired return type.
|
||||
* @return the next value as a T.
|
||||
* @throws IllegalArgumentException if there is no next value or it doesn't conform to the desired type.
|
||||
*/
|
||||
private static <T> T checkedNext(Iterator<?> iterator, Class<T> clazz, String errorMsg) {
|
||||
if (errorMsg == null) {
|
||||
errorMsg = "Expected an additional parameter of type " + clazz.getName();
|
||||
}
|
||||
|
||||
if (!iterator.hasNext()) {
|
||||
throw new IllegalArgumentException(errorMsg + ", missing parameter");
|
||||
}
|
||||
|
||||
Object next = iterator.next();
|
||||
if (clazz.isInstance(next)) {
|
||||
return (T) next;
|
||||
} else if (next == null) {
|
||||
throw new IllegalArgumentException(errorMsg + ", got null");
|
||||
} else {
|
||||
throw new IllegalArgumentException(errorMsg + ", got a " + next.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SpatialViewQuery and(Part part, SpatialViewQuery base, Iterator<Object> iterator) {
|
||||
return create(part, iterator);//and not really supported, all query derivation mutate the original ViewQuery
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SpatialViewQuery or(SpatialViewQuery base, SpatialViewQuery criteria) {
|
||||
//this won't be called unless there's a Or keyword in the method
|
||||
throw new UnsupportedOperationException("Or is not supported for View-based queries");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SpatialViewQuery complete(SpatialViewQuery criteria, Sort sort) {
|
||||
if (sort != null) {
|
||||
throw new IllegalArgumentException("Sort is not supported on Spatial View queries");
|
||||
}
|
||||
|
||||
if (tree.isLimiting()) {
|
||||
query.limit(tree.getMaxResults());
|
||||
}
|
||||
|
||||
if (startRange.isEmpty() && endRange.isEmpty()) {
|
||||
return query;
|
||||
}
|
||||
|
||||
completeRangeIfNeeded(startRange, dimensions);
|
||||
completeRangeIfNeeded(endRange, dimensions);
|
||||
return query.range(startRange, endRange);
|
||||
}
|
||||
}
|
||||
@@ -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.SpatialViewQuery;
|
||||
import com.couchbase.client.java.view.Stale;
|
||||
import com.couchbase.client.java.view.ViewQuery;
|
||||
import com.couchbase.client.java.view.ViewResult;
|
||||
@@ -28,6 +29,8 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.data.couchbase.core.CouchbaseOperations;
|
||||
import org.springframework.data.couchbase.core.CouchbaseQueryExecutionException;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.Shape;
|
||||
import org.springframework.data.mapping.PropertyReferenceException;
|
||||
import org.springframework.data.repository.query.ParametersParameterAccessor;
|
||||
import org.springframework.data.repository.query.QueryMethod;
|
||||
@@ -81,23 +84,33 @@ public class ViewBasedCouchbaseQuery implements RepositoryQuery {
|
||||
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());
|
||||
|
||||
ViewQueryCreator creator = new ViewQueryCreator(tree,
|
||||
new ParametersParameterAccessor(method.getParameters(), runtimeParams),
|
||||
//use a ViewQueryCreator to complete the base query
|
||||
ViewQueryCreator creator = new ViewQueryCreator(tree, new ParametersParameterAccessor(method.getParameters(), runtimeParams),
|
||||
baseQuery, operations.getConverter());
|
||||
|
||||
ViewQuery query = creator.createQuery();
|
||||
|
||||
//detect count prefix in the method name and consider it triggers a reduce
|
||||
if (tree.isCountProjection() == Boolean.TRUE) {
|
||||
return executeReduce(query, designDoc, viewName);
|
||||
} else {
|
||||
//otherwise just execute the query
|
||||
return execute(query);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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.repository.query.support;
|
||||
|
||||
import com.couchbase.client.java.document.json.JsonArray;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.data.geo.Box;
|
||||
import org.springframework.data.geo.Circle;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.geo.Polygon;
|
||||
import org.springframework.data.geo.Shape;
|
||||
|
||||
/**
|
||||
* Utility class to deal with geo/dimensional indexed data and queries.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
public class GeoUtils {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(GeoUtils.class);
|
||||
|
||||
/**
|
||||
* Computes the bounding box approximation for a "near" query (max distance from a point of origin).
|
||||
*
|
||||
* @param origin the point of origin, center for the query.
|
||||
* @param distance the max distance to search within (negative distances will be multiplied by -1).
|
||||
* @return the bounding box approximation for this query ([xMin, yMin, xMax, yMax]).
|
||||
* @throws NullPointerException if any of the origin and distance are null
|
||||
*/
|
||||
public static double[] getBoundingBoxForNear(Point origin, Distance distance) {
|
||||
if (origin == null || distance == null) throw new NullPointerException("Origin and distance required");
|
||||
warnBoundingBoxApproximation("near a Point (within Distance)");
|
||||
|
||||
//since maxDistance COULD be negative, we have to make sure we have correct min/max
|
||||
double maxDistance = Math.abs(distance.getNormalizedValue());
|
||||
double xMin = origin.getX() - maxDistance;
|
||||
double yMin = origin.getY() - maxDistance;
|
||||
double xMax = origin.getX() + maxDistance;
|
||||
double yMax = origin.getY() + maxDistance;
|
||||
|
||||
return new double[] { xMin, yMin, xMax, yMax };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a sequence of {@link Point Points} describing a polygon to a pair of
|
||||
* {@link JsonArray} ranges corresponding to that polygon's bounding box,
|
||||
* and inject the coordinates into startRange and endRange.
|
||||
* If it is already equivalent to a Box (upper-left Point + lower-right Point), set
|
||||
* <i>isBoundingBox</i> to true.
|
||||
* Otherwise, this method will attempt to find the bounding box by finding the lowest
|
||||
* and highest X and Y coordinates.
|
||||
*
|
||||
* @param startRange the startRange to populate with this shape's data.
|
||||
* @param endRange the endRange to populate with this shape's data.
|
||||
* @param isBoundingBox true to skip finding min/max X and Y coordinates and use 2 Points as a {@link Box}.
|
||||
* @param points the sequence of Points.
|
||||
* @throws IllegalArgumentException if no points are provided, or in the case of isBoundingBox true
|
||||
* if more or less than 2 points are provided or the 2 points are not ordered (a.x <= b.x && a.y <= b.y).
|
||||
*/
|
||||
public static void convertPointsTo2DRanges(JsonArray startRange, JsonArray endRange, boolean isBoundingBox, Point... points) {
|
||||
if (points == null || points.length == 0) {
|
||||
throw new IllegalArgumentException("Needs points to convert");
|
||||
}
|
||||
|
||||
if (isBoundingBox) {
|
||||
if (points.length != 2) {
|
||||
throw new IllegalArgumentException("Bounding box must be made of 2 points");
|
||||
}
|
||||
if (points[0].getX() > points[1].getX() || points[0].getY() > points[1].getY()) {
|
||||
throw new IllegalArgumentException("Bounding box must have point A on the lower left of point B");
|
||||
}
|
||||
startRange.add(points[0].getX()).add(points[0].getY());
|
||||
endRange.add(points[1].getX()).add(points[1].getY());
|
||||
} else {
|
||||
//this is like a polygon, find the bounding box
|
||||
warnBoundingBoxApproximation("within a sequence of Points (polygon)");
|
||||
//find the lowest and highest X and Y to get the bounding box
|
||||
double xMin = Double.POSITIVE_INFINITY;
|
||||
double yMin = Double.POSITIVE_INFINITY;
|
||||
double xMax = Double.NEGATIVE_INFINITY;
|
||||
double yMax = Double.NEGATIVE_INFINITY;
|
||||
for (Point point : points) {
|
||||
xMin = point.getX() < xMin ? point.getX() : xMin;
|
||||
xMax = point.getX() > xMax ? point.getX() : xMax;
|
||||
|
||||
yMin = point.getY() < yMin ? point.getY() : yMin;
|
||||
yMax = point.getY() > yMax ? point.getY() : yMax;
|
||||
}
|
||||
|
||||
//once we have the coordinates of lower left and upper right points, use them
|
||||
startRange.add(xMin).add(yMin);
|
||||
endRange.add(xMax).add(yMax);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a {@link Shape} to a pair of {@link JsonArray} ranges, injected into startRange and endRange.
|
||||
*
|
||||
* @param startRange the startRange to populate with this shape's data.
|
||||
* @param endRange the endRange to populate with this shape's data.
|
||||
* @param shape the shape to extract ranges from.
|
||||
* @throws IllegalArgumentException if the {@link Shape} is unsupported.
|
||||
*/
|
||||
public static void convertShapeTo2DRanges(JsonArray startRange, JsonArray endRange, Shape shape) {
|
||||
if (shape instanceof Box) {
|
||||
Box box = (Box) shape;
|
||||
startRange //add minimum coordinates for x and y
|
||||
.add(box.getFirst().getX())
|
||||
.add(box.getFirst().getY());
|
||||
endRange //add maximum coordinates for x and y
|
||||
.add(box.getSecond().getX())
|
||||
.add(box.getSecond().getY());
|
||||
} else if (shape instanceof Polygon) {
|
||||
warnBoundingBoxApproximation("within a Polygon");
|
||||
//find the lowest and highest X and Y to get the bounding box
|
||||
double xMin = Double.POSITIVE_INFINITY;
|
||||
double yMin = Double.POSITIVE_INFINITY;
|
||||
double xMax = Double.NEGATIVE_INFINITY;
|
||||
double yMax = Double.NEGATIVE_INFINITY;
|
||||
for (Point point : (Polygon) shape) {
|
||||
xMin = point.getX() < xMin ? point.getX() : xMin;
|
||||
xMax = point.getX() > xMax ? point.getX() : xMax;
|
||||
|
||||
yMin = point.getY() < yMin ? point.getY() : yMin;
|
||||
yMax = point.getY() > yMax ? point.getY() : yMax;
|
||||
}
|
||||
|
||||
//once we have the coordinates of lower left and upper right points, use them
|
||||
startRange.add(xMin).add(yMin);
|
||||
endRange.add(xMax).add(yMax);
|
||||
} else if (shape instanceof Circle) {
|
||||
warnBoundingBoxApproximation("within a Circle");
|
||||
//here the bounding box is the box that contains the circle
|
||||
Circle circle = (Circle) shape;
|
||||
Point center = circle.getCenter();
|
||||
double radius = circle.getRadius().getNormalizedValue();
|
||||
|
||||
double xMin = center.getX() - radius;
|
||||
double xMax = center.getX() + radius;
|
||||
double yMin = center.getY() - radius;
|
||||
double yMax = center.getY() + radius;
|
||||
|
||||
startRange.add(xMin).add(yMin);
|
||||
endRange.add(xMax).add(yMax);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported shape " + shape.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
private static void warnBoundingBoxApproximation(String kind) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Spatial View queries " + kind + " are made using a bounding box approximation");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import org.springframework.data.couchbase.repository.config.RepositoryOperations
|
||||
import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation;
|
||||
import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod;
|
||||
import org.springframework.data.couchbase.repository.query.PartTreeN1qlBasedQuery;
|
||||
import org.springframework.data.couchbase.repository.query.SpatialViewBasedQuery;
|
||||
import org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery;
|
||||
import org.springframework.data.couchbase.repository.query.ViewBasedCouchbaseQuery;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
@@ -184,7 +185,9 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport {
|
||||
CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, metadata, mappingContext);
|
||||
String namedQueryName = queryMethod.getNamedQueryName();
|
||||
|
||||
if (queryMethod.hasViewAnnotation()) {
|
||||
if (queryMethod.hasDimensionalAnnotation()) {
|
||||
return new SpatialViewBasedQuery(queryMethod, couchbaseOperations);
|
||||
} else if (queryMethod.hasViewAnnotation()) {
|
||||
return new ViewBasedCouchbaseQuery(queryMethod, couchbaseOperations);
|
||||
} else if (queryMethod.hasN1qlAnnotation()) {
|
||||
if (queryMethod.hasInlineN1qlQuery()) {
|
||||
|
||||
Reference in New Issue
Block a user