Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.springframework.cloud.gcp.data.datastore.repository.query.GqlDatastoreQuery Maven / Gradle / Ivy
/*
* Copyright 2017-2019 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
*
* https://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.cloud.gcp.data.datastore.repository.query;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import com.google.cloud.datastore.BaseEntity;
import com.google.cloud.datastore.Cursor;
import com.google.cloud.datastore.GqlQuery;
import com.google.cloud.datastore.GqlQuery.Builder;
import com.google.cloud.datastore.Key;
import org.springframework.cloud.gcp.data.datastore.core.DatastoreOperations;
import org.springframework.cloud.gcp.data.datastore.core.DatastoreResultsIterable;
import org.springframework.cloud.gcp.data.datastore.core.convert.DatastoreNativeTypes;
import org.springframework.cloud.gcp.data.datastore.core.mapping.DatastoreDataException;
import org.springframework.cloud.gcp.data.datastore.core.mapping.DatastoreMappingContext;
import org.springframework.cloud.gcp.data.datastore.core.mapping.DatastorePersistentEntity;
import org.springframework.cloud.gcp.data.datastore.core.mapping.DiscriminatorField;
import org.springframework.cloud.gcp.data.datastore.core.util.ValueUtil;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.SpelEvaluator;
import org.springframework.data.repository.query.SpelQueryContext;
import org.springframework.util.StringUtils;
import static org.springframework.core.annotation.AnnotationUtils.getAnnotation;
/**
* Query Method for GQL queries.
* @param the return type of the Query Method
* @author Chengyuan Zhao
*
* @since 1.1
*/
public class GqlDatastoreQuery extends AbstractDatastoreQuery {
// A small string that isn't used in GQL syntax
private static final String ENTITY_CLASS_NAME_BOOKEND = "|";
private static final Pattern CLASS_NAME_PATTERN = Pattern.compile("\\" + ENTITY_CLASS_NAME_BOOKEND + "\\S+\\"
+ ENTITY_CLASS_NAME_BOOKEND + "");
private final String originalGql;
private String gqlResolvedEntityClassName;
private List originalParamTags;
private QueryMethodEvaluationContextProvider evaluationContextProvider;
private SpelQueryContext.EvaluatingSpelQueryContext evaluatingSpelQueryContext;
/**
* Constructor.
* @param type the underlying entity type
* @param queryMethod the underlying query method to support.
* @param datastoreTemplate used for executing queries.
* @param gql the query text.
* @param evaluationContextProvider the provider used to evaluate SpEL expressions in
* queries.
* @param datastoreMappingContext used for getting metadata about entities.
*/
public GqlDatastoreQuery(Class type, DatastoreQueryMethod queryMethod,
DatastoreOperations datastoreTemplate, String gql,
QueryMethodEvaluationContextProvider evaluationContextProvider,
DatastoreMappingContext datastoreMappingContext) {
super(queryMethod, datastoreTemplate, datastoreMappingContext, type);
this.evaluationContextProvider = evaluationContextProvider;
this.originalGql = StringUtils.trimTrailingCharacter(gql.trim(), ';');
setOriginalParamTags();
setEvaluatingSpelQueryContext();
setGqlResolvedEntityClassName();
}
private static Object getNonEntityObjectFromRow(Object x) {
Object mappedResult;
if (x instanceof Key) {
mappedResult = x;
}
else {
BaseEntity entity = (BaseEntity) x;
Set colNames = entity.getNames();
if (colNames.size() > 1) {
throw new DatastoreDataException(
"The query method returns non-entity types, but the query result has "
+ "more than one column. Use a Projection entity type instead.");
}
mappedResult = entity.getValue((String) colNames.toArray()[0]).get();
}
return mappedResult;
}
@Override
public Object execute(Object[] parameters) {
if (getAnnotation(this.entityType.getSuperclass(), DiscriminatorField.class) != null) {
throw new DatastoreDataException("Can't append discrimination condition");
}
ParsedQueryWithTagsAndValues parsedQueryWithTagsAndValues =
new ParsedQueryWithTagsAndValues(this.originalParamTags, parameters);
GqlQuery query = parsedQueryWithTagsAndValues.bindArgsToGqlQuery();
Class returnedItemType = this.queryMethod.getReturnedObjectType();
boolean isNonEntityReturnType = isNonEntityReturnedType(returnedItemType);
DatastoreResultsIterable found = isNonEntityReturnType
? this.datastoreOperations.queryIterable(query, GqlDatastoreQuery::getNonEntityObjectFromRow)
: this.datastoreOperations.queryKeysOrEntities(query, this.entityType);
Object result;
if (isPageQuery() || isSliceQuery()) {
result = buildPageOrSlice(parameters, parsedQueryWithTagsAndValues, found);
}
else if (this.queryMethod.isCollectionQuery()) {
result = convertCollectionResult(returnedItemType, found);
}
else {
result = convertSingularResult(returnedItemType, isNonEntityReturnType, found);
}
return result;
}
private Object buildPageOrSlice(Object[] parameters, ParsedQueryWithTagsAndValues parsedQueryWithTagsAndValues,
DatastoreResultsIterable found) {
Pageable pageableParam =
new ParametersParameterAccessor(getQueryMethod().getParameters(), parameters).getPageable();
List resultsList = found == null ? Collections.emptyList()
: (List) StreamSupport.stream(found.spliterator(), false).collect(Collectors.toList());
Cursor cursor = found != null ? found.getCursor() : null;
Slice result = isPageQuery()
? buildPage(pageableParam, parsedQueryWithTagsAndValues, cursor, resultsList)
: buildSlice(pageableParam, parsedQueryWithTagsAndValues, cursor, resultsList);
return processRawObjectForProjection(result);
}
private Slice buildSlice(Pageable pageableParam, ParsedQueryWithTagsAndValues parsedQueryWithTagsAndValues,
Cursor cursor, List resultsList) {
GqlQuery nextQuery = parsedQueryWithTagsAndValues.bindArgsToGqlQuery(cursor, 1);
DatastoreResultsIterable next = this.datastoreOperations.queryKeysOrEntities(nextQuery, this.entityType);
Pageable pageable = DatastorePageable.from(pageableParam, cursor, null);
return new SliceImpl(resultsList, pageable, next.iterator().hasNext());
}
private Page buildPage(Pageable pageableParam, ParsedQueryWithTagsAndValues parsedQueryWithTagsAndValues,
Cursor cursor, List resultsList) {
Long count = pageableParam instanceof DatastorePageable
? ((DatastorePageable) pageableParam).getTotalCount()
: null;
if (count == null) {
GqlQuery nextQuery = parsedQueryWithTagsAndValues.bindArgsToGqlQueryNoLimit();
DatastoreResultsIterable next = this.datastoreOperations.queryKeysOrEntities(nextQuery,
this.entityType);
count = StreamSupport.stream(next.spliterator(), false).count();
}
Pageable pageable = DatastorePageable.from(pageableParam, cursor, count);
return new PageImpl(resultsList, pageable, count);
}
private Object convertCollectionResult(Class returnedItemType, Iterable rawResult) {
Object result = this.datastoreOperations.getDatastoreEntityConverter()
.getConversions().convertOnRead(
rawResult, this.queryMethod.getCollectionReturnType(), returnedItemType);
return processRawObjectForProjection(result);
}
private Object convertSingularResult(Class returnedItemType,
boolean isNonEntityReturnType, Iterable rawResult) {
if (rawResult == null) {
return null;
}
Iterator iterator = rawResult.iterator();
if (this.queryMethod.isExistsQuery()) {
return iterator.hasNext();
}
if (this.queryMethod.isCountQuery()) {
return StreamSupport.stream(rawResult.spliterator(), false).count();
}
if (!iterator.hasNext()) {
return null;
}
Object result = iterator.next();
if (iterator.hasNext()) {
throw new DatastoreDataException(
"The query method returns a singular object but "
+ "the query returned more than one result.");
}
return isNonEntityReturnType
? this.datastoreOperations.getDatastoreEntityConverter().getConversions()
.convertOnRead(result, null, returnedItemType)
: this.queryMethod.getResultProcessor().processResult(result);
}
boolean isNonEntityReturnedType(Class returnedType) {
return this.datastoreOperations.getDatastoreEntityConverter().getConversions()
.getDatastoreCompatibleType(returnedType).isPresent();
}
private void setOriginalParamTags() {
this.originalParamTags = new ArrayList<>();
Set seen = new HashSet<>();
Parameters parameters = getQueryMethod().getParameters();
for (int i = 0; i < parameters.getNumberOfParameters(); i++) {
Parameter param = parameters.getParameter(i);
if (Pageable.class.isAssignableFrom(param.getType()) || Sort.class.isAssignableFrom(param.getType())) {
continue;
}
Optional paramName = param.getName();
if (!paramName.isPresent()) {
throw new DatastoreDataException(
"Query method has a parameter without a valid name: "
+ getQueryMethod().getName());
}
String name = paramName.get();
if (seen.contains(name)) {
throw new DatastoreDataException(
"More than one param has the same name: " + name);
}
seen.add(name);
this.originalParamTags.add(name);
}
}
private void setGqlResolvedEntityClassName() {
Matcher matcher = CLASS_NAME_PATTERN.matcher(GqlDatastoreQuery.this.originalGql);
String result = GqlDatastoreQuery.this.originalGql;
while (matcher.find()) {
String matched = matcher.group();
String className = matched.substring(1, matched.length() - 1);
try {
Class entityClass = Class.forName(className);
DatastorePersistentEntity datastorePersistentEntity = GqlDatastoreQuery.this.datastoreMappingContext
.getPersistentEntity(entityClass);
if (datastorePersistentEntity == null) {
throw new DatastoreDataException(
"The class used in the GQL statement is not a Cloud Datastore persistent entity: "
+ className);
}
result = result.replace(matched, datastorePersistentEntity.kindName());
}
catch (ClassNotFoundException ex) {
throw new DatastoreDataException(
"The class name does not refer to an available entity type: "
+ className);
}
}
this.gqlResolvedEntityClassName = result;
}
private void setEvaluatingSpelQueryContext() {
Set originalTags = new HashSet<>(GqlDatastoreQuery.this.originalParamTags);
GqlDatastoreQuery.this.evaluatingSpelQueryContext = SpelQueryContext.EvaluatingSpelQueryContext
.of((counter, spelExpression) -> {
String newTag;
do {
counter++;
newTag = "@SpELtag" + counter;
}
while (originalTags.contains(newTag));
originalTags.add(newTag);
return newTag;
}, (prefix, newTag) -> newTag)
.withEvaluationContextProvider(GqlDatastoreQuery.this.evaluationContextProvider);
}
// Convenience class to hold a grouping of GQL, tags, and parameter values.
private class ParsedQueryWithTagsAndValues {
static final String LIMIT_CLAUSE = " LIMIT @limit";
static final String LIMIT_TAG_NAME = "limit";
static final String OFFSET_CLAUSE = " OFFSET @offset";
static final String OFFSET_TAG_NAME = "offset";
static final String ORDER_BY = " ORDER BY ";
List tagsOrdered;
final Object[] rawParams;
List params;
private final String noLimitQuery;
String finalGql;
int cursorPosition;
int limitPosition;
ParsedQueryWithTagsAndValues(List initialTags, Object[] rawParams) {
this.params = Arrays.stream(rawParams).filter(e -> !(e instanceof Pageable || e instanceof Sort))
.collect(Collectors.toList());
this.rawParams = rawParams;
this.tagsOrdered = new ArrayList<>(initialTags);
SpelEvaluator spelEvaluator = GqlDatastoreQuery.this.evaluatingSpelQueryContext.parse(
GqlDatastoreQuery.this.gqlResolvedEntityClassName,
GqlDatastoreQuery.this.queryMethod.getParameters());
Map results = spelEvaluator.evaluate(this.rawParams);
this.finalGql = spelEvaluator.getQueryString();
for (Map.Entry entry : results.entrySet()) {
this.params.add(entry.getValue());
// Cloud Datastore requires the tag name without the @
this.tagsOrdered.add(entry.getKey().substring(1));
}
ParameterAccessor paramAccessor =
new ParametersParameterAccessor(getQueryMethod().getParameters(), rawParams);
Sort sort = paramAccessor.getSort();
this.finalGql = addSort(this.finalGql, sort);
this.noLimitQuery = this.finalGql;
Pageable pageable = paramAccessor.getPageable();
if (!pageable.equals(Pageable.unpaged())) {
this.finalGql += LIMIT_CLAUSE;
this.tagsOrdered.add(LIMIT_TAG_NAME);
this.limitPosition = this.params.size();
this.params.add(pageable.getPageSize());
this.finalGql += OFFSET_CLAUSE;
this.tagsOrdered.add(OFFSET_TAG_NAME);
this.cursorPosition = this.params.size();
if (pageable instanceof DatastorePageable && ((DatastorePageable) pageable).toCursor() != null) {
this.params.add(((DatastorePageable) pageable).toCursor());
}
else {
this.params.add(pageable.getOffset());
}
}
}
private GqlQuery bindArgsToGqlQuery(Cursor newCursor, int newLimit) {
this.params.set(this.cursorPosition, newCursor);
this.params.set(this.limitPosition, newLimit);
return bindArgsToGqlQuery();
}
private GqlQuery bindArgsToGqlQueryNoLimit() {
this.finalGql = this.noLimitQuery;
this.tagsOrdered = this.tagsOrdered.subList(0, this.limitPosition);
this.params = this.params.subList(0, this.limitPosition);
return bindArgsToGqlQuery();
}
private GqlQuery bindArgsToGqlQuery() {
Builder builder = GqlQuery.newGqlQueryBuilder(this.finalGql);
builder.setAllowLiteral(true);
if (this.tagsOrdered.size() != this.params.size()) {
throw new DatastoreDataException("Annotated GQL Query Method "
+ GqlDatastoreQuery.this.queryMethod.getName() + " has " + this.tagsOrdered.size()
+ " tags but a different number of parameter values: " + this.params.size());
}
for (int i = 0; i < this.tagsOrdered.size(); i++) {
Object val = this.params.get(i);
Object boundVal;
if (val instanceof Cursor) {
boundVal = val;
}
else if (ValueUtil.isCollectionLike(val.getClass())) {
boundVal = convertCollectionParamToCompatibleArray((List) ValueUtil.toListIfArray(val));
}
else {
boundVal = GqlDatastoreQuery.this.datastoreOperations.getDatastoreEntityConverter().getConversions()
.convertOnWriteSingle(convertEntitiesToKeys(val)).get();
}
DatastoreNativeTypes.bindValueToGqlBuilder(builder, this.tagsOrdered.get(i), boundVal);
}
return builder.build();
}
private String addSort(String finalGql, Sort sort) {
if (sort.equals(Sort.unsorted())) {
return finalGql;
}
//similar to Spring Data JPA, we don't map passed sort properties to persistent properties names
// in @Query annotated methods
String orderString = sort.stream().map(order -> order.getProperty() + " " + order.getDirection())
.collect(Collectors.joining(", "));
return finalGql + ORDER_BY + orderString;
}
private Object convertEntitiesToKeys(Object o) {
if (GqlDatastoreQuery.this.datastoreMappingContext.hasPersistentEntityFor(o.getClass())) {
return GqlDatastoreQuery.this.datastoreOperations.getKey(o);
}
return o;
}
}
}