SGF-713 - Override generated OQL from Repository query methods.

This commit is contained in:
John Blum
2018-02-16 16:08:40 -08:00
parent 96037dff60
commit e30b3df921
35 changed files with 2741 additions and 882 deletions

View File

@@ -28,6 +28,7 @@ import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.NonNull;
import org.springframework.util.StringUtils;
/**
@@ -67,9 +68,10 @@ public class GemfirePersistentEntity<T> extends BasicPersistentEntity<T, Gemfire
protected static String resolveRegionName(Class<?> persistentEntityType, Annotation regionAnnotation) {
Optional<String> regionName = Optional.ofNullable(regionAnnotation)
.map((annotation) -> getAnnotationAttributeStringValue(annotation, "value"));
.map(annotation -> getAnnotationAttributeStringValue(annotation, "value"))
.filter(StringUtils::hasText);
return regionName.filter(StringUtils::hasText).orElse(persistentEntityType.getSimpleName());
return regionName.orElse(persistentEntityType.getSimpleName());
}
/* (non-Javadoc) */
@@ -131,13 +133,14 @@ public class GemfirePersistentEntity<T> extends BasicPersistentEntity<T, Gemfire
}
/**
* Returns the name of the {@link org.apache.geode.cache.Region} in which this {@link PersistentEntity}
* will be stored.
* Returns the {@link String name} of the {@link org.apache.geode.cache.Region}
* in which this {@link PersistentEntity} will be stored.
*
* @return the name of the {@link org.apache.geode.cache.Region} in which this {@link PersistentEntity}
* will be stored.
* @return the {@link String name} of the {@link org.apache.geode.cache.Region}
* in which this {@link PersistentEntity} will be stored.
* @see org.apache.geode.cache.Region#getName()
*/
@NonNull
public String getRegionName() {
return this.regionName;
}
@@ -166,7 +169,7 @@ public class GemfirePersistentEntity<T> extends BasicPersistentEntity<T, Gemfire
return null;
}
return (property.isExplicitIdProperty() ? property : null);
return property.isExplicitIdProperty() ? property : null;
}
else {
return property;

View File

@@ -18,10 +18,13 @@ package org.springframework.data.gemfire.mapping;
import static org.springframework.data.gemfire.util.CollectionUtils.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Set;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
@@ -66,11 +69,13 @@ public class GemfirePersistentProperty extends AnnotationBasedPersistentProperty
}
/**
* Determines whether this {@link GemfirePersistentProperty} explicitly identifies an entity property identifier,
* one in which the user explicitly annotated a entity class member (field or getter/setter).
* Determines whether this {@link GemfirePersistentProperty} explicitly identifies
* an {@link GemfirePersistentEntity entity} identifier, one in which the user explicitly annotated
* the {@link GemfirePersistentEntity owning entity} class member ({@link Field} or property,
* i.e. {@link Method getter/setter}).
*
* @return a boolean value indicating whether this {@link GemfirePersistentProperty} explicitly identifies
* an entity property identifier.
* an {@link GemfirePersistentEntity entity} identifier.
* @see org.springframework.data.annotation.Id
* @see #isAnnotationPresent(Class)
*/
@@ -84,15 +89,37 @@ public class GemfirePersistentProperty extends AnnotationBasedPersistentProperty
*/
@Override
public boolean isIdProperty() {
return (super.isIdProperty() || SUPPORTED_IDENTIFIER_NAMES.contains(getName()));
return super.isIdProperty() || SUPPORTED_IDENTIFIER_NAMES.contains(getName());
}
/**
* Determines whether this {@link GemfirePersistentProperty persistent property} is {@literal transient}
* and thus impervious to persistent operations.
*
* A {@link GemfirePersistentProperty persistent property} is considered {@literal transient}
* if the {@link GemfirePersistentEntity owning entity's} field/property is annotated with
* {@link Transient} or the field/property is modified with {@link Modifier#TRANSIENT transient}.
*
* @return a boolean value indicating whether this {@link GemfirePersistentProperty persistent property}
* is {@literal transient} and thus impervious to persistent operations.
*/
@Override
public boolean isTransient() {
return super.isTransient()
|| getProperty().getField().filter(field -> Modifier.isTransient(field.getModifiers())).isPresent();
}
/**
* Returns the {@link String name} of this {@link GemfirePersistentProperty's} {@link Class type}.
*
* @return the {@link String name} of this {@link GemfirePersistentProperty's} {@link Class type}.
* @see java.lang.Class#getName()
* @see #getType()
*/
public String getTypeName() {
return getType().getName();
}
/**
* @inheritDoc
*/

View File

@@ -77,7 +77,8 @@ public class Regions implements Iterable<Region<?, ?>> {
Assert.notNull(entityType, "Entity type must not be null");
String regionName = Optional.ofNullable(this.mappingContext.getPersistentEntity(entityType))
.map(entity -> entity.getRegionName()).orElseGet(entityType::getSimpleName);
.map(entity -> entity.getRegionName())
.orElseGet(entityType::getSimpleName);
return (Region<?, T>) this.regions.get(regionName);
}

View File

@@ -44,8 +44,8 @@ import org.springframework.core.annotation.AliasFor;
public @interface Region {
@SuppressWarnings("unchecked")
List<Class<? extends Annotation>> REGION_ANNOTATION_TYPES = Arrays.asList(
ClientRegion.class, LocalRegion.class, PartitionRegion.class, ReplicateRegion.class, Region.class);
List<Class<? extends Annotation>> REGION_ANNOTATION_TYPES =
Arrays.asList(ClientRegion.class, LocalRegion.class, PartitionRegion.class, ReplicateRegion.class, Region.class);
/**
* Name, or fully-qualified bean name of the {@link org.apache.geode.cache.Region}

View File

@@ -21,13 +21,16 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.data.annotation.QueryAnnotation;
/**
*
* @author Oliver Gierke
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@QueryAnnotation
@Documented
public @interface Query {
String value() default "";

View File

@@ -27,6 +27,8 @@ import org.springframework.data.gemfire.mapping.GemfireMappingContext;
import org.springframework.data.gemfire.mapping.annotation.Region;
import org.springframework.data.gemfire.repository.GemfireRepository;
import org.springframework.data.gemfire.repository.support.GemfireRepositoryFactoryBean;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource;
import org.springframework.data.repository.config.RepositoryConfigurationExtension;
import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
@@ -34,12 +36,13 @@ import org.springframework.data.repository.config.RepositoryConfigurationSource;
import org.springframework.data.repository.config.XmlRepositoryConfigurationSource;
/**
* {@link RepositoryConfigurationExtension} implementation handling GemFire specific extensions to the Repository XML
* namespace and annotation-based configuration meta-data.
* {@link RepositoryConfigurationExtension} implementation handling Apache Geode and Pivotal GemFire specific extensions
* in the Repository XML namespace and Annotation-based configuration meta-data.
*
* @author Oliver Gierke
* @author John Blum
* @see org.springframework.data.repository.config.RepositoryConfigurationExtension
* @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport
*/
public class GemfireRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport {
@@ -47,8 +50,8 @@ public class GemfireRepositoryConfigurationExtension extends RepositoryConfigura
private static final String MAPPING_CONTEXT_PROPERTY_NAME = "gemfireMappingContext";
private static final String MAPPING_CONTEXT_REF_ATTRIBUTE_NAME = "mappingContextRef";
static final String DEFAULT_MAPPING_CONTEXT_BEAN_NAME = String.format("%1$s.%2$s",
GemfireMappingContext.class.getName(), "DEFAULT");
static final String DEFAULT_MAPPING_CONTEXT_BEAN_NAME =
String.format("%1$s.%2$s", GemfireMappingContext.class.getName(), "DEFAULT");
/*
* (non-Javadoc)
@@ -92,9 +95,7 @@ public class GemfireRepositoryConfigurationExtension extends RepositoryConfigura
*/
@Override
public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource configurationSource) {
builder.addPropertyReference(MAPPING_CONTEXT_PROPERTY_NAME,
configurationSource.getAttribute(MAPPING_CONTEXT_REF_ATTRIBUTE_NAME)
.orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME));
addMappingContextPropertyReference(builder, configurationSource);
}
/*
@@ -103,6 +104,22 @@ public class GemfireRepositoryConfigurationExtension extends RepositoryConfigura
*/
@Override
public void postProcess(BeanDefinitionBuilder builder, XmlRepositoryConfigurationSource configurationSource) {
addMappingContextPropertyReference(builder, configurationSource);
}
/**
* Adds a property reference to the store-specific {@link MappingContext}
* in the given {@link BeanDefinitionBuilder bean definition}.
*
* @param builder {@link BeanDefinitionBuilder} used to build the target bean definition.
* @param configurationSource {@link RepositoryConfigurationSource} containing {@link Repository}
* configuration meta-data.
* @see org.springframework.beans.factory.support.BeanDefinitionBuilder
* @see org.springframework.data.repository.config.RepositoryConfigurationSource
*/
private void addMappingContextPropertyReference(BeanDefinitionBuilder builder,
RepositoryConfigurationSource configurationSource) {
builder.addPropertyReference(MAPPING_CONTEXT_PROPERTY_NAME,
configurationSource.getAttribute(MAPPING_CONTEXT_REF_ATTRIBUTE_NAME)
.orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME));
@@ -117,9 +134,21 @@ public class GemfireRepositoryConfigurationExtension extends RepositoryConfigura
super.registerBeansForRoot(registry, configurationSource);
if (!configurationSource.getAttribute(MAPPING_CONTEXT_REF_ATTRIBUTE_NAME).isPresent()) {
if (noMappingContextIsConfigured(configurationSource)) {
registry.registerBeanDefinition(DEFAULT_MAPPING_CONTEXT_BEAN_NAME,
new RootBeanDefinition(GemfireMappingContext.class));
}
}
/**
* Determines whether a {@link GemfireMappingContext mapping context} has already been configured.
*
* @param configurationSource {@link RepositoryConfigurationSource} used to check for the presence
* of an existing {@link MappingContext} configuration.
* @return a boolean value indicating whether a {@link GemfireMappingContext mapping context}
* has already been configured.
*/
private boolean noMappingContextIsConfigured(RepositoryConfigurationSource configurationSource) {
return !configurationSource.getAttribute(MAPPING_CONTEXT_REF_ATTRIBUTE_NAME).isPresent();
}
}

View File

@@ -27,10 +27,14 @@ import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;
/**
* Query creator to create {@link QueryString} instances.
* {@link AbstractQueryCreator} to create {@link QueryString} instances.
*
* @author Oliver Gierke
* @author John Blum
* @see org.springframework.data.gemfire.repository.query.QueryBuilder
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator
* @see org.springframework.data.repository.query.parser.Part
* @see org.springframework.data.repository.query.parser.PartTree
*/
class GemfireQueryCreator extends AbstractQueryCreator<QueryString, Predicates> {
@@ -47,6 +51,7 @@ class GemfireQueryCreator extends AbstractQueryCreator<QueryString, Predicates>
* @param entity must not be {@literal null}.
*/
public GemfireQueryCreator(PartTree tree, GemfirePersistentEntity<?> entity) {
super(tree);
this.queryBuilder = new QueryBuilder(entity, tree);
@@ -59,7 +64,9 @@ class GemfireQueryCreator extends AbstractQueryCreator<QueryString, Predicates>
*/
@Override
public QueryString createQuery(Sort dynamicSort) {
this.indexes = new IndexProvider();
return super.createQuery(dynamicSort);
}
@@ -96,7 +103,8 @@ class GemfireQueryCreator extends AbstractQueryCreator<QueryString, Predicates>
*/
@Override
protected QueryString complete(Predicates criteria, Sort sort) {
QueryString query = queryBuilder.create(criteria).orderBy(sort);
QueryString query = this.queryBuilder.create(criteria).orderBy(sort);
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Created Query [%s]", query.toString()));

View File

@@ -67,7 +67,6 @@ public class GemfireQueryMethod extends QueryMethod {
assertNonPagingQueryMethod(method);
this.method = method;
this.entity = mappingContext.getPersistentEntity(getDomainClass());
}
@@ -81,6 +80,7 @@ public class GemfireQueryMethod extends QueryMethod {
* @see java.lang.reflect.Method#getParameterTypes()
*/
private void assertNonPagingQueryMethod(Method method) {
for (Class<?> type : method.getParameterTypes()) {
if (Pageable.class.isAssignableFrom(type)) {
throw new IllegalStateException(String.format("Pagination is not supported by GemFire Repositories;"
@@ -95,7 +95,7 @@ public class GemfireQueryMethod extends QueryMethod {
* @return the {@link GemfirePersistentEntity} the method deals with.
*/
public GemfirePersistentEntity<?> getPersistentEntity() {
return entity;
return this.entity;
}
/**
@@ -117,9 +117,12 @@ public class GemfireQueryMethod extends QueryMethod {
* @see java.lang.reflect.Method#getAnnotation(Class)
*/
String getAnnotatedQuery() {
Query query = method.getAnnotation(Query.class);
String queryString = (query != null ? (String) AnnotationUtils.getValue(query) : null);
return (StringUtils.hasText(queryString) ? queryString : null);
Query query = this.method.getAnnotation(Query.class);
String queryString = query != null ? (String) AnnotationUtils.getValue(query) : null;
return StringUtils.hasText(queryString) ? queryString : null;
}
/**
@@ -131,7 +134,7 @@ public class GemfireQueryMethod extends QueryMethod {
* @see java.lang.reflect.Method#isAnnotationPresent(Class)
*/
public boolean hasHint() {
return method.isAnnotationPresent(Hint.class);
return this.method.isAnnotationPresent(Hint.class);
}
/**
@@ -142,8 +145,10 @@ public class GemfireQueryMethod extends QueryMethod {
* @see java.lang.reflect.Method#getAnnotation(Class)
*/
public String[] getHints() {
Hint hint = method.getAnnotation(Hint.class);
return (hint != null ? hint.value() : EMPTY_STRING_ARRAY);
return hint != null ? hint.value() : EMPTY_STRING_ARRAY;
}
/**
@@ -155,7 +160,7 @@ public class GemfireQueryMethod extends QueryMethod {
* @see java.lang.reflect.Method#isAnnotationPresent(Class)
*/
public boolean hasImport() {
return method.isAnnotationPresent(Import.class);
return this.method.isAnnotationPresent(Import.class);
}
/**
@@ -166,8 +171,10 @@ public class GemfireQueryMethod extends QueryMethod {
* @see java.lang.reflect.Method#getAnnotation(Class)
*/
public String getImport() {
Import importStatement = method.getAnnotation(Import.class);
return (importStatement != null ? importStatement.value() : null);
return importStatement != null ? importStatement.value() : null;
}
/**
@@ -179,7 +186,7 @@ public class GemfireQueryMethod extends QueryMethod {
* @see java.lang.reflect.Method#isAnnotationPresent(Class)
*/
public boolean hasLimit() {
return method.isAnnotationPresent(Limit.class);
return this.method.isAnnotationPresent(Limit.class);
}
/**
@@ -190,8 +197,10 @@ public class GemfireQueryMethod extends QueryMethod {
* @see java.lang.reflect.Method#getAnnotation(Class)
*/
public int getLimit() {
Limit limit = method.getAnnotation(Limit.class);
return (limit != null ? limit.value() : Integer.MAX_VALUE);
return limit != null ? limit.value() : Integer.MAX_VALUE;
}
/**
@@ -202,7 +211,6 @@ public class GemfireQueryMethod extends QueryMethod {
* @see java.lang.reflect.Method#isAnnotationPresent(Class)
*/
public boolean hasTrace() {
return method.isAnnotationPresent(Trace.class);
return this.method.isAnnotationPresent(Trace.class);
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.gemfire.repository.query;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.util.Assert;
@@ -28,29 +29,37 @@ import org.springframework.util.Assert;
* @see org.springframework.data.gemfire.repository.query.GemfireQueryMethod
* @see org.springframework.data.repository.query.RepositoryQuery
*/
abstract class GemfireRepositoryQuery implements RepositoryQuery {
public abstract class GemfireRepositoryQuery implements RepositoryQuery {
private final GemfireQueryMethod queryMethod;
private QueryPostProcessor<?, String> queryPostProcessor = ProvidedQueryPostProcessor.IDENTITY;
/*
* (non-Javadoc)
* Constructor used for testing purposes only!
*/
GemfireRepositoryQuery() {
queryMethod = null;
this.queryMethod = null;
}
/**
* Creates a new {@link GemfireRepositoryQuery} using the given {@link GemfireQueryMethod}.
*
* @param queryMethod must not be {@literal null}.
* Constructs a new instance of {@link GemfireRepositoryQuery} initialized with
* the given {@link GemfireQueryMethod}.
*
* @param queryMethod {@link GemfireQueryMethod} capturing the meta-data
* for the {@link Repository} {@link QueryMethod}; must not be {@literal null}.
* @throws IllegalArgumentException if {@link GemfireQueryMethod query method} is {@literal null}.
* @see org.springframework.data.gemfire.repository.query.GemfireQueryMethod
*/
public GemfireRepositoryQuery(GemfireQueryMethod queryMethod) {
Assert.notNull(queryMethod);
Assert.notNull(queryMethod, "QueryMethod must not be null");
this.queryMethod = queryMethod;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.RepositoryQuery#getQueryMethod()
*/
@@ -59,4 +68,38 @@ abstract class GemfireRepositoryQuery implements RepositoryQuery {
return this.queryMethod;
}
/**
* Returns a reference to the composed {@link QueryPostProcessor QueryPostProcessors}, which gets applied
* to the OQL query prior to execution.
*
* @return a reference to the composed {@link QueryPostProcessor QueryPostProcessors}.
* @see org.springframework.data.gemfire.repository.query.QueryPostProcessor
*/
protected QueryPostProcessor<?, String> getQueryPostProcessor() {
return this.queryPostProcessor;
}
/**
* Registers the given {@link QueryPostProcessor} used to post process OQL queries
* generated from {@link Repository} {@link QueryMethod query methods}.
*
* @param queryPostProcessor {@link QueryPostProcessor} to register.
* @return this {@link GemfireRepositoryQuery}.
* @see org.springframework.data.gemfire.repository.query.QueryPostProcessor
*/
public GemfireRepositoryQuery register(QueryPostProcessor<?, String> queryPostProcessor) {
this.queryPostProcessor = this.queryPostProcessor.processBefore(queryPostProcessor);
return this;
}
enum ProvidedQueryPostProcessor implements QueryPostProcessor<Repository, String> {
IDENTITY {
@Override
public String postProcess(QueryMethod queryMethod, String query, Object... arguments) {
return query;
}
}
}
}

View File

@@ -22,26 +22,29 @@ import java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.data.gemfire.GemfireTemplate;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;
/**
* {@link GemfireRepositoryQuery} backed by a {@link PartTree} and thus, deriving an OQL query from the backing query
* method's name.
*
* {@link GemfireRepositoryQuery} backed by a {@link PartTree}, deriving an OQL query
* from the backing query method's name/signature.
*
* @author Oliver Gierke
* @author John Blum
* @see org.springframework.data.gemfire.repository.query.GemfireRepositoryQuery
*/
public class PartTreeGemfireRepositoryQuery extends GemfireRepositoryQuery {
private final GemfireQueryMethod method;
private final PartTree tree;
private final GemfireTemplate template;
private final PartTree tree;
/**
* Creates a new {@link PartTreeGemfireRepositoryQuery} using the given {@link GemfireQueryMethod} and
* {@link GemfireTemplate}.
*
*
* @param method must not be {@literal null}.
* @param template must not be {@literal null}.
*/
@@ -51,30 +54,51 @@ public class PartTreeGemfireRepositoryQuery extends GemfireRepositoryQuery {
Class<?> domainClass = method.getEntityInformation().getJavaType();
this.tree = new PartTree(method.getName(), domainClass);
this.method = method;
this.template = template;
this.tree = new PartTree(method.getName(), domainClass);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[])
*/
@Override
public Object execute(Object[] parameters) {
ParametersParameterAccessor parameterAccessor = new ParametersParameterAccessor(method.getParameters(), parameters);
public Object execute(Object[] arguments) {
QueryString query = new GemfireQueryCreator(tree, method.getPersistentEntity())
.createQuery(parameterAccessor.getSort());
QueryString query = createQuery(this.method, this.tree, arguments);
RepositoryQuery repositoryQuery = new StringBasedGemfireRepositoryQuery(query.toString(), method, template);
GemfireRepositoryQuery repositoryQuery = newRepositoryQuery(query, this.method, this.template);
return repositoryQuery.execute(prepareStringParameters(parameters));
return repositoryQuery.execute(prepareStringParameters(arguments));
}
private QueryString createQuery(GemfireQueryMethod queryMethod, PartTree tree, Object[] arguments) {
ParametersParameterAccessor parameterAccessor =
new ParametersParameterAccessor(queryMethod.getParameters(), arguments);
GemfireQueryCreator queryCreator = new GemfireQueryCreator(tree, queryMethod.getPersistentEntity());
return queryCreator.createQuery(parameterAccessor.getSort());
}
private GemfireRepositoryQuery newRepositoryQuery(QueryString query,
GemfireQueryMethod queryMethod, GemfireTemplate template) {
GemfireRepositoryQuery repositoryQuery =
new StringBasedGemfireRepositoryQuery(query.toString(), queryMethod, template);
repositoryQuery.register(getQueryPostProcessor());
return repositoryQuery;
}
private Object[] prepareStringParameters(Object[] parameters) {
Iterator<Part> partsIterator = tree.getParts().iterator();
List<Object> stringParameters = new ArrayList<Object>(parameters.length);
Iterator<Part> partsIterator = this.tree.getParts().iterator();
List<Object> stringParameters = new ArrayList<>(parameters.length);
for (Object parameter : parameters) {
if (parameter == null || parameter instanceof Sort) {
@@ -99,5 +123,4 @@ public class PartTreeGemfireRepositoryQuery extends GemfireRepositoryQuery {
return stringParameters.toArray();
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.gemfire.repository.query;
import org.apache.geode.cache.Region;
import org.springframework.data.gemfire.mapping.GemfirePersistentEntity;
import org.springframework.data.gemfire.repository.query.support.OqlKeyword;
import org.springframework.data.repository.query.parser.PartTree;
@@ -39,20 +40,21 @@ class QueryBuilder {
/* (non-Javadoc) */
static String asQuery(GemfirePersistentEntity<?> entity, PartTree tree) {
return String.format(SELECT_OQL_TEMPLATE, (tree.isDistinct() ? OqlKeyword.DISTINCT : ""),
return String.format(SELECT_OQL_TEMPLATE, tree.isDistinct() ? OqlKeyword.DISTINCT : "",
entity.getRegionName(), DEFAULT_ALIAS).replaceAll("\\s{2,}", " ");
}
/* (non-Javadoc) */
static String validateQuery(String query) {
Assert.hasText(query, "An OQL Query must be specified");
Assert.hasText(query, "Query is required");
return query;
}
/**
* Constructs an instance of {@link QueryBuilder} initialized with the given query {@link String}.
* Constructs a new instance of {@link QueryBuilder} initialized with the given {@link String query}.
*
* @param query {@link String} containing the base OQL query.
* @param query {@link String} containing the base {@link String OQL query}.
* @see #validateQuery(String)
*/
public QueryBuilder(String query) {
@@ -60,16 +62,15 @@ class QueryBuilder {
}
/**
* Constructs an instance of {@link QueryBuilder} with the given {@link GemfirePersistentEntity}
* and {@link PartTree} that determines the GemFire {@link org.apache.geode.cache.Region}
* to query and whether the query should capture unique results.
* Constructs a new instance of {@link QueryBuilder} initialized with the given {@link GemfirePersistentEntity}
* and {@link PartTree} used to determine the {@link Region} to query and whether the query
* should capture unique results.
*
* @param entity {@link GemfirePersistentEntity} used to determine the GemFire
* {@link org.apache.geode.cache.Region} to query.
* @param tree {@link PartTree} containing parts of the OQL Query for determining things
* like uniqueness.
* @param entity {@link GemfirePersistentEntity} used to determine the {@link Region} to query.
* @param tree {@link PartTree} containing parts of the OQL Query for determining things like uniqueness.
* @see org.springframework.data.gemfire.mapping.GemfirePersistentEntity
* @see org.springframework.data.repository.query.parser.PartTree
* @see #QueryBuilder(String)
*/
public QueryBuilder(GemfirePersistentEntity<?> entity, PartTree tree) {
this(asQuery(entity, tree));
@@ -97,8 +98,10 @@ class QueryBuilder {
* @see org.springframework.data.gemfire.repository.query.Predicate
*/
protected String withPredicate(String query, Predicate predicate) {
return (predicate == null ? query
: String.format(WHERE_CLAUSE_TEMPLATE, query, predicate.toString(DEFAULT_ALIAS)));
return predicate != null
? String.format(WHERE_CLAUSE_TEMPLATE, query, predicate.toString(DEFAULT_ALIAS))
: query;
}
/*

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2017 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.gemfire.repository.query;
import org.springframework.core.Ordered;
import org.springframework.data.gemfire.repository.Query;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* {@link QueryPostProcessor} defines a contract for implementations to post process a given {@link String OQL query}
* and possibly return a new or modified version of the same {@link String OQL query}.
*
* {@link QueryPostProcessor QueryPostProcessors} are particularly useful for {@link Repository}
* {@link QueryMethod query method} generated {@link String queries}, giving the user a chance
* via the callback to further process the generated {@link String query}.
*
* @author John Blum
* @param <T> {@link Class type} identifying the {@link Repository Repositories} to match on for registration.
* @param <QUERY> {@link Class type} of the query to process.
* @see org.springframework.data.repository.Repository
* @see org.springframework.data.repository.query.QueryMethod
* @since 2.1.0
*/
@FunctionalInterface
@SuppressWarnings("unused")
public interface QueryPostProcessor<T extends Repository, QUERY> extends Ordered {
Object[] EMPTY_ARGUMENTS = new Object[0];
/**
* Defines the {@link Integer order} of this {@link QueryPostProcessor} relative to
* other {@link QueryPostProcessor QueryPostProcessors} in a sort.
*
* Defaults to the {@link Ordered#LOWEST_PRECEDENCE}.
*
* @return an {@link Integer} value specifying the {@link Integer order} of this {@link QueryPostProcessor}
* relative to other {@link QueryPostProcessor QueryPostProcessors} in a sort.
* @see org.springframework.core.Ordered#getOrder()
*/
@Override
default int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
/**
* Callback used to post process the given {@link QUERY OQL query} and return a possibly new
* or modified {@link String OQL query}.
*
* This callback is invoked for OQL queries generated from the SD Repository {@link QueryMethod} signature
* as well as OQL queries specified using the {@link Query @Query} annotation and OQL queries
* defined in a application properties file.
*
* @param query {@link QUERY OQL query} to process.
* @return a possibly new or modified version of the same {@link String OQL query}.
* @see org.springframework.data.repository.query.QueryMethod
*/
default QUERY postProcess(@NonNull QueryMethod queryMethod, QUERY query) {
return postProcess(queryMethod, query, EMPTY_ARGUMENTS);
}
/**
* Callback used to post process the given {@link QUERY OQL query} and return a possibly new
* or modified {@link String OQL query}.
*
* This callback is invoked for OQL queries generated from the SD Repository {@link QueryMethod} signature
* as well as OQL queries specified using the {@link Query @Query} annotation and OQL queries
* defined in a application properties file.
*
* @param query {@link QUERY OQL query} to process.
* @param arguments array of {@link Object Objects} containing the arguments to the query parameters.
* @return a possibly new or modified version of the same {@link String OQL query}.
* @see org.springframework.data.repository.query.QueryMethod
*/
QUERY postProcess(@NonNull QueryMethod queryMethod, QUERY query, Object... arguments);
/**
* Builder method used to compose 2 {@link QueryPostProcessor QueryPostProcessors}
* with this {@link QueryPostProcessor} preceding the given {@link QueryPostProcessor}.
*
* @param queryPostProcessor {@link QueryPostProcessor} to compose with this {@link QueryPostProcessor}.
* @return a composed {@link QueryPostProcessor} consisting of this {@link QueryPostProcessor}
* followed by the given {@link QueryPostProcessor}. Returns this {@link QueryPostProcessor}
* if given {@link QueryPostProcessor} is {@literal null}.
*/
@NonNull
default QueryPostProcessor<?, QUERY> processBefore(@Nullable QueryPostProcessor<?, QUERY> queryPostProcessor) {
return queryPostProcessor == null ? this : (queryMethod, query, arguments) ->
queryPostProcessor.postProcess(queryMethod, this.postProcess(queryMethod, query, arguments), arguments);
}
/**
* Builder method used to compose 2 {@link QueryPostProcessor QueryPostProcessors}
* with the given {@link QueryPostProcessor} preceding this {@link QueryPostProcessor}.
*
* @param queryPostProcessor {@link QueryPostProcessor} to compose with this {@link QueryPostProcessor}.
* @return a composed {@link QueryPostProcessor} consisting of the given {@link QueryPostProcessor}
* followed by this {@link QueryPostProcessor}. Returns this {@link QueryPostProcessor}
* if given {@link QueryPostProcessor} is {@literal null}.
*/
@NonNull
default QueryPostProcessor<?, QUERY> processAfter(@Nullable QueryPostProcessor<?, QUERY> queryPostProcessor) {
return queryPostProcessor == null ? this : (queryMethod, query, arguments) ->
this.postProcess(queryMethod, queryPostProcessor.postProcess(queryMethod, query, arguments), arguments);
}
}

View File

@@ -16,6 +16,8 @@
package org.springframework.data.gemfire.repository.query;
import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeIsEmpty;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -30,7 +32,7 @@ import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* {@link QueryString} is a utility class used to construct GemFire OQL query statement syntax.
* {@link QueryString} is a utility class used to construct GemFire OQL query statements.
*
* @author Oliver Gierke
* @author David Turanski
@@ -42,6 +44,7 @@ import org.springframework.util.StringUtils;
*/
public class QueryString {
// OQL Query Patterns
protected static final Pattern HINT_PATTERN = Pattern.compile("<HINT '\\w+'(, '\\w+')*>");
protected static final Pattern IMPORT_PATTERN = Pattern.compile("IMPORT .+;");
protected static final Pattern LIMIT_PATTERN = Pattern.compile("LIMIT \\d+");
@@ -59,40 +62,82 @@ public class QueryString {
private static final String IN_PARAMETER_PATTERN = "(?<=IN (SET|LIST) \\$)\\d";
private static final String REGION_PATTERN = "\\/(\\/?\\w)+";
private final String query;
private static final String COUNT_QUERY = "count(*)";
private static final String STAR_QUERY = "*";
/* (non-Javadoc) */
static String asQuery(Class<?> domainType, boolean isCountQuery) {
return String.format(SELECT_OQL_TEMPLATE, (isCountQuery ? "count(*)" : "*"),
validateDomainType(domainType).getSimpleName());
}
/* (non-Javadoc) */
static <T> Class<T> validateDomainType(Class<T> domainType) {
Assert.notNull(domainType, "domainType must not be null");
return domainType;
}
/* (non-Javadoc) */
static String validateQuery(String query) {
Assert.hasText(query, "An OQL Query must be specified");
return query;
/**
* Factory method used to construct an instance of {@link QueryString} initialized with the given {@link String query}.
*
* @param query {@link String} containing the OQL query.
* @return a new {@link QueryString} initialized with the given {@link String query}.
* @throws IllegalArgumentException if {@link String query} is not specified.
* @see #QueryString(String)
*/
public static QueryString of(String query) {
return new QueryString(query);
}
/**
* Constructs an instance of {@link QueryString} initialized with the given GemFire OQL Query {@link String}.
* Factory method used to construct an instance of {@link QueryString} initialized with
* the given {@link Class domain type} for which the query will be created.
*
* @param query {@link String} specifying the GemFire OQL Query.
* @throws IllegalArgumentException if the query string is unspecified (null or empty).
* @param domainType {@link Class domain object type} for which the query will be created.
* @return a new {@link QueryString} initialized with the given {@link String query}.
* @throws IllegalArgumentException if {@link Class domain type} is {@literal null}.
* @see #QueryString(Class)
*/
public static QueryString from(Class<?> domainType) {
return new QueryString(domainType);
}
/**
* Factory method used to construct an instance of {@link QueryString} initialized with
* the given {@link Class domain type} for which a count query will be created.
*
* @param domainType {@link Class domain object type} for which the query will be created.
* @return a new {@link QueryString} initialized with the given {@link String query}.
* @throws IllegalArgumentException if {@link Class domain type} is {@literal null}.
* @see #QueryString(Class)
*/
public static QueryString count(Class<?> domainType) {
return new QueryString(domainType, true);
}
static String asQuery(Class<?> domainType, boolean isCountQuery) {
return String.format(SELECT_OQL_TEMPLATE, isCountQuery ? COUNT_QUERY : STAR_QUERY,
validateDomainType(domainType).getSimpleName());
}
static <T> Class<T> validateDomainType(Class<T> domainType) {
Assert.notNull(domainType, "Domain type is required");
return domainType;
}
static String validateQuery(String query) {
Assert.hasText(query, String.format("Query [%s] is required", query));
return query;
}
private final String query;
/**
* Constructs a new instance of {@link QueryString} initialized with the given {@link String OQL query}.
*
* @param query {@link String} containing the OQL query.
* @throws IllegalArgumentException if {@link String query} is {@literal null} or empty.
* @see #validateQuery(String)
* @see java.lang.String
*/
public QueryString(String query) {
this.query = validateQuery(query);
}
/**
* Constructs a GemFire OQL {@literal SELECT} Query for the given domain class.
* Constructs a new instance of {@link QueryString} initialized from the given {@link Class domain type},
* which is used to construct an OQL {@literal SELECT} query statement.
*
* @param domainType application domain object type to query; must not be {@literal null}.
* @param domainType {@link Class application domain type} to query; must not be {@literal null}.
* @throws IllegalArgumentException if {@link Class domain type} is {@literal null}.
* @see #QueryString(Class, boolean)
*/
@SuppressWarnings("unused")
@@ -101,16 +146,49 @@ public class QueryString {
}
/**
* Constructs a GemFire OQL {@literal SELECT} Query for the given domain class. {@code isCountQuery} indicates
* whether to select a count or select the contents of the objects of the given domain object type.
* Constructs a new instance of {@link QueryString} initialized from the given {@link Class domain type},
* which is used to construct an OQL {@literal SELECT} query statement.
*
* @param domainType application domain object type to query; must not be {@literal null}.
* @param isCountQuery boolean value to indicate if this is a count query.
* @throws IllegalArgumentException if {@code domainType} is null.
* {@code asCountQuery} is a {@link Boolean} flag indicating whether to select a count or select the contents
* of the objects for the given {@link Class domain type}.
*
* @param domainType {@link Class application domain type} to query; must not be {@literal null}.
* @param asCountQuery boolean value to indicate if this is a count query.
* @throws IllegalArgumentException if {@link Class domain type} is {@literal null}.
* @see #asQuery(Class, boolean)
* @see #QueryString(String)
*/
public QueryString(Class<?> domainType, boolean isCountQuery) {
this(asQuery(domainType, isCountQuery));
public QueryString(Class<?> domainType, boolean asCountQuery) {
this(asQuery(domainType, asCountQuery));
}
/**
* Replaces the {@literal SELECT query} with a {@literal SELECT DISTINCT query} if the {@link String query}
* is not already distinct; i.e. does not contain the {@literal DISTINCT} keyword.
*
* @return a {@literal SELECT DISTINCT query} if {@link String query} does not contain
* the {@literal DISTINCT} keyword.
* @see java.lang.String#replaceFirst(String, String)
* @see #asDistinct(String)
*/
public QueryString asDistinct() {
return QueryString.of(asDistinct(this.query));
}
/**
* Replaces the {@literal SELECT query} with a {@literal SELECT DISTINCT query} if the {@link String query}
* is not already distinct; i.e. does not contain the {@literal DISTINCT} keyword.
*
* @param query {@link String} containing the {@link String query} to evaluate.
* @return a {@literal SELECT DISTINCT query} if {@link String query} does not contain
* the {@literal DISTINCT} keyword.
* @see java.lang.String#replaceFirst(String, String)
*/
String asDistinct(String query) {
return query.contains(OqlKeyword.DISTINCT.getKeyword()) ? query
: query.replaceFirst(OqlKeyword.SELECT.getKeyword(),
String.format("%1$s %2$s", OqlKeyword.SELECT.getKeyword(), OqlKeyword.DISTINCT.getKeyword()));
}
/**
@@ -121,8 +199,9 @@ public class QueryString {
* @return a Query String having "in" parameters bound with values.
*/
public QueryString bindIn(Collection<?> values) {
if (values != null) {
return new QueryString(this.query.replaceFirst(IN_PATTERN, String.format("(%s)",
if (!nullSafeIsEmpty(values)) {
return QueryString.of(this.query.replaceFirst(IN_PATTERN, String.format("(%s)",
StringUtils.collectionToDelimitedString(values, ", ", "'", "'"))));
}
@@ -130,35 +209,40 @@ public class QueryString {
}
/**
* Replaces the domain classes referenced inside the current query with the given {@link Region}.
* Replaces the {@link Class domain classes} referenced inside the current {@link String query}
* with the given {@link Region}.
*
* @param domainClass the class type of the GemFire persistent entity to query; must not be {@literal null}.
* @param region the GemFire Region in which to query; must not be {@literal null}.
* @return a Query String with the FROM clause in the OQL statement evaluated and replaced with
* the fully-qualified Region to query.
* @param domainClass {@link Class type} of the persistent entity to query; must not be {@literal null}.
* @param region {@link Region} to query; must not be {@literal null}.
* @return a new {@link QueryString} with an OQL {@literal SELECT statement} having a {@literal FROM clause}
* based on the selected {@link Region}.
* @see org.apache.geode.cache.Region
* @see java.lang.Class
*/
@SuppressWarnings("unused")
public QueryString forRegion(Class<?> domainClass, Region<?, ?> region) {
return new QueryString(this.query.replaceAll(REGION_PATTERN, region.getFullPath()));
public QueryString fromRegion(Class<?> domainClass, Region<?, ?> region) {
return QueryString.of(this.query.replaceAll(REGION_PATTERN, region.getFullPath()));
}
/**
/**
* Returns the parameter indexes used in this query.
*
* @return the parameter indexes used in this query or an empty {@link Iterable} if none are used.
* @see java.lang.Iterable
*/
public Iterable<Integer> getInParameterIndexes() {
Pattern pattern = Pattern.compile(IN_PARAMETER_PATTERN);
Matcher matcher = pattern.matcher(query);
List<Integer> result = new ArrayList<>();
Matcher matcher = pattern.matcher(this.query);
List<Integer> indexes = new ArrayList<>();
while (matcher.find()) {
result.add(Integer.parseInt(matcher.group()));
indexes.add(Integer.parseInt(matcher.group()));
}
return result;
return indexes;
}
/**
@@ -171,36 +255,26 @@ public class QueryString {
* @see org.springframework.data.gemfire.repository.query.QueryString
*/
public QueryString orderBy(Sort sort) {
if (hasSort(sort)) {
StringBuilder orderClause = new StringBuilder("ORDER BY ");
StringBuilder orderByClause = new StringBuilder("ORDER BY ");
int count = 0;
for (Sort.Order order : sort) {
orderClause.append(count++ > 0 ? ", " : "");
orderClause.append(String.format("%1$s %2$s", order.getProperty(), order.getDirection()));
orderByClause.append(count++ > 0 ? ", " : "");
orderByClause.append(String.format("%1$s %2$s", order.getProperty(), order.getDirection()));
}
return new QueryString(String.format("%1$s %2$s", makeDistinct(this.query), orderClause.toString()));
return new QueryString(String.format("%1$s %2$s", asDistinct(this.query), orderByClause.toString()));
}
return this;
}
/* (non-Javadoc) */
private boolean hasSort(Sort sort) {
return (sort != null && sort.iterator().hasNext());
}
/**
* Replaces the SELECT query with a SELECT DISTINCT query if the query does not contain the DISTINCT OQL keyword.
*
* @param query {@link String} containing the query to evaluate.
* @return a SELECT DISTINCT query if {@code query} does not contain the DISTINCT OQL keyword.
*/
String makeDistinct(String query) {
return (query.contains(OqlKeyword.DISTINCT.getKeyword()) ? query
: query.replaceFirst(OqlKeyword.SELECT.getKeyword(),
String.format("%1$s %2$s", OqlKeyword.SELECT.getKeyword(), OqlKeyword.DISTINCT.getKeyword())));
return sort != null && sort.iterator().hasNext();
}
/**
@@ -210,15 +284,17 @@ public class QueryString {
* @return a new {@link QueryString} if hints are not null or empty, or return this {@link QueryString}.
*/
public QueryString withHints(String... hints) {
if (!ObjectUtils.isEmpty(hints)) {
StringBuilder builder = new StringBuilder();
for (String hint : hints) {
builder.append(builder.length() > 0 ? ", " : "");
builder.append(String.format("'%1$s'", hint));
builder.append(String.format("'%s'", hint));
}
return new QueryString(String.format(HINTS_OQL_TEMPLATE, builder.toString(), query));
return QueryString.of(String.format(HINTS_OQL_TEMPLATE, builder.toString(), this.query));
}
return this;
@@ -231,8 +307,8 @@ public class QueryString {
* @return a new {@link QueryString} if an import was declared, or return this {@link QueryString}.
*/
public QueryString withImport(String importExpression) {
return (StringUtils.hasText(importExpression) ?
new QueryString(String.format(IMPORT_OQL_TEMPLATE, importExpression, query)) : this);
return StringUtils.hasText(importExpression) ?
QueryString.of(String.format(IMPORT_OQL_TEMPLATE, importExpression, this.query)) : this;
}
/**
@@ -242,7 +318,7 @@ public class QueryString {
* @return a new {@link QueryString} if a limit was specified, or return this {@link QueryString}.
*/
public QueryString withLimit(Integer limit) {
return (limit != null ? new QueryString(String.format(LIMIT_OQL_TEMPLATE, query, limit)) : this);
return limit != null ? QueryString.of(String.format(LIMIT_OQL_TEMPLATE, this.query, limit)) : this;
}
/**
@@ -251,15 +327,18 @@ public class QueryString {
* @return a new {@link QueryString} with tracing enabled.
*/
public QueryString withTrace() {
return new QueryString(String.format(TRACE_OQL_TEMPLATE, query));
return QueryString.of(String.format(TRACE_OQL_TEMPLATE, this.query));
}
/*
* (non-Javadoc)
/**
* Returns a {@link String} representation of this {@link QueryString}.
*
* @return a {@link String} representation of this {@link QueryString}.
* @see java.lang.Object#toString()
* @see java.lang.String
*/
@Override
public String toString() {
return query;
return this.query;
}
}

View File

@@ -15,14 +15,19 @@
*/
package org.springframework.data.gemfire.repository.query;
import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeSize;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException;
import java.util.Collection;
import java.util.Collections;
import org.apache.geode.cache.query.SelectResults;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.gemfire.GemfireTemplate;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
@@ -49,8 +54,14 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery {
* Constructor used for testing purposes only!
*/
StringBasedGemfireRepositoryQuery() {
query = null;
template = null;
this.query = null;
this.template = null;
register(ProvidedQueryPostProcessors.LIMIT
.processBefore(ProvidedQueryPostProcessors.IMPORT)
.processBefore(ProvidedQueryPostProcessors.HINT)
.processBefore(ProvidedQueryPostProcessors.TRACE));
}
/**
@@ -73,32 +84,61 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery {
* @param template must not be {@literal null}.
*/
public StringBasedGemfireRepositoryQuery(String query, GemfireQueryMethod queryMethod, GemfireTemplate template) {
super(queryMethod);
Assert.notNull(template, "GemfireTemplate must not be null");
Assert.state(!(queryMethod.isModifyingQuery() || queryMethod.isPageQuery()), INVALID_QUERY);
this.userDefinedQuery |= !StringUtils.hasText(query);
this.query = new QueryString(StringUtils.hasText(query) ? query : queryMethod.getAnnotatedQuery());
this.query = QueryString.of(StringUtils.hasText(query) ? query : queryMethod.getAnnotatedQuery());
this.template = template;
if (queryMethod.isModifyingQuery() || queryMethod.isPageQuery()) {
throw new IllegalStateException(INVALID_QUERY);
}
register(ProvidedQueryPostProcessors.LIMIT
.processBefore(ProvidedQueryPostProcessors.IMPORT)
.processBefore(ProvidedQueryPostProcessors.HINT)
.processBefore(ProvidedQueryPostProcessors.TRACE));
}
/*
* (non-Javadoc)
/**
* Sets this {@link RepositoryQuery} to be user-defined.
*
* @return this {@link RepositoryQuery}.
*/
public StringBasedGemfireRepositoryQuery asUserDefinedQuery() {
this.userDefinedQuery = true;
return this;
}
/*
* (non-Javadoc)
/**
* Determines whether the query represented by this {@link RepositoryQuery} is user-defined or was generated by
* the Spring Data {@link Repository} infrastructure.
*
* @return a boolean value indicating whether this {@link RepositoryQuery} is user-defined or was generated by
* the Spring Data {@link Repository} infrastructure.
*/
public boolean isUserDefinedQuery() {
return userDefinedQuery;
return this.userDefinedQuery;
}
/**
* Returns a reference to the {@link QueryString managed query}.
*
* @return a reference to the {@link QueryString managed query}.
* @see org.springframework.data.gemfire.repository.query.QueryString
*/
protected QueryString getQuery() {
return this.query;
}
/**
* Returns a reference to the {@link GemfireTemplate} used to perform all data access and query operations.
*
* @return a reference to the {@link GemfireTemplate} used to perform all data access and query operations.
* @see org.springframework.data.gemfire.GemfireTemplate
*/
protected GemfireTemplate getTemplate() {
return this.template;
}
/*
@@ -106,72 +146,69 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery {
* @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[])
*/
@Override
public Object execute(Object[] parameters) {
QueryMethod localQueryMethod = getQueryMethod();
public Object execute(Object[] arguments) {
QueryString query = (isUserDefinedQuery() ? this.query
: this.query.forRegion(localQueryMethod.getEntityInformation().getJavaType(), template.getRegion()));
QueryMethod queryMethod = getQueryMethod();
QueryString query = preProcess(queryMethod, getQuery(), arguments);
String queryString = query.toString();
String processedQueryString = getQueryPostProcessor().postProcess(queryMethod, queryString, arguments);
SelectResults<?> selectResults = getTemplate().find(processedQueryString, arguments);
return postProcess(queryMethod, selectResults);
}
QueryString preProcess(QueryMethod queryMethod, QueryString query, Object[] arguments) {
query = isUserDefinedQuery() ? query
: query.fromRegion(queryMethod.getEntityInformation().getJavaType(), getTemplate().getRegion());
ParametersParameterAccessor parameterAccessor =
new ParametersParameterAccessor(localQueryMethod.getParameters(), parameters);
new ParametersParameterAccessor(queryMethod.getParameters(), arguments);
for (Integer index : query.getInParameterIndexes()) {
query = query.bindIn(toCollection(parameterAccessor.getBindableValue(index - 1)));
}
query = applyQueryAnnotationExtensions(localQueryMethod, query);
return query;
}
Collection<?> result = toCollection(template.find(query.toString(), parameters));
Object postProcess(QueryMethod queryMethod, SelectResults<?> selectResults) {
if (localQueryMethod.isCollectionQuery()) {
return result;
Collection<?> collection = toCollection(selectResults);
if (queryMethod.isCollectionQuery()) {
return collection;
}
else if (localQueryMethod.isQueryForEntity()) {
if (result.isEmpty()) {
else if (queryMethod.isQueryForEntity()) {
if (collection.isEmpty()) {
return null;
}
else if (result.size() == 1) {
return result.iterator().next();
else if (collection.size() == 1) {
return collection.iterator().next();
}
else {
throw new IncorrectResultSizeDataAccessException(1, result.size());
throw new IncorrectResultSizeDataAccessException(1, collection.size());
}
}
else if (isSingleResultNonEntityQuery(localQueryMethod, result)) {
return result.iterator().next();
else if (isSingleNonEntityResult(queryMethod, collection)) {
return collection.iterator().next();
}
else {
throw new IllegalStateException("Unsupported query: " + query.toString());
throw newIllegalStateException("Unsupported query: %s", query.toString());
}
}
QueryString applyQueryAnnotationExtensions(QueryMethod queryMethod, QueryString queryString) {
QueryString resolvedQueryString = queryString;
@SuppressWarnings("all")
boolean isSingleNonEntityResult(QueryMethod method, Collection<?> result) {
if (queryMethod instanceof GemfireQueryMethod) {
GemfireQueryMethod gemfireQueryMethod = (GemfireQueryMethod) queryMethod;
String query = queryString.toString().toUpperCase();
Class<?> methodReturnType = method.getReturnedObjectType();
if (gemfireQueryMethod.hasImport() && !QueryString.IMPORT_PATTERN.matcher(query).find()) {
resolvedQueryString = resolvedQueryString.withImport(gemfireQueryMethod.getImport());
}
if (gemfireQueryMethod.hasHint() && !QueryString.HINT_PATTERN.matcher(query).find()) {
resolvedQueryString = resolvedQueryString.withHints(gemfireQueryMethod.getHints());
}
if (gemfireQueryMethod.hasLimit() && !QueryString.LIMIT_PATTERN.matcher(query).find()) {
resolvedQueryString = resolvedQueryString.withLimit(gemfireQueryMethod.getLimit());
}
if (gemfireQueryMethod.hasTrace() && !QueryString.TRACE_PATTERN.matcher(query).find()) {
resolvedQueryString = resolvedQueryString.withTrace();
}
}
methodReturnType = methodReturnType != null ? methodReturnType : Void.class;
return resolvedQueryString;
}
boolean isSingleResultNonEntityQuery(QueryMethod method, Collection<?> result) {
return (!method.isCollectionQuery() && method.getReturnedObjectType() != null
&& !Void.TYPE.equals(method.getReturnedObjectType()) && result != null && result.size() == 1);
return nullSafeSize(result) == 1 && !Void.TYPE.equals(methodReturnType) && !method.isCollectionQuery();
}
/**
@@ -185,7 +222,8 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery {
* @see org.springframework.util.CollectionUtils#arrayToList(Object)
* @see org.apache.geode.cache.query.SelectResults
*/
Collection<?> toCollection(final Object source) {
Collection<?> toCollection(Object source) {
if (source instanceof SelectResults) {
return ((SelectResults) source).asList();
}
@@ -198,6 +236,81 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery {
return Collections.emptyList();
}
return (source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singletonList(source));
return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singletonList(source);
}
enum ProvidedQueryPostProcessors implements QueryPostProcessor<Repository, String> {
HINT {
@Override
public String postProcess(QueryMethod queryMethod, String query, Object... arguments) {
if (queryMethod instanceof GemfireQueryMethod) {
GemfireQueryMethod gemfireQueryMethod = (GemfireQueryMethod) queryMethod;
if (gemfireQueryMethod.hasHint() && !QueryString.HINT_PATTERN.matcher(query).find()) {
query = QueryString.of(query).withHints(gemfireQueryMethod.getHints()).toString();
}
}
return query;
}
},
IMPORT {
@Override
public String postProcess(QueryMethod queryMethod, String query, Object... arguments) {
if (queryMethod instanceof GemfireQueryMethod) {
GemfireQueryMethod gemfireQueryMethod = (GemfireQueryMethod) queryMethod;
if (gemfireQueryMethod.hasImport() && !QueryString.IMPORT_PATTERN.matcher(query).find()) {
query = QueryString.of(query).withImport(gemfireQueryMethod.getImport()).toString();
}
}
return query;
}
},
LIMIT {
@Override
public String postProcess(QueryMethod queryMethod, String query, Object... arguments) {
if (queryMethod instanceof GemfireQueryMethod) {
GemfireQueryMethod gemfireQueryMethod = (GemfireQueryMethod) queryMethod;
if (gemfireQueryMethod.hasLimit() && !QueryString.LIMIT_PATTERN.matcher(query).find()) {
query = QueryString.of(query).withLimit(gemfireQueryMethod.getLimit()).toString();
}
}
return query;
}
},
TRACE {
@Override
public String postProcess(QueryMethod queryMethod, String query, Object... arguments) {
if (queryMethod instanceof GemfireQueryMethod) {
GemfireQueryMethod gemfireQueryMethod = (GemfireQueryMethod) queryMethod;
if (gemfireQueryMethod.hasTrace() && !QueryString.TRACE_PATTERN.matcher(query).find()) {
query = QueryString.of(query).withTrace().toString();
}
}
return query;
}
}
}
}

View File

@@ -28,6 +28,7 @@ import org.springframework.util.StringUtils;
* @since 1.0.0
*/
public enum OqlKeyword {
AND,
AS,
COUNT,

View File

@@ -23,6 +23,7 @@ import java.lang.reflect.Method;
import java.util.Optional;
import org.apache.geode.cache.Region;
import org.springframework.context.ApplicationContext;
import org.springframework.data.gemfire.GemfireTemplate;
import org.springframework.data.gemfire.mapping.GemfirePersistentEntity;
import org.springframework.data.gemfire.mapping.GemfirePersistentProperty;
@@ -34,6 +35,7 @@ import org.springframework.data.gemfire.repository.query.PartTreeGemfireReposito
import org.springframework.data.gemfire.repository.query.StringBasedGemfireRepositoryQuery;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -41,6 +43,9 @@ import org.springframework.data.repository.core.support.RepositoryFactorySupport
import org.springframework.data.repository.query.EvaluationContextProvider;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -54,25 +59,65 @@ import org.springframework.util.StringUtils;
*/
public class GemfireRepositoryFactory extends RepositoryFactorySupport {
private static final Class<org.springframework.data.gemfire.mapping.annotation.Region> REGION_ANNOTATION =
org.springframework.data.gemfire.mapping.annotation.Region.class;
static final String REGION_NOT_FOUND = "Region [%1$s] for Domain Type [%2$s] using Repository [%3$s] was not found;"
+ " You must configure a Region with name [%1$s] in the application context";
static final String REGION_REPOSITORY_ID_TYPE_MISMATCH =
"Region [%1$s] requires keys of type [%2$s], but Repository [%3$s] declared an id of type [%4$s]";
static final String REPOSITORY_ENTITY_ID_TYPE_MISMATCH =
"Repository [%1$s] declared an id of type [%2$s], but entity [%3$s] has an id of type [%4$s]";
private final MappingContext<? extends GemfirePersistentEntity<?>, GemfirePersistentProperty> mappingContext;
private final Regions regions;
/**
* Creates a new {@link GemfireRepositoryFactory}.
* Constructs a new instance of {@link GemfireRepositoryFactory} initialized with the given collection
* of configured {@link Region Regions} and the {@link MappingContext}.
*
* @param regions must not be {@literal null}.
* @param mappingContext the {@link MappingContext} used by the constructed Repository for mapping entities
* to the underlying data store, must not be {@literal null}.
* @param regions {@link Iterable} collection of configured {@link Region Regions} used by this application;
* must not be {@literal null}.
* @param mappingContext {@link MappingContext} used to map entities to the underlying data store,
* must not be {@literal null}.
* @throws IllegalArgumentException if either {@link Regions} or the {@link MappingContext} is {@literal null}.
* @see org.springframework.data.gemfire.mapping.GemfireMappingContext
* @see org.springframework.data.gemfire.mapping.Regions
* @see org.springframework.data.mapping.context.MappingContext
*/
public GemfireRepositoryFactory(Iterable<Region<?, ?>> regions,
MappingContext<? extends GemfirePersistentEntity<?>, GemfirePersistentProperty> mappingContext) {
public GemfireRepositoryFactory(@NonNull Iterable<Region<?, ?>> regions,
@NonNull MappingContext<? extends GemfirePersistentEntity<?>, GemfirePersistentProperty> mappingContext) {
Assert.notNull(regions, "Regions must not be null");
Assert.notNull(mappingContext, "MappingContext must not be null");
Assert.notNull(regions, "Regions are required");
Assert.notNull(mappingContext, "MappingContext is required");
this.regions = new Regions(regions, mappingContext);
this.mappingContext = mappingContext;
this.regions = new Regions(regions, this.mappingContext);
}
/**
* Returns a reference to the GemFire {@link MappingContext} used to provide mapping meta-data
* between {@link Class entity types} and the data store.
*
* @return a reference to the GemFire {@link MappingContext}.
* @see org.springframework.data.gemfire.mapping.GemfireMappingContext
* @see org.springframework.data.mapping.context.MappingContext
*/
protected MappingContext<? extends GemfirePersistentEntity<?>, GemfirePersistentProperty> getMappingContext() {
return this.mappingContext;
}
/**
* Returns a reference to the configured, application-defined {@link Region Regions}.
*
* @return a reference to the configured, application-defined {@link Region Regions}.
* @see org.springframework.data.gemfire.mapping.Regions
*/
protected Regions getRegions() {
return this.regions;
}
/*
@@ -80,13 +125,18 @@ public class GemfireRepositoryFactory extends RepositoryFactorySupport {
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getEntityInformation(java.lang.Class)
*/
@Override
@SuppressWarnings("unchecked")
public <T, ID> GemfireEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
return new DefaultGemfireEntityInformation<>(resolvePersistentEntity(domainClass));
}
GemfirePersistentEntity<T> entity =
(GemfirePersistentEntity<T>) mappingContext.getPersistentEntity(domainClass);
return new DefaultGemfireEntityInformation<>(entity);
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport
* #getRepositoryBaseClass(org.springframework.data.repository.core.RepositoryMetadata)
*/
@Override
protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
return SimpleGemfireRepository.class;
}
/*
@@ -99,53 +149,96 @@ public class GemfireRepositoryFactory extends RepositoryFactorySupport {
GemfireEntityInformation<?, Serializable> entityInformation =
getEntityInformation(repositoryInformation.getDomainType());
GemfireTemplate gemfireTemplate = getTemplate(repositoryInformation);
GemfireTemplate gemfireTemplate = newTemplate(repositoryInformation);
return getTargetRepositoryViaReflection(repositoryInformation, gemfireTemplate, entityInformation);
}
GemfireTemplate getTemplate(RepositoryMetadata metadata) {
GemfirePersistentEntity<?> entity = mappingContext.getPersistentEntity(metadata.getDomainType());
String entityRegionName = entity.getRegionName();
String repositoryRegionName = getRepositoryRegionName(metadata.getRepositoryInterface());
String resolvedRegionName = StringUtils.hasText(repositoryRegionName) ? repositoryRegionName : entityRegionName;
Region<?, ?> region = regions.getRegion(resolvedRegionName);
if (region == null) {
throw newIllegalStateException("No Region [%1$s] was found for domain class [%2$s];"
+ " Make sure you have configured a GemFire Region of that name in your application context",
resolvedRegionName, metadata.getDomainType().getName());
}
Class<?> regionKeyType = region.getAttributes().getKeyConstraint();
Class<?> entityIdType = metadata.getIdType();
if (regionKeyType != null && entity.getIdProperty() != null) {
Assert.isTrue(regionKeyType.isAssignableFrom(entityIdType), String.format(
"The Region referenced only supports keys of type [%1$s], but the entity to be stored has an id of type [%2$s]",
regionKeyType.getName(), entityIdType.getName()));
}
return new GemfireTemplate(region);
}
String getRepositoryRegionName(Class<?> repositoryInterface) {
return (repositoryInterface.isAnnotationPresent(org.springframework.data.gemfire.mapping.annotation.Region.class) ?
repositoryInterface.getAnnotation(org.springframework.data.gemfire.mapping.annotation.Region.class).value() : null);
}
/*
* (non-Javadoc)
/**
* Constructs a new instance of {@link GemfireTemplate} initialized with the identified {@link Region}
* used to back all persistent, data access operations defined by the {@link Repository}.
*
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport
* #getRepositoryBaseClass(org.springframework.data.repository.core.RepositoryMetadata)
* @param repositoryMetadata {@link RepositoryMetadata} containing meta-data about the {@link Repository}.
* @return a new instance of {@link GemfireTemplate} initialized with the identified {@link Region}.
* @see #resolvePersistentEntity(Class)
* @see #resolveRegion(RepositoryMetadata, GemfirePersistentEntity)
* @see #validate(RepositoryMetadata, GemfirePersistentEntity, Region)
* @see org.springframework.data.repository.core.RepositoryMetadata
* @see org.springframework.data.gemfire.GemfireTemplate
* @see org.apache.geode.cache.Region
*/
@Override
protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
return SimpleGemfireRepository.class;
protected GemfireTemplate newTemplate(RepositoryMetadata repositoryMetadata) {
GemfirePersistentEntity<?> entity = resolvePersistentEntity(repositoryMetadata.getDomainType());
return new GemfireTemplate(validate(repositoryMetadata, entity, resolveRegion(repositoryMetadata, entity)));
}
@Nullable
@SuppressWarnings("unchecked")
private <T> GemfirePersistentEntity<T> resolvePersistentEntity(Class<?> domainType) {
return (GemfirePersistentEntity<T>) getMappingContext().getPersistentEntity(domainType);
}
private Region<?, ?> resolveRegion(RepositoryMetadata repositoryMetadata, GemfirePersistentEntity entity) {
String resolvedRegionName = getRepositoryRegionName(repositoryMetadata)
.orElseGet(() -> getEntityRegionName(repositoryMetadata, entity));
return resolveRegion(repositoryMetadata, resolvedRegionName);
}
private Region<?, ?> validate(RepositoryMetadata repositoryMetadata, GemfirePersistentEntity<?> entity,
Region<?, ?> region) {
Assert.notNull(region, "Region is required");
Class<?> repositoryIdType = repositoryMetadata.getIdType();
Optional.ofNullable(region.getAttributes().getKeyConstraint())
.ifPresent(regionKeyType -> Assert.isTrue(regionKeyType.isAssignableFrom(repositoryIdType),
() -> String.format(REGION_REPOSITORY_ID_TYPE_MISMATCH, region.getFullPath(), regionKeyType.getName(),
repositoryMetadata.getRepositoryInterface().getName(), repositoryIdType.getName())));
Optional.ofNullable(entity)
.map(GemfirePersistentEntity::getIdProperty)
.ifPresent(entityIdProperty -> Assert.isTrue(repositoryIdType.isAssignableFrom(entityIdProperty.getType()),
() -> String.format(REPOSITORY_ENTITY_ID_TYPE_MISMATCH, repositoryMetadata.getRepositoryInterface().getName(),
repositoryIdType.getName(), entityIdProperty.getOwner().getName(), entityIdProperty.getTypeName())));
return region;
}
String getEntityRegionName(@NonNull RepositoryMetadata repositoryMetadata,
@Nullable GemfirePersistentEntity entity) {
Optional<GemfirePersistentEntity> optionalEntity = Optional.ofNullable(entity);
return optionalEntity
.map(GemfirePersistentEntity::getRegionName)
.filter(StringUtils::hasText)
.orElseGet(() -> optionalEntity
.map(GemfirePersistentEntity::getType)
.map(Class::getSimpleName)
.orElseGet(() -> repositoryMetadata.getDomainType().getSimpleName()));
}
Optional<String> getRepositoryRegionName(@NonNull RepositoryMetadata repositoryMetadata) {
return Optional.ofNullable(repositoryMetadata)
.map(RepositoryMetadata::getRepositoryInterface)
.filter(repositoryInterface -> repositoryInterface.isAnnotationPresent(REGION_ANNOTATION))
.map(repositoryInterface -> repositoryInterface.getAnnotation(REGION_ANNOTATION))
.map(regionAnnotation -> regionAnnotation.value())
.filter(StringUtils::hasText);
}
Region<?, ?> resolveRegion(@NonNull RepositoryMetadata repositoryMetadata, String regionNamePath) {
return Optional.ofNullable(getRegions().getRegion(regionNamePath))
.orElseThrow(() -> newIllegalStateException(REGION_NOT_FOUND,
regionNamePath, repositoryMetadata.getDomainType().getName(),
repositoryMetadata.getRepositoryInterface().getName()));
}
/*
@@ -158,22 +251,33 @@ public class GemfireRepositoryFactory extends RepositoryFactorySupport {
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(Key key,
EvaluationContextProvider evaluationContextProvider) {
return Optional.of(
(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) -> {
return Optional.of((Method method, RepositoryMetadata repositoryMetadata, ProjectionFactory projectionFactory,
NamedQueries namedQueries) -> {
GemfireQueryMethod queryMethod = new GemfireQueryMethod(method, metadata, factory, mappingContext);
GemfireTemplate template = getTemplate(metadata);
GemfireQueryMethod queryMethod =
newQueryMethod(method, repositoryMetadata, projectionFactory, evaluationContextProvider);
GemfireTemplate template = newTemplate(repositoryMetadata);
if (queryMethod.hasAnnotatedQuery()) {
return new StringBasedGemfireRepositoryQuery(queryMethod, template).asUserDefinedQuery();
}
if (namedQueries.hasQuery(queryMethod.getNamedQueryName())) {
return new StringBasedGemfireRepositoryQuery(namedQueries.getQuery(queryMethod.getNamedQueryName()),
String namedQueryName = queryMethod.getNamedQueryName();
if (namedQueries.hasQuery(namedQueryName)) {
return new StringBasedGemfireRepositoryQuery(namedQueries.getQuery(namedQueryName),
queryMethod, template).asUserDefinedQuery();
}
return new PartTreeGemfireRepositoryQuery(queryMethod, template);
});
}
@SuppressWarnings({ "unchecked", "unused" })
protected <T extends QueryMethod> T newQueryMethod(Method method, RepositoryMetadata repositoryMetadata,
ProjectionFactory projectionFactory, EvaluationContextProvider evaluationContextProvider) {
return (T) new GemfireQueryMethod(method, repositoryMetadata, projectionFactory, getMappingContext());
}
}

View File

@@ -16,23 +16,41 @@
package org.springframework.data.gemfire.repository.support;
import java.io.Serializable;
import java.util.Collection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.WeakHashMap;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.apache.geode.cache.Region;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.OrderComparator;
import org.springframework.data.gemfire.mapping.GemfirePersistentEntity;
import org.springframework.data.gemfire.mapping.GemfirePersistentProperty;
import org.springframework.data.gemfire.repository.query.GemfireRepositoryQuery;
import org.springframework.data.gemfire.repository.query.QueryPostProcessor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.RepositoryDefinition;
import org.springframework.data.repository.core.support.QueryCreationListener;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* {@link FactoryBean} adapter for {@link GemfireRepositoryFactory}.
*
@@ -50,9 +68,11 @@ import org.springframework.util.Assert;
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport
* @see org.apache.geode.cache.Region
*/
public class GemfireRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable>
public class GemfireRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
extends RepositoryFactoryBeanSupport<T, S, ID> implements ApplicationContextAware {
private ApplicationContext applicationContext;
private Iterable<Region<?, ?>> regions;
private MappingContext<? extends GemfirePersistentEntity<?>, GemfirePersistentProperty> mappingContext;
@@ -67,33 +87,49 @@ public class GemfireRepositoryFactoryBean<T extends Repository<S, ID>, S, ID ext
}
/**
* Sets a reference to the Spring {@link ApplicationContext} in which this object runs.
* Sets a reference to the Spring {@link ApplicationContext}.
*
* @param applicationContext the Spring {@link ApplicationContext} reference.
* @param applicationContext reference to the Spring {@link ApplicationContext}.
* @see org.springframework.context.ApplicationContextAware#setApplicationContext(ApplicationContext)
* @see org.springframework.context.ApplicationContext
*/
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
@SuppressWarnings({ "rawtypes", "unchecked" })
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Collection<Region> regions = applicationContext.getBeansOfType(Region.class).values();
this.regions = (Iterable) Collections.unmodifiableCollection(regions);
this.applicationContext = applicationContext;
this.regions = Collections.unmodifiableSet(applicationContext.getBeansOfType(Region.class).entrySet().stream()
.<Region<?, ?>>map(Map.Entry::getValue)
.collect(Collectors.toSet()));
}
/**
* Configures the {@link MappingContext} used to perform domain object type to store mappings.
* Returns an {@link Optional} reference to the configured Spring {@link ApplicationContext}.
*
* @param mappingContext the {@link MappingContext} to set.
* @return an {@link Optional} reference to the configured Spring {@link ApplicationContext}.
* @see org.springframework.context.ApplicationContext
* @see java.util.Optional
*/
protected Optional<ApplicationContext> getApplicationContext() {
return Optional.ofNullable(this.applicationContext);
}
/**
* Configures the {@link MappingContext} used to perform application domain object type to data store mappings.
*
* @param mappingContext {@link MappingContext} to configure.
* @see org.springframework.data.gemfire.mapping.GemfireMappingContext
* @see org.springframework.data.mapping.context.MappingContext
*/
@Autowired(required = false)
public void setGemfireMappingContext(MappingContext<? extends GemfirePersistentEntity<?>, GemfirePersistentProperty> mappingContext) {
setMappingContext(mappingContext);
this.mappingContext = mappingContext;
}
/**
* Returns a reference to the Spring Data {@link MappingContext} used to perform domain object type
* Returns a reference to the Spring Data {@link MappingContext} used to perform application domain object type
* to data store mappings.
*
* @return a reference to the {@link MappingContext}.
@@ -106,10 +142,31 @@ public class GemfireRepositoryFactoryBean<T extends Repository<S, ID>, S, ID ext
}
/**
* Returns an {@link Iterable} reference to the GemFire {@link Region}s defined
* in the Spring {@link ApplicationContext}.
* Attempts to resolve the {@link MappingContext} used to map {@link GemfirePersistentEntity entities}
* to Pivotal GemFire.
*
* @return a reference to all GemFire {@link Region}s defined in the Spring {@link ApplicationContext}.
* @return a reference to the resolved {@link MappingContext}.
* @throws IllegalStateException if the {@link MappingContext} cannot be resolved.
* @see org.springframework.data.gemfire.mapping.GemfireMappingContext
* @see org.springframework.data.mapping.context.MappingContext
*/
private MappingContext<? extends GemfirePersistentEntity<?>, GemfirePersistentProperty> resolveGemfireMappingContext() {
MappingContext<? extends GemfirePersistentEntity<?>, GemfirePersistentProperty> mappingContext =
getGemfireMappingContext();
Assert.state(mappingContext != null, "GemfireMappingContext must not be null");
return mappingContext;
}
/**
* Returns an {@link Iterable} reference to the {@link Region Regions}
* defined in the Spring {@link ApplicationContext}.
*
* @return a reference to all {@link Region Regions} defined in the Spring {@link ApplicationContext}.
* @see org.apache.geode.cache.Region
* @see java.lang.Iterable
*/
protected Iterable<Region<?, ?>> getRegions() {
return this.regions;
@@ -123,7 +180,15 @@ public class GemfireRepositoryFactoryBean<T extends Repository<S, ID>, S, ID ext
*/
@Override
protected RepositoryFactorySupport createRepositoryFactory() {
return new GemfireRepositoryFactory(getRegions(), getGemfireMappingContext());
GemfireRepositoryFactory repositoryFactory =
new GemfireRepositoryFactory(getRegions(), getGemfireMappingContext());
getApplicationContext()
.map(applicationContext -> new QueryPostProcessorRegistrationOnQueryCreationListener(applicationContext))
.ifPresent(repositoryFactory::addQueryCreationListener);
return repositoryFactory;
}
/*
@@ -132,7 +197,108 @@ public class GemfireRepositoryFactoryBean<T extends Repository<S, ID>, S, ID ext
*/
@Override
public void afterPropertiesSet() {
Assert.state(getGemfireMappingContext() != null, "GemfireMappingContext must not be null");
resolveGemfireMappingContext();
super.afterPropertiesSet();
}
protected class QueryPostProcessorRegistrationOnQueryCreationListener
implements QueryCreationListener<GemfireRepositoryQuery> {
private Iterable<QueryPostProcessorMetadata> queryPostProcessorsMetadata;
public QueryPostProcessorRegistrationOnQueryCreationListener(ApplicationContext applicationContext) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
List<QueryPostProcessor> queryPostProcessors =
new ArrayList<>(applicationContext.getBeansOfType(QueryPostProcessor.class).values());
queryPostProcessors.sort(OrderComparator.INSTANCE);
this.queryPostProcessorsMetadata = queryPostProcessors.stream()
.map(QueryPostProcessorMetadata::from)
.collect(Collectors.toList());
}
protected Iterable<QueryPostProcessorMetadata> getQueryPostProcessorsMetadata() {
return this.queryPostProcessorsMetadata;
}
@Override
public void onCreation(GemfireRepositoryQuery repositoryQuery) {
Class<?> repositoryInterface = getRepositoryInformation().getRepositoryInterface();
StreamSupport.stream(getQueryPostProcessorsMetadata().spliterator(), false)
.filter(queryPostProcessorMetadata -> queryPostProcessorMetadata.isMatch(repositoryInterface))
.forEach(queryPostProcessorMetadata -> queryPostProcessorMetadata.register(repositoryQuery));
}
}
static class QueryPostProcessorMetadata {
private static final Map<QueryPostProcessorKey, QueryPostProcessorMetadata> cache = new WeakHashMap<>();
private final Class<?> declaredRepositoryType;
private final QueryPostProcessor<?, ?> queryPostProcessor;
static QueryPostProcessorMetadata from(@NonNull QueryPostProcessor<?, ?> queryPostProcessor) {
return cache.computeIfAbsent(QueryPostProcessorKey.of(queryPostProcessor),
key -> new QueryPostProcessorMetadata(key.getQueryPostProcessor()));
}
@SuppressWarnings("unchecked")
QueryPostProcessorMetadata(@NonNull QueryPostProcessor<?, ?> queryPostProcessor) {
Assert.notNull(queryPostProcessor, "QueryPostProcessor must not be null");
this.queryPostProcessor = queryPostProcessor;
List<TypeInformation<?>> typeArguments = ClassTypeInformation.from(queryPostProcessor.getClass())
.getRequiredSuperTypeInformation(QueryPostProcessor.class)
.getTypeArguments();
this.declaredRepositoryType = Optional.of(typeArguments)
.filter(list -> !list.isEmpty())
.map(list -> list.get(0))
.map(typeInfo -> typeInfo.getType())
.orElse((Class) Repository.class);
}
@NonNull
Class<?> getDeclaredRepositoryType() {
return this.declaredRepositoryType;
}
@NonNull
@SuppressWarnings("unchecked")
QueryPostProcessor<?, String> getQueryPostProcessor() {
return (QueryPostProcessor<?, String>) this.queryPostProcessor;
}
boolean isMatch(Class<?> repositoryInterface) {
return repositoryInterface != null
&& (getDeclaredRepositoryType().isAssignableFrom(repositoryInterface)
|| repositoryInterface.isAnnotationPresent(RepositoryDefinition.class));
}
GemfireRepositoryQuery register(GemfireRepositoryQuery repositoryQuery) {
repositoryQuery.register(getQueryPostProcessor());
return repositoryQuery;
}
@EqualsAndHashCode
@RequiredArgsConstructor(staticName = "of")
private static class QueryPostProcessorKey {
@lombok.NonNull @Getter
QueryPostProcessor<?, ?> queryPostProcessor;
}
}
}

View File

@@ -66,6 +66,7 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
* @param entityInformation must not be {@literal null}.
*/
public SimpleGemfireRepository(GemfireTemplate template, EntityInformation<T, ID> entityInformation) {
Assert.notNull(template, "Template must not be null");
Assert.notNull(entityInformation, "EntityInformation must not be null");
@@ -79,9 +80,10 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public <U extends T> U save(U entity) {
ID id = entityInformation.getRequiredId(entity);
template.put(id, entity);
ID id = this.entityInformation.getRequiredId(entity);
this.template.put(id, entity);
return entity;
}
@@ -92,11 +94,12 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public <U extends T> Iterable<U> saveAll(Iterable<U> entities) {
Map<ID, U> entitiesToSave = new HashMap<>();
entities.forEach(entity -> entitiesToSave.put(entityInformation.getRequiredId(entity), entity));
entities.forEach(entity -> entitiesToSave.put(this.entityInformation.getRequiredId(entity), entity));
template.putAll(entitiesToSave);
this.template.putAll(entitiesToSave);
return entitiesToSave.values();
}
@@ -107,9 +110,10 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public T save(Wrapper<T, ID> wrapper) {
T entity = wrapper.getEntity();
template.put(wrapper.getKey(), entity);
this.template.put(wrapper.getKey(), entity);
return entity;
}
@@ -120,8 +124,9 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public long count() {
SelectResults<Integer> results =
template.find(String.format("SELECT count(*) FROM %s", template.getRegion().getFullPath()));
this.template.find(String.format("SELECT count(*) FROM %s", this.template.getRegion().getFullPath()));
return Long.valueOf(results.iterator().next());
}
@@ -141,7 +146,7 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public Optional<T> findById(ID id) {
return Optional.ofNullable(template.get(id));
return Optional.ofNullable(this.template.get(id));
}
/*
@@ -150,8 +155,9 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public Collection<T> findAll() {
SelectResults<T> results =
template.find(String.format("SELECT * FROM %s", template.getRegion().getFullPath()));
this.template.find(String.format("SELECT * FROM %s", this.template.getRegion().getFullPath()));
return results.asList();
}
@@ -162,11 +168,12 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public Iterable<T> findAll(Sort sort) {
QueryString query = new QueryString("SELECT * FROM /RegionPlaceholder")
.forRegion(entityInformation.getJavaType(), template.getRegion())
QueryString query = QueryString.of("SELECT * FROM /RegionPlaceholder")
.fromRegion(this.entityInformation.getJavaType(), this.template.getRegion())
.orderBy(sort);
SelectResults<T> selectResults = template.find(query.toString());
SelectResults<T> selectResults = this.template.find(query.toString());
return selectResults.asList();
}
@@ -177,9 +184,10 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public Collection<T> findAllById(Iterable<ID> ids) {
List<ID> keys = Streamable.of(ids).stream().collect(StreamUtils.toUnmodifiableList());
return CollectionUtils.<ID, T>nullSafeMap(template.getAll(keys)).values().stream()
return CollectionUtils.<ID, T>nullSafeMap(this.template.getAll(keys)).values().stream()
.filter(Objects::nonNull).collect(Collectors.toList());
}
@@ -189,7 +197,7 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public void deleteById(ID id) {
template.remove(id);
this.template.remove(id);
}
/*
@@ -198,7 +206,7 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public void delete(T entity) {
deleteById(entityInformation.getRequiredId(entity));
deleteById(this.entityInformation.getRequiredId(entity));
}
/*
@@ -216,8 +224,9 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
* @see org.apache.geode.cache.RegionAttributes#getDataPolicy()
*/
boolean isPartitioned(Region<?, ?> region) {
return (region != null && region.getAttributes() != null
&& isPartitioned(region.getAttributes().getDataPolicy()));
return region != null && region.getAttributes() != null
&& isPartitioned(region.getAttributes().getDataPolicy());
}
/*
@@ -225,7 +234,7 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
* @see org.apache.geode.cache.DataPolicy#withPartitioning()
*/
boolean isPartitioned(DataPolicy dataPolicy) {
return (dataPolicy != null && dataPolicy.withPartitioning());
return dataPolicy != null && dataPolicy.withPartitioning();
}
/*
@@ -234,8 +243,9 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
* @see org.apache.geode.cache.Cache#getCacheTransactionManager()
*/
boolean isTransactionPresent(Region<?, ?> region) {
return (region.getRegionService() instanceof Cache
&& isTransactionPresent(((Cache) region.getRegionService()).getCacheTransactionManager()));
return region.getRegionService() instanceof Cache
&& isTransactionPresent(((Cache) region.getRegionService()).getCacheTransactionManager());
}
/*
@@ -243,7 +253,7 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
* @see org.apache.geode.cache.CacheTransactionManager#exists()
*/
boolean isTransactionPresent(CacheTransactionManager cacheTransactionManager) {
return (cacheTransactionManager != null && cacheTransactionManager.exists());
return cacheTransactionManager != null && cacheTransactionManager.exists();
}
/* (non-Javadoc) */
@@ -257,7 +267,8 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public void deleteAll() {
template.execute((GemfireCallback<Void>) region -> {
this.template.execute((GemfireCallback<Void>) region -> {
if (isPartitioned(region) || isTransactionPresent(region)) {
doRegionClear(region);
}

View File

@@ -20,6 +20,7 @@ import static java.util.stream.StreamSupport.stream;
import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
@@ -29,12 +30,16 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* The CollectionUtils class is a utility class for working with Java Collections Framework and classes.
* {@link CollectionUtils} is a utility class for working with the Java Collections Framework and classes.
*
* @author John Blum
* @see java.util.Collection
@@ -62,7 +67,7 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio
* @see java.lang.Iterable
* @see java.util.Collection
*/
public static <E, T extends Collection<E>> T addAll(T collection, Iterable<E> iterable) {
public static <E, T extends Collection<E>> T addAll(@NonNull T collection, @Nullable Iterable<E> iterable) {
Assert.notNull(collection, "Collection is required");
@@ -79,7 +84,7 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio
* @return an unmodifiable {@link Set} containing the elements from the given object array.
*/
@SafeVarargs
public static <T> Set<T> asSet(T... elements) {
public static <T> Set<T> asSet(@NonNull T... elements) {
Set<T> set = new HashSet<>(elements.length);
@@ -96,17 +101,10 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio
* @return a boolean value indicating whether the collection contains at least 1 element from the given array.
* @see java.util.Collection#contains(Object)
*/
public static boolean containsAny(Collection<?> collection, Object... elements) {
public static boolean containsAny(@Nullable Collection<?> collection, @Nullable Object... elements) {
if (collection != null) {
for (Object element : nullSafeArray(elements, Object.class)) {
if (collection.contains(element)) {
return true;
}
}
}
return false;
return Arrays.asList(nullSafeArray(elements, Object.class)).stream()
.anyMatch(element -> nullSafeCollection(collection).contains(element));
}
/**
@@ -117,8 +115,9 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio
* @see java.lang.Iterable
* @see #nullSafeIterable(Iterable)
*/
@NonNull
public static <T> Iterable<T> emptyIterable() {
return nullSafeIterable(null);
return Collections::emptyIterator;
}
/**
@@ -130,8 +129,9 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio
* @see java.lang.Iterable
* @see java.util.Enumeration
*/
public static <T> Iterable<T> iterable(Enumeration<T> enumeration) {
return () -> toIterator(enumeration);
@NonNull
public static <T> Iterable<T> iterable(@Nullable Enumeration<T> enumeration) {
return () -> toIterator(nullSafeEnumeration(enumeration));
}
/**
@@ -143,8 +143,9 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio
* @see java.lang.Iterable
* @see java.util.Iterator
*/
public static <T> Iterable<T> iterable(Iterator<T> iterator) {
return () -> iterator;
@NonNull
public static <T> Iterable<T> iterable(@Nullable Iterator<T> iterator) {
return () -> nullSafeIterator(iterator);
}
/**
@@ -158,10 +159,27 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio
* @see java.util.Collections#emptyList()
* @see java.util.Collection
*/
public static <T> Collection<T> nullSafeCollection(Collection<T> collection) {
@NonNull
public static <T> Collection<T> nullSafeCollection(@Nullable Collection<T> collection) {
return collection != null ? collection : Collections.emptyList();
}
/**
* Null-safe operation returning the given {@link Enumeration} if not {@literal null}
* or an {@link Collections#emptyEnumeration() empty Enumeration} if {@literal null}.
*
* @param <T> {@link Class type} of elements contained in the {@link Enumeration}.
* @param enumeration {@link Enumeration} to evaluate.
* @return the given {@link Enumeration} if not {@literal null}
* or an {@link Collections#emptyEnumeration() empty Enumeration}.
* @see java.util.Collections#emptyEnumeration()
* @see java.util.Enumeration
*/
@NonNull
public static <T> Enumeration<T> nullSafeEnumeration(@Nullable Enumeration<T> enumeration) {
return enumeration != null ? enumeration : Collections.emptyEnumeration();
}
/**
* A null-safe operation returning the original Iterable object if non-null or a default, empty Iterable
* implementation if null.
@@ -169,11 +187,12 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio
* @param <T> the class type of the iterable elements.
* @param iterable the Iterable object evaluated for a null reference.
* @return the Iterable object if not null or a default, empty Iterable implementation otherwise.
* @see #emptyIterable()
* @see java.lang.Iterable
* @see java.util.Iterator
*/
public static <T> Iterable<T> nullSafeIterable(Iterable<T> iterable) {
return iterable != null ? iterable : Collections::emptyIterator;
@NonNull
public static <T> Iterable<T> nullSafeIterable(@Nullable Iterable<T> iterable) {
return iterable != null ? iterable : emptyIterable();
}
/**
@@ -186,10 +205,27 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio
* @return {@code iterable} if not {@literal null} or empty otherwise return {@code defaultIterable}.
* @see java.lang.Iterable
*/
public static <E, T extends Iterable<E>> T nullSafeIterable(T iterable, T defaultIterable) {
@Nullable
public static <E, T extends Iterable<E>> T nullSafeIterable(@Nullable T iterable, @Nullable T defaultIterable) {
return Optional.ofNullable(iterable).filter(it -> it.iterator().hasNext()).orElse(defaultIterable);
}
/**
* Null-safe operation returning the given {@link Iterator} if not {@literal null}
* or an {@link Collections#emptyIterator() empty Iterator} if {@literal null}.
*
* @param <T> {@link Class type} of elements contained in the {@link Iterator}.
* @param iterator {@link Iterator} to evaluate.
* @return the given {@link Iterator} if not {@literal null}
* or an {@link Collections#emptyIterator() empty Iterator}.
* @see java.util.Collections#emptyIterator()
* @see java.util.Iterator
*/
@NonNull
public static <T> Iterator<T> nullSafeIterator(@Nullable Iterator<T> iterator) {
return iterator != null ? iterator : Collections.emptyIterator();
}
/**
* Null-safe operation returning the given {@link List} if not {@literal null}
* or an empty {@link List} if {@literal null}.
@@ -200,7 +236,8 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio
* @see java.util.Collections#emptyList()
* @see java.util.List
*/
public static <T> List<T> nullSafeList(List<T> list) {
@NonNull
public static <T> List<T> nullSafeList(@Nullable List<T> list) {
return list != null ? list : Collections.emptyList();
}
@@ -216,7 +253,8 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio
* @see java.util.Map
*/
@SuppressWarnings("all")
public static <K, V> Map<K, V> nullSafeMap(Map<K, V> map) {
@NonNull
public static <K, V> Map<K, V> nullSafeMap(@Nullable Map<K, V> map) {
return map != null ? map : Collections.<K, V>emptyMap();
}
@@ -230,10 +268,59 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio
* @see java.util.Collections#emptySet()
* @see java.util.Set
*/
public static <T> Set<T> nullSafeSet(Set<T> set) {
@NonNull
public static <T> Set<T> nullSafeSet(@Nullable Set<T> set) {
return set != null ? set : Collections.emptySet();
}
/**
* Determines whether the given {@link Collection} is {@link Collection#isEmpty() empty}.
*
* @param collection {@link Collection} to evaluate.
* @return a boolean value indicating whether the given {@link Collection} is {@link Collection#isEmpty() empty}.
* @see #nullSafeCollection(Collection)
* @see java.util.Collection#isEmpty()
*/
public static boolean nullSafeIsEmpty(@Nullable Collection<?> collection) {
return nullSafeCollection(collection).isEmpty();
}
/**
* Determines whether the given {@link Map} is {@link Map#isEmpty() empty}.
*
* @param map {@link Map} to evaluate.
* @return a boolean value indicating whether the given {@link Map} is {@link Map#isEmpty() empty}.
* @see #nullSafeMap(Map)
* @see java.util.Map#isEmpty()
*/
public static boolean nullSafeIsEmpty(@Nullable Map<?, ?> map) {
return nullSafeMap(map).isEmpty();
}
/**
* Determines the {@link Collection#size()} of the given {@link Collection}.
*
* @param collection {@link Collection} to evaluate.
* @return the {@link Collection#size()} of the given {@link Collection}.
* @see #nullSafeCollection(Collection)
* @see java.util.Collection#size()
*/
public static int nullSafeSize(@Nullable Collection<?> collection) {
return nullSafeCollection(collection).size();
}
/**
* Determines the {@link Map#size()} of the given {@link Map}.
*
* @param map {@link Map} to evaluate.
* @return the {@link Map#size()} of the given {@link Map}.
* @see #nullSafeMap(Map)
* @see java.util.Map#size()
*/
public static int nullSafeSize(@Nullable Map<?, ?> map) {
return nullSafeMap(map).size();
}
/**
* Sors the elements of the given {@link List} by their natural, {@link Comparable} ordering.
*
@@ -243,7 +330,9 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio
* @see java.util.Collections#sort(List)
* @see java.util.List
*/
public static <T extends Comparable<T>> List<T> sort(List<T> list) {
public static <T extends Comparable<T>> List<T> sort(@NonNull List<T> list) {
Assert.notNull(list, "List is required");
Collections.sort(list);
@@ -263,7 +352,7 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio
* @throws NullPointerException if either the list or indexes are null.
* @see java.util.List
*/
public static <T> List<T> subList(List<T> source, int... indices) {
public static <T> List<T> subList(@NonNull List<T> source, int... indices) {
Assert.notNull(source, "List is required");
@@ -276,23 +365,36 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio
return result;
}
/* (non-Javadoc) */
public static String toString(Map<?, ?> map) {
/**
* Returns a {@link String} representation of the given {@link Map}.
*
* @param map {@link Map} represent as a {@link String}.
* @return a {@link String} describing the given {@link Map}.
* @see #newSortedMap(Map)
* @see java.util.Map
*/
@NonNull
public static String toString(@Nullable Map<?, ?> map) {
StringBuilder builder = new StringBuilder("{\n");
int count = 0;
AtomicInteger count = new AtomicInteger(0);
for (Map.Entry<?, ?> entry : new TreeMap<>(map).entrySet()) {
builder.append(++count > 1 ? ",\n" : "");
newSortedMap(map).forEach((key, value) -> {
builder.append(count.incrementAndGet() > 1 ? ",\n" : "");
builder.append("\t");
builder.append(entry.getKey());
builder.append(key);
builder.append(" = ");
builder.append(entry.getValue());
}
builder.append(value);
});
builder.append("\n}");
return builder.toString();
}
@NonNull
private static SortedMap<?, ?> newSortedMap(@Nullable Map<?, ?> map) {
return new TreeMap<>(nullSafeMap(map));
}
}