com.arangodb.springframework.repository.query.ArangoAqlQuery Maven / Gradle / Ivy
/*
* DISCLAIMER
*
* Copyright 2017 ArangoDB GmbH, Cologne, Germany
*
* 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.
*
* Copyright holder is ArangoDB GmbH, Cologne, Germany
*/
package com.arangodb.springframework.repository.query;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.geo.GeoPage;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.util.Assert;
import com.arangodb.ArangoCursor;
import com.arangodb.entity.BaseDocument;
import com.arangodb.entity.BaseEdgeDocument;
import com.arangodb.entity.IndexType;
import com.arangodb.model.AqlQueryOptions;
import com.arangodb.springframework.annotation.BindVars;
import com.arangodb.springframework.annotation.Param;
import com.arangodb.springframework.annotation.Query;
import com.arangodb.springframework.annotation.QueryOptions;
import com.arangodb.springframework.core.ArangoOperations;
import com.arangodb.springframework.core.mapping.ArangoMappingContext;
import com.arangodb.springframework.repository.query.derived.DerivedQueryCreator;
import com.arangodb.velocypack.VPackSlice;
/**
* Implements execute(Object[]) method which is called every time a user-defined AQL or derived method is called
*/
public class ArangoAqlQuery implements RepositoryQuery {
private static final Set> GEO_RETURN_TYPES = new HashSet<>();
private static final Set> DESERIALIZABLE_TYPES = new HashSet<>();
static {
GEO_RETURN_TYPES.add(GeoResult.class);
GEO_RETURN_TYPES.add(GeoResults.class);
GEO_RETURN_TYPES.add(GeoPage.class);
DESERIALIZABLE_TYPES.add(Map.class);
DESERIALIZABLE_TYPES.add(BaseDocument.class);
DESERIALIZABLE_TYPES.add(BaseEdgeDocument.class);
DESERIALIZABLE_TYPES.add(String.class);
DESERIALIZABLE_TYPES.add(Boolean.class);
DESERIALIZABLE_TYPES.add(boolean.class);
DESERIALIZABLE_TYPES.add(Integer.class);
DESERIALIZABLE_TYPES.add(int.class);
DESERIALIZABLE_TYPES.add(Long.class);
DESERIALIZABLE_TYPES.add(long.class);
DESERIALIZABLE_TYPES.add(Short.class);
DESERIALIZABLE_TYPES.add(short.class);
DESERIALIZABLE_TYPES.add(Double.class);
DESERIALIZABLE_TYPES.add(double.class);
DESERIALIZABLE_TYPES.add(Float.class);
DESERIALIZABLE_TYPES.add(float.class);
DESERIALIZABLE_TYPES.add(BigInteger.class);
DESERIALIZABLE_TYPES.add(BigDecimal.class);
DESERIALIZABLE_TYPES.add(Number.class);
DESERIALIZABLE_TYPES.add(Character.class);
DESERIALIZABLE_TYPES.add(char.class);
DESERIALIZABLE_TYPES.add(Date.class);
DESERIALIZABLE_TYPES.add(java.sql.Date.class);
DESERIALIZABLE_TYPES.add(java.sql.Timestamp.class);
DESERIALIZABLE_TYPES.add(VPackSlice.class);
DESERIALIZABLE_TYPES.add(UUID.class);
DESERIALIZABLE_TYPES.add(byte[].class);
DESERIALIZABLE_TYPES.add(Byte.class);
DESERIALIZABLE_TYPES.add(byte.class);
DESERIALIZABLE_TYPES.add(Enum.class);
DESERIALIZABLE_TYPES.add(Instant.class);
DESERIALIZABLE_TYPES.add(LocalDate.class);
DESERIALIZABLE_TYPES.add(LocalDateTime.class);
DESERIALIZABLE_TYPES.add(OffsetDateTime.class);
DESERIALIZABLE_TYPES.add(ZonedDateTime.class);
}
private static final Logger LOGGER = LoggerFactory.getLogger(ArangoAqlQuery.class);
private final ArangoOperations operations;
private final Class> domainClass;
private final Method method;
private final RepositoryMetadata metadata;
private ArangoParameterAccessor accessor;
private boolean isCountProjection = false;
private boolean isExistsProjection = false;
private final ProjectionFactory factory;
public ArangoAqlQuery(final Class> domainClass, final Method method, final RepositoryMetadata metadata,
final ArangoOperations operations, final ProjectionFactory factory) {
this.domainClass = domainClass;
this.method = method;
this.metadata = metadata;
this.operations = operations;
this.factory = factory;
}
@Override
public QueryMethod getQueryMethod() {
return new ArangoQueryMethod(method, metadata, factory);
}
/**
* This method contains main logic showing how all user-defined methods are implemented
*
* @param arguments
* @return
*/
@SuppressWarnings("unchecked")
@Override
public Object execute(final Object[] arguments) {
Map bindVars = new HashMap<>();
String query = getQueryAnnotationValue();
AqlQueryOptions options = getAqlQueryOptions();
boolean optionsFound = false;
if (query == null) { // derived method
final PartTree tree = new PartTree(method.getName(), domainClass);
isCountProjection = tree.isCountProjection();
isExistsProjection = tree.isExistsProjection();
accessor = new ArangoParameterAccessor(new ArangoParameters(method), arguments);
options = updateAqlQueryOptions(options, accessor.getAqlQueryOptions());
if (Page.class.isAssignableFrom(method.getReturnType())) {
if (options == null) {
options = new AqlQueryOptions().fullCount(true);
} else {
options = options.fullCount(true);
}
}
final List geoFields = new LinkedList<>();
if (GEO_RETURN_TYPES.contains(method.getReturnType())) {
operations.collection(
operations.getConverter().getMappingContext().getPersistentEntity(domainClass).getCollection())
.getIndexes().forEach(i -> {
if ((i.getType() == IndexType.geo1) && geoFields.isEmpty()) {
i.getFields().forEach(f -> geoFields.add(f));
}
});
}
query = new DerivedQueryCreator((ArangoMappingContext) operations.getConverter().getMappingContext(),
domainClass, tree, accessor, bindVars, geoFields,
operations.getVersion().getVersion().compareTo("3.2.0") < 0).createQuery();
} else if (arguments != null) { // AQL query method
final Set bindings = getBindings(query);
final Annotation[][] annotations = method.getParameterAnnotations();
Assert.isTrue(arguments.length == annotations.length, "arguments.length != annotations.length");
final Map bindVarsLocal = new HashMap<>();
boolean bindVarsFound = false;
for (int i = 0; i < arguments.length; ++i) {
if (arguments[i] instanceof AqlQueryOptions) {
Assert.isTrue(!optionsFound, "AqlQueryOptions are already set");
optionsFound = true;
options = updateAqlQueryOptions(options, (AqlQueryOptions) arguments[i]);
continue;
}
String parameter = null;
final Annotation specialAnnotation = getSpecialAnnotation(annotations[i]);
if (specialAnnotation != null) {
if (specialAnnotation.annotationType() == Param.class) {
parameter = ((Param) specialAnnotation).value();
} else if (specialAnnotation.annotationType() == BindVars.class) {
Assert.isTrue(arguments[i] instanceof Map, "@BindVars must be a Map");
Assert.isTrue(!bindVarsFound, "@BindVars duplicated");
bindVars = (Map) arguments[i];
bindVarsFound = true;
continue;
}
}
if (parameter == null) {
final String key = String.format("%d", i);
if (bindings.contains(key)) {
Assert.isTrue(!bindVarsLocal.containsKey(key), "duplicate parameter name");
bindVarsLocal.put(key, arguments[i]);
} else if (bindings.contains("@" + key)) {
Assert.isTrue(!bindVarsLocal.containsKey("@" + key), "duplicate parameter name");
bindVarsLocal.put("@" + key, arguments[i]);
} else {
LOGGER.debug("Local parameter '@{}' is not used in the query", key);
}
} else {
Assert.isTrue(!bindVarsLocal.containsKey(parameter), "duplicate parameter name");
bindVarsLocal.put(parameter, arguments[i]);
}
}
mergeBindVars(bindVars, bindVarsLocal);
}
return convertResult(operations.query(query, bindVars, options, getResultClass()));
}
/**
* Merges AqlQueryOptions derived from @QueryOptions with dynamically passed AqlQueryOptions which takes priority
*
* @param oldStatic
* @param newDynamic
* @return
*/
private AqlQueryOptions updateAqlQueryOptions(final AqlQueryOptions oldStatic, final AqlQueryOptions newDynamic) {
if (oldStatic == null) {
return newDynamic;
}
final Integer batchSize = newDynamic.getBatchSize();
if (batchSize != null) {
oldStatic.batchSize(batchSize);
}
final Integer maxPlans = newDynamic.getMaxPlans();
if (maxPlans != null) {
oldStatic.maxPlans(maxPlans);
}
final Integer ttl = newDynamic.getTtl();
if (ttl != null) {
oldStatic.ttl(ttl);
}
final Boolean cache = newDynamic.getCache();
if (cache != null) {
oldStatic.cache(cache);
}
final Boolean count = newDynamic.getCount();
if (count != null) {
oldStatic.count(count);
}
final Boolean fullCount = newDynamic.getFullCount();
if (fullCount != null) {
oldStatic.fullCount(fullCount);
}
final Boolean profile = newDynamic.getProfile();
if (profile != null) {
oldStatic.profile(profile);
}
final Collection rules = newDynamic.getRules();
if (rules != null) {
oldStatic.rules(rules);
}
return oldStatic;
}
private AqlQueryOptions getAqlQueryOptions() {
final QueryOptions queryOptions = method.getAnnotation(QueryOptions.class);
if (queryOptions == null) {
return null;
}
final AqlQueryOptions options = new AqlQueryOptions();
final int batchSize = queryOptions.batchSize();
if (batchSize != -1) {
options.batchSize(batchSize);
}
final int maxPlans = queryOptions.maxPlans();
if (maxPlans != -1) {
options.maxPlans(maxPlans);
}
final int ttl = queryOptions.ttl();
if (ttl != -1) {
options.ttl(ttl);
}
options.cache(queryOptions.cache());
options.count(queryOptions.count());
options.fullCount(queryOptions.fullCount());
options.profile(queryOptions.profile());
options.rules(Arrays.asList(queryOptions.rules()));
return options;
}
private Class> getResultClass() {
if (isCountProjection || isExistsProjection) {
return Integer.class;
}
if (GEO_RETURN_TYPES.contains(method.getReturnType())) {
return Object.class;
}
if (DESERIALIZABLE_TYPES.contains(method.getReturnType())) {
return method.getReturnType();
}
return domainClass;
}
/**
* Returns Param or BindVars Annotation if it is present in the given array or null otherwise
*
* @param annotations
* @return
*/
private Annotation getSpecialAnnotation(final Annotation[] annotations) {
Annotation specialAnnotation = null;
for (final Annotation annotation : annotations) {
if (annotation.annotationType() == BindVars.class || annotation.annotationType() == Param.class) {
Assert.isTrue(specialAnnotation == null, "@BindVars or @Param should be used only once per parameter");
specialAnnotation = annotation;
}
}
return specialAnnotation;
}
private String getQueryAnnotationValue() {
final Query query = method.getAnnotation(Query.class);
return query == null ? null : query.value();
}
/**
* Merges bindVars Map passed by a user with a Map created from the rest of the arguments which take priority
*
* @param bindVars
* @param bindVarsLocal
*/
private void mergeBindVars(final Map bindVars, final Map bindVarsLocal) {
for (final String key : bindVarsLocal.keySet()) {
if (bindVars.containsKey(key)) {
LOGGER.debug("Local parameter '{}' overrides @BindVars Map", key);
}
bindVars.put(key, bindVarsLocal.get(key));
}
}
private Object convertResult(final ArangoCursor> result) {
if (isExistsProjection) {
if (!result.hasNext()) {
return false;
}
return Integer.valueOf(result.next().toString()) > 0;
}
final ArangoResultConverter resultConverter = new ArangoResultConverter(accessor, result, operations,
domainClass);
return resultConverter.convertResult(method.getReturnType());
}
private String removeAqlStringLiterals(final String query) {
final StringBuilder fixedQuery = new StringBuilder();
for (int i = 0; i < query.length(); ++i) {
if (query.charAt(i) == '"') {
for (++i; i < query.length(); ++i) {
if (query.charAt(i) == '"') {
++i;
break;
}
if (query.charAt(i) == '\\') {
++i;
}
}
} else if (query.charAt(i) == '\'') {
for (++i; i < query.length(); ++i) {
if (query.charAt(i) == '\'') {
++i;
break;
}
if (query.charAt(i) == '\\') {
++i;
}
}
}
fixedQuery.append(query.charAt(i));
}
return fixedQuery.toString();
}
/**
* Returns all bindings used in AQL query String including bindings prefixed with both single and double '@'
* character ignoring AQL string literals
*
* @param query
* @return
*/
private Set getBindings(final String query) {
final String fixedQuery = removeAqlStringLiterals(query);
final Set bindings = new HashSet<>();
final Matcher matcher = Pattern.compile("@\\S+").matcher(fixedQuery);
while (matcher.find()) {
bindings.add(matcher.group().substring(1));
}
return bindings;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy