persons = repository.findAll(Sort.by(new Order(ASC, "lastname")));
+
+ // ...
+ }
+}
+----
+====
diff --git a/src/main/java/org/springframework/data/elasticsearch/NoSuchIndexException.java b/src/main/java/org/springframework/data/elasticsearch/NoSuchIndexException.java
new file mode 100644
index 000000000..d7ff760ef
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/NoSuchIndexException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch;
+
+import org.springframework.dao.NonTransientDataAccessResourceException;
+
+/**
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+public class NoSuchIndexException extends NonTransientDataAccessResourceException {
+
+ private final String index;
+
+ public NoSuchIndexException(String index, Throwable cause) {
+ super(String.format("Index %s not found.", index), cause);
+ this.index = index;
+ }
+
+ public String getIndex() {
+ return index;
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java
index 99c46d7ab..02328d823 100644
--- a/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java
+++ b/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java
@@ -42,6 +42,22 @@ public interface ClientConfiguration {
return new ClientConfigurationBuilder();
}
+ /**
+ * Creates a new {@link ClientConfiguration} instance configured to localhost.
+ *
+ *
+ *
+ * // "localhost:9200"
+ * ClientConfiguration configuration = ClientConfiguration.localhost();
+ *
+ *
+ * @return a new {@link ClientConfiguration} instance
+ * @see ClientConfigurationBuilder#connectedToLocalhost()
+ */
+ static ClientConfiguration localhost() {
+ return new ClientConfigurationBuilder().connectedToLocalhost().build();
+ }
+
/**
* Creates a new {@link ClientConfiguration} instance configured to a single host given {@code hostAndPort}.
*
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/reactive/DefaultReactiveElasticsearchClient.java b/src/main/java/org/springframework/data/elasticsearch/client/reactive/DefaultReactiveElasticsearchClient.java
index a734ef26b..c19cf11d7 100644
--- a/src/main/java/org/springframework/data/elasticsearch/client/reactive/DefaultReactiveElasticsearchClient.java
+++ b/src/main/java/org/springframework/data/elasticsearch/client/reactive/DefaultReactiveElasticsearchClient.java
@@ -545,7 +545,7 @@ public class DefaultReactiveElasticsearchClient implements ReactiveElasticsearch
return Mono.justOrEmpty(responseType
.cast(ReflectionUtils.invokeMethod(fromXContent, responseType, createParser(mediaType, content))));
- } catch (Exception errorParseFailure) {
+ } catch (Throwable errorParseFailure) { // cause elasticsearch also uses AssertionError
try {
return Mono.error(BytesRestResponse.errorFromXContent(createParser(mediaType, content)));
diff --git a/src/main/java/org/springframework/data/elasticsearch/config/AbstractReactiveElasticsearchConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/config/AbstractReactiveElasticsearchConfiguration.java
index 178b454d9..411cb740f 100644
--- a/src/main/java/org/springframework/data/elasticsearch/config/AbstractReactiveElasticsearchConfiguration.java
+++ b/src/main/java/org/springframework/data/elasticsearch/config/AbstractReactiveElasticsearchConfiguration.java
@@ -47,7 +47,7 @@ public abstract class AbstractReactiveElasticsearchConfiguration extends Elastic
* @return never {@literal null}.
*/
@Bean
- public ReactiveElasticsearchOperations reactiveElasticsearchOperations() {
+ public ReactiveElasticsearchOperations reactiveElasticsearchTemplate() {
ReactiveElasticsearchTemplate template = new ReactiveElasticsearchTemplate(reactiveElasticsearchClient(),
elasticsearchConverter());
diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchExceptionTranslator.java b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchExceptionTranslator.java
index fddf3f1a2..8ce6fa06c 100644
--- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchExceptionTranslator.java
+++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchExceptionTranslator.java
@@ -22,6 +22,8 @@ import org.elasticsearch.ElasticsearchException;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
+import org.springframework.data.elasticsearch.NoSuchIndexException;
+import org.springframework.util.CollectionUtils;
/**
* @author Christoph Strobl
@@ -33,15 +35,22 @@ public class ElasticsearchExceptionTranslator implements PersistenceExceptionTra
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
if (ex instanceof ElasticsearchException) {
- // TODO: exception translation
- ElasticsearchException elasticsearchExption = (ElasticsearchException) ex;
-// elasticsearchExption.get
+
+ ElasticsearchException elasticsearchException = (ElasticsearchException) ex;
+
+ if (!indexAvailable(elasticsearchException)) {
+ return new NoSuchIndexException(elasticsearchException.getMetadata("es.index").toString(), ex);
+ }
}
- if(ex.getCause() instanceof ConnectException) {
+ if (ex.getCause() instanceof ConnectException) {
return new DataAccessResourceFailureException(ex.getMessage(), ex);
}
return null;
}
+
+ private boolean indexAvailable(ElasticsearchException ex) {
+ return !CollectionUtils.contains(ex.getMetadata("es.index_uuid").iterator(), "_na_");
+ }
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperations.java
index 0f42cd284..fbe2ec9c6 100644
--- a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperations.java
+++ b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperations.java
@@ -22,6 +22,7 @@ import org.elasticsearch.index.query.QueryBuilders;
import org.reactivestreams.Publisher;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient;
+import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.lang.Nullable;
@@ -451,6 +452,13 @@ public interface ReactiveElasticsearchOperations {
*/
Mono deleteBy(Query query, Class> entityType, @Nullable String index, @Nullable String type);
+ /**
+ * Get the {@link ElasticsearchConverter} used.
+ *
+ * @return never {@literal null}
+ */
+ ElasticsearchConverter getElasticsearchConverter();
+
/**
* Callback interface to be used with {@link #execute(ClientCallback)} for operating directly on
* {@link ReactiveElasticsearchClient}.
diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java
index e20a735cc..7734cf2d7 100644
--- a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java
+++ b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java
@@ -52,6 +52,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
+import org.springframework.data.elasticsearch.NoSuchIndexException;
import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient;
import org.springframework.data.elasticsearch.core.EntityOperations.AdaptibleEntity;
import org.springframework.data.elasticsearch.core.EntityOperations.Entity;
@@ -62,6 +63,7 @@ import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersiste
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
+import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.data.elasticsearch.core.query.StringQuery;
@@ -69,7 +71,6 @@ import org.springframework.data.mapping.context.MappingContext;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
-import org.springframework.util.ObjectUtils;
/**
* @author Christoph Strobl
@@ -141,7 +142,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
IndexCoordinates indexCoordinates = operations.determineIndex(entity, index, type);
IndexRequest request = id != null
- ? new IndexRequest(indexCoordinates.getIndexName(), indexCoordinates.getTypeName(), id.toString())
+ ? new IndexRequest(indexCoordinates.getIndexName(), indexCoordinates.getTypeName(), converter.convertId(id))
: new IndexRequest(indexCoordinates.getIndexName(), indexCoordinates.getTypeName());
try {
@@ -163,7 +164,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
Object parentId = entity.getParentId();
if (parentId != null) {
- request.parent(parentId.toString());
+ request.parent(converter.convertId(parentId));
}
}
@@ -307,7 +308,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
Entity> elasticsearchEntity = operations.forEntity(entity);
- return Mono.defer(() -> doDeleteById(entity, ObjectUtils.nullSafeToString(elasticsearchEntity.getId()),
+ return Mono.defer(() -> doDeleteById(entity, converter.convertId(elasticsearchEntity.getId()),
elasticsearchEntity.getPersistentEntity(), index, type));
}
@@ -384,6 +385,15 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
this.indicesOptions = indicesOptions;
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations#getElasticsearchConverter()
+ */
+ @Override
+ public ElasticsearchConverter getElasticsearchConverter() {
+ return converter;
+ }
+
// Customization Hooks
/**
@@ -491,7 +501,9 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
* @return a {@link Mono} emitting the result of the operation.
*/
protected Mono doFindById(GetRequest request) {
- return Mono.from(execute(client -> client.get(request)));
+
+ return Mono.from(execute(client -> client.get(request))) //
+ .onErrorResume(NoSuchIndexException.class, it -> Mono.empty());
}
/**
@@ -501,7 +513,9 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
* @return a {@link Mono} emitting the result of the operation.
*/
protected Mono doExists(GetRequest request) {
- return Mono.from(execute(client -> client.exists(request)));
+
+ return Mono.from(execute(client -> client.exists(request))) //
+ .onErrorReturn(NoSuchIndexException.class, false);
}
/**
@@ -516,7 +530,8 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
QUERY_LOGGER.debug("Executing doFind: {}", request);
}
- return Flux.from(execute(client -> client.search(request)));
+ return Flux.from(execute(client -> client.search(request))) //
+ .onErrorResume(NoSuchIndexException.class, it -> Mono.empty());
}
/**
@@ -531,7 +546,8 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
QUERY_LOGGER.debug("Executing doScan: {}", request);
}
- return Flux.from(execute(client -> client.scroll(request)));
+ return Flux.from(execute(client -> client.scroll(request))) //
+ .onErrorResume(NoSuchIndexException.class, it -> Mono.empty());
}
/**
@@ -551,7 +567,8 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
}
return Mono.just(it.getId());
- });
+ }) //
+ .onErrorResume(NoSuchIndexException.class, it -> Mono.empty());
}
/**
@@ -561,7 +578,9 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
* @return a {@link Mono} emitting the result of the operation.
*/
protected Mono doDeleteBy(DeleteByQueryRequest request) {
- return Mono.from(execute(client -> client.deleteBy(request)));
+
+ return Mono.from(execute(client -> client.deleteBy(request))) //
+ .onErrorResume(NoSuchIndexException.class, it -> Mono.empty());
}
// private helpers
@@ -621,7 +640,11 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
elasticsearchQuery = new CriteriaQueryProcessor().createQueryFromCriteria(((CriteriaQuery) query).getCriteria());
} else if (query instanceof StringQuery) {
elasticsearchQuery = new WrapperQueryBuilder(((StringQuery) query).getSource());
- } else {
+ } else if (query instanceof NativeSearchQuery) {
+ elasticsearchQuery = ((NativeSearchQuery) query).getQuery();
+ }
+
+ else {
throw new IllegalArgumentException(String.format("Unknown query type '%s'.", query.getClass()));
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchConverter.java
index 422e4a8a7..f7106164b 100644
--- a/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchConverter.java
+++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchConverter.java
@@ -19,14 +19,15 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.util.Assert;
/**
* ElasticsearchConverter
*
* @author Rizwan Idrees
* @author Mohsin Husen
+ * @author Christoph Strobl
*/
-
public interface ElasticsearchConverter {
/**
@@ -42,4 +43,22 @@ public interface ElasticsearchConverter {
* @return never {@literal null}.
*/
ConversionService getConversionService();
+
+ /**
+ * Convert a given {@literal idValue} to its {@link String} representation taking potentially registered
+ * {@link org.springframework.core.convert.converter.Converter Converters} into account.
+ *
+ * @param idValue must not be {@literal null}.
+ * @return never {@literal null}.
+ * @since 3.2
+ */
+ default String convertId(Object idValue) {
+
+ Assert.notNull(idValue, "idValue must not be null!");
+ if (!getConversionService().canConvert(idValue.getClass(), String.class)) {
+ return idValue.toString();
+ }
+
+ return getConversionService().convert(idValue, String.class);
+ }
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java
index 39a2fd479..e7da961ae 100644
--- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java
+++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java
@@ -100,14 +100,24 @@ public class SimpleElasticsearchPersistentEntity extends BasicPersistentEntit
@Override
public String getIndexName() {
- Expression expression = parser.parseExpression(indexName, ParserContext.TEMPLATE_EXPRESSION);
- return expression.getValue(context, String.class);
+
+ if(indexName != null) {
+ Expression expression = parser.parseExpression(indexName, ParserContext.TEMPLATE_EXPRESSION);
+ return expression.getValue(context, String.class);
+ }
+
+ return getTypeInformation().getType().getSimpleName();
}
@Override
public String getIndexType() {
- Expression expression = parser.parseExpression(indexType, ParserContext.TEMPLATE_EXPRESSION);
- return expression.getValue(context, String.class);
+
+ if(indexType != null) {
+ Expression expression = parser.parseExpression(indexType, ParserContext.TEMPLATE_EXPRESSION);
+ return expression.getValue(context, String.class);
+ }
+
+ return "";
}
@Override
diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java
index 173edf94d..23cb344b2 100644
--- a/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java
+++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java
@@ -15,10 +15,13 @@
*/
package org.springframework.data.elasticsearch.core.query;
+import java.util.Arrays;
import java.util.Collection;
import java.util.List;
+
import org.elasticsearch.action.search.SearchType;
import org.elasticsearch.action.support.IndicesOptions;
+import org.elasticsearch.index.query.QueryBuilders;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@@ -31,12 +34,24 @@ import org.springframework.data.domain.Sort;
* @author Mark Paluch
* @author Alen Turkovic
* @author Sascha Woo
+ * @author Christoph Strobl
*/
public interface Query {
int DEFAULT_PAGE_SIZE = 10;
Pageable DEFAULT_PAGE = PageRequest.of(0, DEFAULT_PAGE_SIZE);
+ /**
+ * Get get a {@link Query} that matches all documents in the index.
+ *
+ * @return new instance of {@link Query}.
+ * @since 3.2
+ * @see QueryBuilders#matchAllQuery()
+ */
+ static Query findAll() {
+ return new StringQuery(QueryBuilders.matchAllQuery().toString());
+ }
+
/**
* restrict result to entries on given page. Corresponds to the 'start' and 'rows' parameter in elasticsearch
*
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/ReactiveElasticsearchRepository.java b/src/main/java/org/springframework/data/elasticsearch/repository/ReactiveElasticsearchRepository.java
new file mode 100644
index 000000000..7187f3df6
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/ReactiveElasticsearchRepository.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository;
+
+import org.springframework.data.repository.NoRepositoryBean;
+import org.springframework.data.repository.reactive.ReactiveSortingRepository;
+
+/**
+ * Elasticsearch specific {@link org.springframework.data.repository.Repository} interface with reactive support.
+ *
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+@NoRepositoryBean
+public interface ReactiveElasticsearchRepository extends ReactiveSortingRepository {
+
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/config/ElasticsearchRepositoryConfigExtension.java b/src/main/java/org/springframework/data/elasticsearch/repository/config/ElasticsearchRepositoryConfigExtension.java
index 824f20a9a..687df57b5 100644
--- a/src/main/java/org/springframework/data/elasticsearch/repository/config/ElasticsearchRepositoryConfigExtension.java
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/config/ElasticsearchRepositoryConfigExtension.java
@@ -29,6 +29,7 @@ import org.springframework.data.elasticsearch.repository.support.ElasticsearchRe
import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource;
import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
import org.springframework.data.repository.config.XmlRepositoryConfigurationSource;
+import org.springframework.data.repository.core.RepositoryMetadata;
import org.w3c.dom.Element;
/**
@@ -39,6 +40,7 @@ import org.w3c.dom.Element;
* @author Rizwan Idrees
* @author Mohsin Husen
* @author Mark Paluch
+ * @author Christoph Strobl
*/
public class ElasticsearchRepositoryConfigExtension extends RepositoryConfigurationExtensionSupport {
@@ -88,7 +90,7 @@ public class ElasticsearchRepositoryConfigExtension extends RepositoryConfigurat
*/
@Override
protected Collection> getIdentifyingAnnotations() {
- return Collections.> singleton(Document.class);
+ return Collections.singleton(Document.class);
}
/*
@@ -97,6 +99,15 @@ public class ElasticsearchRepositoryConfigExtension extends RepositoryConfigurat
*/
@Override
protected Collection> getIdentifyingTypes() {
- return Arrays.> asList(ElasticsearchRepository.class, ElasticsearchCrudRepository.class);
+ return Arrays.asList(ElasticsearchRepository.class, ElasticsearchCrudRepository.class);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#useRepositoryConfiguration(org.springframework.data.repository.core.RepositoryMetadata)
+ */
+ @Override
+ protected boolean useRepositoryConfiguration(RepositoryMetadata metadata) {
+ return !metadata.isReactiveRepository();
}
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/config/EnableReactiveElasticsearchRepositories.java b/src/main/java/org/springframework/data/elasticsearch/repository/config/EnableReactiveElasticsearchRepositories.java
new file mode 100644
index 000000000..5590b73bf
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/config/EnableReactiveElasticsearchRepositories.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.context.annotation.ComponentScan.Filter;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.elasticsearch.repository.support.ReactiveElasticsearchRepositoryFactoryBean;
+import org.springframework.data.repository.config.DefaultRepositoryBaseClass;
+import org.springframework.data.repository.query.QueryLookupStrategy;
+import org.springframework.data.repository.query.QueryLookupStrategy.Key;
+
+/**
+ * Annotation to activate reactive Elasticsearch repositories. If no base package is configured through either
+ * {@link #value()}, {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of
+ * annotated class.
+ *
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+@Import(ReactiveElasticsearchRepositoriesRegistrar.class)
+public @interface EnableReactiveElasticsearchRepositories {
+
+ /**
+ * Alias for the {@link #basePackages()} attribute.
+ */
+ String[] value() default {};
+
+ /**
+ * Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with) this
+ * attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names.
+ */
+ String[] basePackages() default {};
+
+ /**
+ * Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components. The
+ * package of each class specified will be scanned. Consider creating a special no-op marker class or interface in
+ * each package that serves no purpose other than being referenced by this attribute.
+ */
+ Class>[] basePackageClasses() default {};
+
+ /**
+ * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from
+ * everything in {@link #basePackages()} to everything in the base packages that matches the given filter or filters.
+ */
+ Filter[] includeFilters() default {};
+
+ /**
+ * Specifies which types are not eligible for component scanning.
+ */
+ Filter[] excludeFilters() default {};
+
+ /**
+ * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So
+ * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning
+ * for {@code PersonRepositoryImpl}.
+ *
+ * @return
+ */
+ String repositoryImplementationPostfix() default "Impl";
+
+ /**
+ * Configures the location of where to find the Spring Data named queries properties file. Will default to
+ * {@code META-INF/elasticsearch-named-queries.properties}.
+ *
+ * @return
+ */
+ String namedQueriesLocation() default "";
+
+ /**
+ * Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to
+ * {@link Key#CREATE_IF_NOT_FOUND}.
+ *
+ * @return
+ */
+ Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND;
+
+ /**
+ * Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to
+ * {@link ReactiveElasticsearchRepositoryFactoryBean}.
+ *
+ * @return
+ */
+ Class> repositoryFactoryBeanClass() default ReactiveElasticsearchRepositoryFactoryBean.class;
+
+ /**
+ * Configure the repository base class to be used to create repository proxies for this particular configuration.
+ *
+ * @return
+ */
+ Class> repositoryBaseClass() default DefaultRepositoryBaseClass.class;
+
+ /**
+ * Configures the name of the {@link org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations} bean
+ * to be used with the repositories detected.
+ *
+ * @return
+ */
+ String reactiveElasticsearchTemplateRef() default "reactiveElasticsearchTemplate";
+
+ /**
+ * Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the
+ * repositories infrastructure.
+ */
+ boolean considerNestedRepositories() default false;
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoriesRegistrar.java b/src/main/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoriesRegistrar.java
new file mode 100644
index 000000000..3510d89a0
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoriesRegistrar.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.config;
+
+import java.lang.annotation.Annotation;
+
+import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport;
+import org.springframework.data.repository.config.RepositoryConfigurationExtension;
+
+/**
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+class ReactiveElasticsearchRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport {
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getAnnotation()
+ */
+ @Override
+ protected Class extends Annotation> getAnnotation() {
+ return EnableReactiveElasticsearchRepositories.class;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getExtension()
+ */
+ @Override
+ protected RepositoryConfigurationExtension getExtension() {
+ return new ReactiveElasticsearchRepositoryConfigurationExtension();
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoryConfigurationExtension.java
new file mode 100644
index 000000000..1e4e1f52e
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoryConfigurationExtension.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.config;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.core.annotation.AnnotationAttributes;
+import org.springframework.data.config.ParsingUtils;
+import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository;
+import org.springframework.data.elasticsearch.repository.support.ReactiveElasticsearchRepositoryFactoryBean;
+import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource;
+import org.springframework.data.repository.config.XmlRepositoryConfigurationSource;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.w3c.dom.Element;
+
+/**
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+public class ReactiveElasticsearchRepositoryConfigurationExtension extends ElasticsearchRepositoryConfigExtension {
+
+ private static final String ELASTICSEARCH_TEMPLATE_REF = "reactive-elasticsearch-template-ref";
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getModuleName()
+ */
+ @Override
+ public String getModuleName() {
+ return "Reactive Elasticsearch";
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#getRepositoryFactoryClassName()
+ */
+ public String getRepositoryFactoryClassName() {
+ return ReactiveElasticsearchRepositoryFactoryBean.class.getName();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getIdentifyingTypes()
+ */
+ @Override
+ protected Collection> getIdentifyingTypes() {
+ return Collections.singleton(ReactiveElasticsearchRepository.class);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.XmlRepositoryConfigurationSource)
+ */
+ @Override
+ public void postProcess(BeanDefinitionBuilder builder, XmlRepositoryConfigurationSource config) {
+
+ Element element = config.getElement();
+
+ ParsingUtils.setPropertyReference(builder, element, ELASTICSEARCH_TEMPLATE_REF, "reactiveElasticsearchOperations");
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource)
+ */
+ @Override
+ public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource config) {
+
+ AnnotationAttributes attributes = config.getAttributes();
+
+ builder.addPropertyReference("reactiveElasticsearchOperations",
+ attributes.getString("reactiveElasticsearchTemplateRef"));
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#useRepositoryConfiguration(org.springframework.data.repository.core.RepositoryMetadata)
+ */
+ @Override
+ protected boolean useRepositoryConfiguration(RepositoryMetadata metadata) {
+ return metadata.isReactiveRepository();
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java
new file mode 100644
index 000000000..2853830a2
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.query;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import org.reactivestreams.Publisher;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
+import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
+import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
+import org.springframework.data.elasticsearch.core.query.Query;
+import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchQueryExecution.ResultProcessingConverter;
+import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchQueryExecution.ResultProcessingExecution;
+import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.repository.query.ParameterAccessor;
+import org.springframework.data.repository.query.QueryMethod;
+import org.springframework.data.repository.query.RepositoryQuery;
+import org.springframework.data.repository.query.ResultProcessor;
+
+/**
+ * AbstractElasticsearchRepositoryQuery
+ *
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+abstract class AbstractReactiveElasticsearchRepositoryQuery implements RepositoryQuery {
+
+ private final ReactiveElasticsearchQueryMethod queryMethod;
+ private final ReactiveElasticsearchOperations elasticsearchOperations;
+
+ AbstractReactiveElasticsearchRepositoryQuery(ReactiveElasticsearchQueryMethod queryMethod,
+ ReactiveElasticsearchOperations elasticsearchOperations) {
+
+ this.queryMethod = queryMethod;
+ this.elasticsearchOperations = elasticsearchOperations;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[])
+ */
+ public Object execute(Object[] parameters) {
+
+ return queryMethod.hasReactiveWrapperParameter() ? executeDeferred(parameters)
+ : execute(new ReactiveElasticsearchParametersParameterAccessor(queryMethod, parameters));
+ }
+
+ private Object executeDeferred(Object[] parameters) {
+
+ ReactiveElasticsearchParametersParameterAccessor parameterAccessor = new ReactiveElasticsearchParametersParameterAccessor(
+ queryMethod, parameters);
+
+ if (getQueryMethod().isCollectionQuery()) {
+ return Flux.defer(() -> (Publisher) execute(parameterAccessor));
+ }
+
+ return Mono.defer(() -> (Mono) execute(parameterAccessor));
+ }
+
+ private Object execute(ElasticsearchParameterAccessor parameterAccessor) {
+
+ ResultProcessor processor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor);
+
+ Query query = createQuery(
+ new ConvertingParameterAccessor(elasticsearchOperations.getElasticsearchConverter(), parameterAccessor));
+
+ Class> typeToRead = processor.getReturnedType().getTypeToRead();
+ String indexName = queryMethod.getEntityInformation().getIndexName();
+ String indexTypeName = queryMethod.getEntityInformation().getIndexTypeName();
+
+ ReactiveElasticsearchQueryExecution execution = getExecution(parameterAccessor,
+ new ResultProcessingConverter(processor, elasticsearchOperations));
+
+ return execution.execute(query, processor.getReturnedType().getDomainType(), indexName, indexTypeName, typeToRead);
+ }
+
+ private ReactiveElasticsearchQueryExecution getExecution(ElasticsearchParameterAccessor accessor,
+ Converter resultProcessing) {
+ return new ResultProcessingExecution(getExecutionToWrap(accessor, elasticsearchOperations), resultProcessing);
+ }
+
+ /**
+ * Creates a {@link Query} instance using the given {@link ParameterAccessor}
+ *
+ * @param accessor must not be {@literal null}.
+ * @return
+ */
+ protected abstract Query createQuery(ElasticsearchParameterAccessor accessor);
+
+ private ReactiveElasticsearchQueryExecution getExecutionToWrap(ElasticsearchParameterAccessor accessor,
+ ReactiveElasticsearchOperations operations) {
+
+ if (isDeleteQuery()) {
+ return (q, t, i, it, tt) -> operations.deleteBy(q, t, i, it);
+ } else if (isCountQuery()) {
+ return (q, t, i, it, tt) -> operations.count(q, t, i, it);
+ } else if (isExistsQuery()) {
+ return (q, t, i, it, tt) -> operations.count(q, t, i, it).map(count -> count > 0);
+ } else if (queryMethod.isCollectionQuery()) {
+ return (q, t, i, it, tt) -> operations.find(q.setPageable(accessor.getPageable()), t, i, it, tt);
+ } else {
+ return (q, t, i, it, tt) -> operations.find(q, t, i, it, tt);
+ }
+ }
+
+ abstract boolean isDeleteQuery();
+
+ abstract boolean isCountQuery();
+
+ abstract boolean isExistsQuery();
+
+ abstract boolean isLimiting();
+
+ @Override
+ public QueryMethod getQueryMethod() {
+ return queryMethod;
+ }
+
+ protected ReactiveElasticsearchOperations getElasticsearchOperations() {
+ return elasticsearchOperations;
+ }
+
+ protected MappingContext extends ElasticsearchPersistentEntity>, ElasticsearchPersistentProperty> getMappingContext() {
+ return elasticsearchOperations.getElasticsearchConverter().getMappingContext();
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ConvertingParameterAccessor.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ConvertingParameterAccessor.java
new file mode 100644
index 000000000..1edb3f14b
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ConvertingParameterAccessor.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.query;
+
+import java.util.Iterator;
+import java.util.Optional;
+
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
+import org.springframework.lang.Nullable;
+
+/**
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+public class ConvertingParameterAccessor implements ElasticsearchParameterAccessor {
+
+ private final ElasticsearchConverter converter;
+ private final ElasticsearchParameterAccessor delegate;
+
+ public ConvertingParameterAccessor(ElasticsearchConverter converter, ElasticsearchParameterAccessor delegate) {
+
+ this.converter = converter;
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Object[] getValues() {
+ return delegate.getValues();
+ }
+
+ @Override
+ public Pageable getPageable() {
+ return delegate.getPageable();
+ }
+
+ @Override
+ public Sort getSort() {
+ return delegate.getSort();
+ }
+
+ @Override
+ public Optional> getDynamicProjection() {
+ return delegate.getDynamicProjection();
+ }
+
+ @Override
+ public Object getBindableValue(int index) {
+ return getConvertedValue(delegate.getBindableValue(index));
+ }
+
+ @Override
+ public boolean hasBindableNullValue() {
+ return delegate.hasBindableNullValue();
+ }
+
+ @Override
+ public Iterator iterator() {
+ return delegate.iterator();
+ }
+
+ @Nullable
+ private Object getConvertedValue(Object value) {
+
+ if (value == null) {
+ return "null";
+ }
+
+ if (converter.getConversionService().canConvert(value.getClass(), String.class)) {
+ return converter.getConversionService().convert(value, String.class);
+ }
+
+ return value.toString();
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchEntityMetadata.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchEntityMetadata.java
new file mode 100644
index 000000000..735945de7
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchEntityMetadata.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.query;
+
+import org.springframework.data.repository.core.EntityMetadata;
+
+/**
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+public interface ElasticsearchEntityMetadata extends EntityMetadata {
+
+ String getIndexName();
+
+ String getIndexTypeName();
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameterAccessor.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameterAccessor.java
new file mode 100644
index 000000000..1e8977ca9
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameterAccessor.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.query;
+
+import org.springframework.data.repository.query.ParameterAccessor;
+
+/**
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+public interface ElasticsearchParameterAccessor extends ParameterAccessor {
+
+ /**
+ * Returns the raw parameter values of the underlying query method.
+ *
+ * @return
+ */
+ Object[] getValues();
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameters.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameters.java
new file mode 100644
index 000000000..f141a6998
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameters.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.query;
+
+import java.lang.reflect.Method;
+import java.util.List;
+
+import org.springframework.core.MethodParameter;
+import org.springframework.data.elasticsearch.repository.query.ElasticsearchParameters.ElasticsearchParameter;
+import org.springframework.data.geo.Distance;
+import org.springframework.data.repository.query.Parameter;
+import org.springframework.data.repository.query.Parameters;
+
+/**
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+public class ElasticsearchParameters extends Parameters {
+
+ public ElasticsearchParameters(Method method) {
+ super(method);
+ }
+
+ private ElasticsearchParameters(List parameters) {
+ super(parameters);
+ }
+
+ @Override
+ protected ElasticsearchParameter createParameter(MethodParameter parameter) {
+ return new ElasticsearchParameter(parameter);
+ }
+
+ @Override
+ protected ElasticsearchParameters createFrom(List parameters) {
+ return new ElasticsearchParameters(parameters);
+ }
+
+ /**
+ * Custom {@link Parameter} implementation adding parameters of type {@link Distance} to the special ones.
+ *
+ * @author Christoph Strobl
+ */
+ class ElasticsearchParameter extends Parameter {
+
+ private final MethodParameter parameter;
+
+ /**
+ * Creates a new {@link ElasticsearchParameter}.
+ *
+ * @param parameter must not be {@literal null}.
+ */
+ ElasticsearchParameter(MethodParameter parameter) {
+
+ super(parameter);
+ this.parameter = parameter;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.query.Parameter#isSpecialParameter()
+ */
+ @Override
+ public boolean isSpecialParameter() {
+ return super.isSpecialParameter();
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParametersParameterAccessor.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParametersParameterAccessor.java
new file mode 100644
index 000000000..63489c1ba
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParametersParameterAccessor.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.query;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.data.repository.query.ParametersParameterAccessor;
+
+/**
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+class ElasticsearchParametersParameterAccessor extends ParametersParameterAccessor
+ implements ElasticsearchParameterAccessor {
+
+ private final List values;
+
+ /**
+ * Creates a new {@link ElasticsearchParametersParameterAccessor}.
+ *
+ * @param method must not be {@literal null}.
+ * @param values must not be {@literal null}.
+ */
+ ElasticsearchParametersParameterAccessor(ElasticsearchQueryMethod method, Object[] values) {
+
+ super(method.getParameters(), values);
+ this.values = Arrays.asList(values);
+ }
+
+ @Override
+ public Object[] getValues() {
+ return values.toArray();
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java
index 22a973751..f53c2e65d 100644
--- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java
@@ -19,9 +19,15 @@ import java.lang.reflect.Method;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.elasticsearch.annotations.Query;
+import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
+import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
+import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryMethod;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
/**
* ElasticsearchQueryMethod
@@ -30,14 +36,23 @@ import org.springframework.data.repository.query.QueryMethod;
* @author Mohsin Husen
* @author Oliver Gierke
* @author Mark Paluch
+ * @author Christoph Strobl
*/
public class ElasticsearchQueryMethod extends QueryMethod {
private final Query queryAnnotation;
+ private final MappingContext extends ElasticsearchPersistentEntity>, ElasticsearchPersistentProperty> mappingContext;
+ private @Nullable ElasticsearchEntityMetadata> metadata;
+
+ public ElasticsearchQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
+ MappingContext extends ElasticsearchPersistentEntity>, ElasticsearchPersistentProperty> mappingContext) {
- public ElasticsearchQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) {
super(method, metadata, factory);
+
+ Assert.notNull(mappingContext, "MappingContext must not be null!");
+
this.queryAnnotation = method.getAnnotation(Query.class);
+ this.mappingContext = mappingContext;
}
public boolean hasAnnotatedQuery() {
@@ -47,4 +62,42 @@ public class ElasticsearchQueryMethod extends QueryMethod {
public String getAnnotatedQuery() {
return (String) AnnotationUtils.getValue(queryAnnotation, "value");
}
+
+ /**
+ * @return the {@link ElasticsearchEntityMetadata} for the query methods {@link #getReturnedObjectType() return type}.
+ * @since 3.2
+ */
+ public ElasticsearchEntityMetadata> getEntityInformation() {
+
+ if (metadata == null) {
+
+ Class> returnedObjectType = getReturnedObjectType();
+ Class> domainClass = getDomainClass();
+
+ if (ClassUtils.isPrimitiveOrWrapper(returnedObjectType)) {
+
+ this.metadata = new SimpleElasticsearchEntityMetadata<>((Class) domainClass,
+ mappingContext.getRequiredPersistentEntity(domainClass));
+
+ } else {
+
+ ElasticsearchPersistentEntity> returnedEntity = mappingContext.getPersistentEntity(returnedObjectType);
+ ElasticsearchPersistentEntity> managedEntity = mappingContext.getRequiredPersistentEntity(domainClass);
+ returnedEntity = returnedEntity == null || returnedEntity.getType().isInterface() ? managedEntity
+ : returnedEntity;
+ ElasticsearchPersistentEntity> collectionEntity = domainClass.isAssignableFrom(returnedObjectType)
+ ? returnedEntity
+ : managedEntity;
+
+ this.metadata = new SimpleElasticsearchEntityMetadata<>((Class) returnedEntity.getType(),
+ collectionEntity);
+ }
+ }
+
+ return this.metadata;
+ }
+
+ protected MappingContext extends ElasticsearchPersistentEntity>, ElasticsearchPersistentProperty> getMappingContext() {
+ return mappingContext;
+ }
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchParametersParameterAccessor.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchParametersParameterAccessor.java
new file mode 100644
index 000000000..787769836
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchParametersParameterAccessor.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.query;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.core.publisher.MonoProcessor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.data.repository.util.ReactiveWrapperConverters;
+import org.springframework.data.repository.util.ReactiveWrappers;
+
+/**
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+class ReactiveElasticsearchParametersParameterAccessor extends ElasticsearchParametersParameterAccessor {
+
+ private final List> subscriptions;
+
+ /**
+ * Creates a new {@link ElasticsearchParametersParameterAccessor}.
+ *
+ * @param method must not be {@literal null}.
+ * @param values must not be {@literal null}.
+ */
+ ReactiveElasticsearchParametersParameterAccessor(ReactiveElasticsearchQueryMethod method, Object[] values) {
+ super(method, values);
+
+ this.subscriptions = new ArrayList<>(values.length);
+
+ for (int i = 0; i < values.length; i++) {
+
+ Object value = values[i];
+
+ if (value == null || !ReactiveWrappers.supports(value.getClass())) {
+
+ subscriptions.add(null);
+ continue;
+ }
+
+ if (ReactiveWrappers.isSingleValueType(value.getClass())) {
+ subscriptions.add(ReactiveWrapperConverters.toWrapper(value, Mono.class).toProcessor());
+ } else {
+ subscriptions.add(ReactiveWrapperConverters.toWrapper(value, Flux.class).collectList().toProcessor());
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.query.ParametersParameterAccessor#getValue(int)
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ protected T getValue(int index) {
+
+ if (subscriptions.get(index) != null) {
+ return (T) subscriptions.get(index).block();
+ }
+
+ return super.getValue(index);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.elasticsearch.repository.query.ElasticsearchParametersParameterAccessor#getValues(int)
+ */
+ @Override
+ public Object[] getValues() {
+
+ Object[] result = new Object[getValues().length];
+ for (int i = 0; i < result.length; i++) {
+ result[i] = getValue(i);
+ }
+ return result;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.query.ParametersParameterAccessor#getBindableValue(int)
+ */
+ public Object getBindableValue(int index) {
+ return getValue(getParameters().getBindableParameter(index).getIndex());
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryExecution.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryExecution.java
new file mode 100644
index 000000000..399c2442d
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryExecution.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.query;
+
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
+import org.springframework.data.elasticsearch.core.query.Query;
+import org.springframework.data.repository.query.ResultProcessor;
+import org.springframework.data.repository.query.ReturnedType;
+import org.springframework.lang.Nullable;
+import org.springframework.util.ClassUtils;
+
+/**
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+public interface ReactiveElasticsearchQueryExecution {
+
+ Object execute(Query query, Class> type, String indexName, String indexType, @Nullable Class> targetType);
+
+ /**
+ * An {@link ReactiveElasticsearchQueryExecution} that wraps the results of the given delegate with the given result
+ * processing.
+ */
+ @RequiredArgsConstructor
+ final class ResultProcessingExecution implements ReactiveElasticsearchQueryExecution {
+
+ private final @NonNull ReactiveElasticsearchQueryExecution delegate;
+ private final @NonNull Converter converter;
+
+ @Override
+ public Object execute(Query query, Class> type, String indexName, String indexType,
+ @Nullable Class> targetType) {
+ return converter.convert(delegate.execute(query, type, indexName, indexType, targetType));
+ }
+ }
+
+ /**
+ * A {@link Converter} to post-process all source objects using the given {@link ResultProcessor}.
+ *
+ * @author Mark Paluch
+ */
+ @RequiredArgsConstructor
+ final class ResultProcessingConverter implements Converter {
+
+ private final @NonNull ResultProcessor processor;
+ private final @NonNull ReactiveElasticsearchOperations operations;
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
+ */
+ @Override
+ public Object convert(Object source) {
+
+ ReturnedType returnedType = processor.getReturnedType();
+
+ if (ClassUtils.isPrimitiveOrWrapper(returnedType.getReturnedType())) {
+ return source;
+ }
+
+ return processor.processResult(source, it -> it);
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethod.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethod.java
new file mode 100644
index 000000000..17902d7f0
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethod.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.query;
+
+import static org.springframework.data.repository.util.ClassUtils.*;
+
+import java.lang.reflect.Method;
+
+import org.springframework.dao.InvalidDataAccessApiUsageException;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
+import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
+import org.springframework.data.elasticsearch.repository.query.ElasticsearchParameters.ElasticsearchParameter;
+import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.util.ReactiveWrapperConverters;
+import org.springframework.data.repository.util.ReactiveWrappers;
+import org.springframework.data.util.ClassTypeInformation;
+import org.springframework.data.util.TypeInformation;
+import org.springframework.util.ClassUtils;
+
+/**
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+public class ReactiveElasticsearchQueryMethod extends ElasticsearchQueryMethod {
+
+ private static final ClassTypeInformation PAGE_TYPE = ClassTypeInformation.from(Page.class);
+ private static final ClassTypeInformation SLICE_TYPE = ClassTypeInformation.from(Slice.class);
+
+ private final Method method;
+
+ public ReactiveElasticsearchQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
+ MappingContext extends ElasticsearchPersistentEntity>, ElasticsearchPersistentProperty> mappingContext) {
+
+ super(method, metadata, factory, mappingContext);
+ this.method = method;
+
+ if (hasParameterOfType(method, Pageable.class)) {
+
+ TypeInformation> returnType = ClassTypeInformation.fromReturnTypeOf(method);
+
+ boolean multiWrapper = ReactiveWrappers.isMultiValueType(returnType.getType());
+ boolean singleWrapperWithWrappedPageableResult = ReactiveWrappers.isSingleValueType(returnType.getType())
+ && (PAGE_TYPE.isAssignableFrom(returnType.getRequiredComponentType())
+ || SLICE_TYPE.isAssignableFrom(returnType.getRequiredComponentType()));
+
+ if (singleWrapperWithWrappedPageableResult) {
+ throw new InvalidDataAccessApiUsageException(
+ String.format("'%s.%s' must not use sliced or paged execution. Please use Flux.buffer(size, skip).",
+ ClassUtils.getShortName(method.getDeclaringClass()), method.getName()));
+ }
+
+ if (!multiWrapper) {
+ throw new IllegalStateException(String.format(
+ "Method has to use a either multi-item reactive wrapper return type or a wrapped Page/Slice type. Offending method: %s",
+ method.toString()));
+ }
+
+ if (hasParameterOfType(method, Sort.class)) {
+ throw new IllegalStateException(String.format("Method must not have Pageable *and* Sort parameter. "
+ + "Use sorting capabilities on Pageble instead! Offending method: %s", method.toString()));
+ }
+ }
+ }
+
+ @Override
+ protected ElasticsearchParameters createParameters(Method method) {
+ return new ElasticsearchParameters(method);
+ }
+
+ /**
+ * Check if the given {@link org.springframework.data.repository.query.QueryMethod} receives a reactive parameter
+ * wrapper as one of its parameters.
+ *
+ * @return
+ */
+ public boolean hasReactiveWrapperParameter() {
+
+ for (ElasticsearchParameter param : getParameters()) {
+ if (ReactiveWrapperConverters.supports(param.getType())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.query.QueryMethod#isStreamQuery()
+ */
+ @Override
+ public boolean isStreamQuery() {
+
+ // All reactive query methods are streaming.
+ return true;
+ }
+
+ @Override
+ public ElasticsearchParameters getParameters() {
+ return (ElasticsearchParameters) super.getParameters();
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQuery.java
new file mode 100644
index 000000000..9c42c084d
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQuery.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.query;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
+import org.springframework.data.elasticsearch.core.query.StringQuery;
+import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+public class ReactiveElasticsearchStringQuery extends AbstractReactiveElasticsearchRepositoryQuery {
+
+ private static final Pattern PARAMETER_PLACEHOLDER = Pattern.compile("\\?(\\d+)");
+ private final String query;
+
+ public ReactiveElasticsearchStringQuery(ReactiveElasticsearchQueryMethod queryMethod,
+ ReactiveElasticsearchOperations operations, SpelExpressionParser expressionParser,
+ QueryMethodEvaluationContextProvider evaluationContextProvider) {
+
+ this(queryMethod.getAnnotatedQuery(), queryMethod, operations, expressionParser, evaluationContextProvider);
+ }
+
+ public ReactiveElasticsearchStringQuery(String query, ReactiveElasticsearchQueryMethod queryMethod,
+ ReactiveElasticsearchOperations operations, SpelExpressionParser expressionParser,
+ QueryMethodEvaluationContextProvider evaluationContextProvider) {
+
+ super(queryMethod, operations);
+ this.query = query;
+ }
+
+ @Override
+ protected StringQuery createQuery(ElasticsearchParameterAccessor parameterAccessor) {
+ String queryString = replacePlaceholders(this.query, parameterAccessor);
+ return new StringQuery(queryString);
+ }
+
+ private String replacePlaceholders(String input, ElasticsearchParameterAccessor accessor) {
+
+ Matcher matcher = PARAMETER_PLACEHOLDER.matcher(input);
+ String result = input;
+ while (matcher.find()) {
+ String group = matcher.group();
+ int index = Integer.parseInt(matcher.group(1));
+ result = result.replace(group, getParameterWithIndex(accessor, index));
+ }
+ return result;
+ }
+
+ private String getParameterWithIndex(ElasticsearchParameterAccessor accessor, int index) {
+ return ObjectUtils.nullSafeToString(accessor.getBindableValue(index));
+ }
+
+ @Override
+ boolean isCountQuery() {
+ return false;
+ }
+
+ @Override
+ boolean isDeleteQuery() {
+ return false;
+ }
+
+ @Override
+ boolean isExistsQuery() {
+ return false;
+ }
+
+ @Override
+ boolean isLimiting() {
+ return false;
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactivePartTreeElasticsearchQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactivePartTreeElasticsearchQuery.java
new file mode 100644
index 000000000..e0ddafc2e
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactivePartTreeElasticsearchQuery.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.query;
+
+import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
+import org.springframework.data.elasticsearch.core.query.Query;
+import org.springframework.data.elasticsearch.repository.query.parser.ElasticsearchQueryCreator;
+import org.springframework.data.repository.query.ResultProcessor;
+import org.springframework.data.repository.query.parser.PartTree;
+
+/**
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+public class ReactivePartTreeElasticsearchQuery extends AbstractReactiveElasticsearchRepositoryQuery {
+
+ private final PartTree tree;
+ private final ResultProcessor processor;
+
+ public ReactivePartTreeElasticsearchQuery(ReactiveElasticsearchQueryMethod queryMethod,
+ ReactiveElasticsearchOperations elasticsearchOperations) {
+ super(queryMethod, elasticsearchOperations);
+
+ this.processor = queryMethod.getResultProcessor();
+ this.tree = new PartTree(queryMethod.getName(), processor.getReturnedType().getDomainType());
+ }
+
+ @Override
+ protected Query createQuery(ElasticsearchParameterAccessor accessor) {
+ return new ElasticsearchQueryCreator(tree, accessor, getMappingContext()).createQuery();
+ }
+
+ @Override
+ boolean isLimiting() {
+ return tree.isLimiting();
+ }
+
+ @Override
+ boolean isExistsQuery() {
+ return tree.isExistsProjection();
+ }
+
+ @Override
+ boolean isDeleteQuery() {
+ return tree.isDelete();
+ }
+
+ @Override
+ boolean isCountQuery() {
+ return tree.isCountProjection();
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/SimpleElasticsearchEntityMetadata.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/SimpleElasticsearchEntityMetadata.java
new file mode 100644
index 000000000..893985981
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/SimpleElasticsearchEntityMetadata.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.query;
+
+import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
+import org.springframework.util.Assert;
+
+/**
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+public class SimpleElasticsearchEntityMetadata implements ElasticsearchEntityMetadata {
+
+ private final Class type;
+ private final ElasticsearchPersistentEntity> entity;
+
+ public SimpleElasticsearchEntityMetadata(Class type, ElasticsearchPersistentEntity> entity) {
+
+ Assert.notNull(type, "Type must not be null!");
+ Assert.notNull(entity, "Entity must not be null!");
+
+ this.type = type;
+ this.entity = entity;
+ }
+
+ @Override
+ public String getIndexName() {
+ return entity.getIndexName();
+ }
+
+ @Override
+ public String getIndexTypeName() {
+ return entity.getIndexType();
+ }
+
+ @Override
+ public Class getJavaType() {
+ return type;
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java
index 0c705042e..cea00de86 100644
--- a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java
@@ -102,7 +102,8 @@ public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport {
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
NamedQueries namedQueries) {
- ElasticsearchQueryMethod queryMethod = new ElasticsearchQueryMethod(method, metadata, factory);
+ ElasticsearchQueryMethod queryMethod = new ElasticsearchQueryMethod(method, metadata, factory,
+ elasticsearchOperations.getElasticsearchConverter().getMappingContext());
String namedQueryName = queryMethod.getNamedQueryName();
if (namedQueries.hasQuery(namedQueryName)) {
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java
new file mode 100644
index 000000000..8b4a431da
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.support;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.Optional;
+
+import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
+import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
+import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
+import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchQueryMethod;
+import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchStringQuery;
+import org.springframework.data.elasticsearch.repository.query.ReactivePartTreeElasticsearchQuery;
+import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.repository.core.NamedQueries;
+import org.springframework.data.repository.core.RepositoryInformation;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport;
+import org.springframework.data.repository.query.QueryLookupStrategy;
+import org.springframework.data.repository.query.QueryLookupStrategy.Key;
+import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
+import org.springframework.data.repository.query.RepositoryQuery;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * Factory to create {@link org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository}
+ * instances.
+ *
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+public class ReactiveElasticsearchRepositoryFactory extends ReactiveRepositoryFactorySupport {
+
+ private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
+
+ private final ReactiveElasticsearchOperations operations;
+ private final MappingContext extends ElasticsearchPersistentEntity>, ElasticsearchPersistentProperty> mappingContext;
+
+ /**
+ * Creates a new {@link ReactiveElasticsearchRepositoryFactory} with the given
+ * {@link ReactiveElasticsearchOperations}.
+ *
+ * @param elasticsearchOperations must not be {@literal null}.
+ */
+ public ReactiveElasticsearchRepositoryFactory(ReactiveElasticsearchOperations elasticsearchOperations) {
+
+ Assert.notNull(elasticsearchOperations, "ReactiveElasticsearchOperations must not be null!");
+
+ this.operations = elasticsearchOperations;
+ this.mappingContext = elasticsearchOperations.getElasticsearchConverter().getMappingContext();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryBaseClass(org.springframework.data.repository.core.RepositoryMetadata)
+ */
+ @Override
+ protected Class> getRepositoryBaseClass(RepositoryMetadata metadata) {
+ return SimpleReactiveElasticsearchRepository.class;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getTargetRepository(org.springframework.data.repository.core.RepositoryInformation)
+ */
+ @Override
+ protected Object getTargetRepository(RepositoryInformation information) {
+
+ ElasticsearchEntityInformation, Serializable> entityInformation = getEntityInformation(
+ information.getDomainType(), information);
+ return getTargetRepositoryViaReflection(information, entityInformation, operations);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key, org.springframework.data.repository.query.EvaluationContextProvider)
+ */
+ @Override
+ protected Optional getQueryLookupStrategy(@Nullable Key key,
+ QueryMethodEvaluationContextProvider evaluationContextProvider) {
+ return Optional.of(new ElasticsearchQueryLookupStrategy(operations, evaluationContextProvider, mappingContext));
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getEntityInformation(java.lang.Class)
+ */
+ public ElasticsearchEntityInformation getEntityInformation(Class domainClass) {
+ return getEntityInformation(domainClass, null);
+ }
+
+ @SuppressWarnings("unchecked")
+ private ElasticsearchEntityInformation getEntityInformation(Class domainClass,
+ @Nullable RepositoryInformation information) {
+
+ ElasticsearchPersistentEntity> entity = mappingContext.getRequiredPersistentEntity(domainClass);
+
+ return new MappingElasticsearchEntityInformation<>((ElasticsearchPersistentEntity) entity, entity.getIndexName(),
+ entity.getIndexType());
+ }
+
+ /**
+ * @author Christoph Strobl
+ */
+ @RequiredArgsConstructor(access = AccessLevel.PACKAGE)
+ private static class ElasticsearchQueryLookupStrategy implements QueryLookupStrategy {
+
+ private final ReactiveElasticsearchOperations operations;
+ private final QueryMethodEvaluationContextProvider evaluationContextProvider;
+ private final MappingContext extends ElasticsearchPersistentEntity>, ElasticsearchPersistentProperty> mappingContext;
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries)
+ */
+ @Override
+ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
+ NamedQueries namedQueries) {
+
+ ReactiveElasticsearchQueryMethod queryMethod = new ReactiveElasticsearchQueryMethod(method, metadata, factory,
+ mappingContext);
+ String namedQueryName = queryMethod.getNamedQueryName();
+
+ if (namedQueries.hasQuery(namedQueryName)) {
+ String namedQuery = namedQueries.getQuery(namedQueryName);
+
+ return new ReactiveElasticsearchStringQuery(namedQuery, queryMethod, operations, EXPRESSION_PARSER,
+ evaluationContextProvider);
+ } else if (queryMethod.hasAnnotatedQuery()) {
+ return new ReactiveElasticsearchStringQuery(queryMethod, operations, EXPRESSION_PARSER,
+ evaluationContextProvider);
+ } else {
+ return new ReactivePartTreeElasticsearchQuery(queryMethod, operations);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactoryBean.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactoryBean.java
new file mode 100644
index 000000000..4e8b3ba5c
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactoryBean.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.support;
+
+import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
+import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.repository.Repository;
+import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
+import org.springframework.data.repository.core.support.RepositoryFactorySupport;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * {@link org.springframework.beans.factory.FactoryBean} to create
+ * {@link org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository} instances.
+ *
+ * @author Christoph Strobl
+ * @since 3.2
+ * @see org.springframework.data.repository.reactive.ReactiveSortingRepository
+ */
+public class ReactiveElasticsearchRepositoryFactoryBean, S, ID>
+ extends RepositoryFactoryBeanSupport {
+
+ private @Nullable ReactiveElasticsearchOperations operations;
+ private boolean mappingContextConfigured = false;
+
+ /**
+ * Creates a new {@link ReactiveElasticsearchRepositoryFactoryBean} for the given repository interface.
+ *
+ * @param repositoryInterface must not be {@literal null}.
+ */
+ public ReactiveElasticsearchRepositoryFactoryBean(Class extends T> repositoryInterface) {
+ super(repositoryInterface);
+ }
+
+ /**
+ * Configures the {@link ReactiveElasticsearchOperations} to be used.
+ *
+ * @param operations the operations to set
+ */
+ public void setReactiveElasticsearchOperations(@Nullable ReactiveElasticsearchOperations operations) {
+ this.operations = operations;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#setMappingContext(org.springframework.data.mapping.context.MappingContext)
+ */
+ @Override
+ protected void setMappingContext(MappingContext, ?> mappingContext) {
+
+ super.setMappingContext(mappingContext);
+ this.mappingContextConfigured = true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * org.springframework.data.repository.support.RepositoryFactoryBeanSupport
+ * #createRepositoryFactory()
+ */
+ @Override
+ protected final RepositoryFactorySupport createRepositoryFactory() {
+
+ return getFactoryInstance(operations);
+ }
+
+ /**
+ * Creates and initializes a {@link RepositoryFactorySupport} instance.
+ *
+ * @param operations
+ * @return
+ */
+ protected RepositoryFactorySupport getFactoryInstance(ReactiveElasticsearchOperations operations) {
+ return new ReactiveElasticsearchRepositoryFactory(operations);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * org.springframework.data.repository.support.RepositoryFactoryBeanSupport
+ * #afterPropertiesSet()
+ */
+ @Override
+ public void afterPropertiesSet() {
+
+ super.afterPropertiesSet();
+ Assert.state(operations != null, "ReactiveElasticsearchOperations must not be null!");
+
+ if (!mappingContextConfigured) {
+ setMappingContext(operations.getElasticsearchConverter().getMappingContext());
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepository.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepository.java
new file mode 100644
index 000000000..29ed2d612
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepository.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.support;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import org.reactivestreams.Publisher;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
+import org.springframework.data.elasticsearch.core.query.Query;
+import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository;
+import org.springframework.util.Assert;
+
+/**
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+public class SimpleReactiveElasticsearchRepository implements ReactiveElasticsearchRepository {
+
+ private final ElasticsearchEntityInformation entityInformation;
+ private final ReactiveElasticsearchOperations elasticsearchOperations;
+
+ public SimpleReactiveElasticsearchRepository(ElasticsearchEntityInformation entityInformation,
+ ReactiveElasticsearchOperations elasticsearchOperations) {
+
+ Assert.notNull(entityInformation, "EntityInformation must not be null!");
+ Assert.notNull(elasticsearchOperations, "ElasticsearchOperations must not be null!");
+
+ this.entityInformation = entityInformation;
+ this.elasticsearchOperations = elasticsearchOperations;
+ }
+
+ @Override
+ public Flux findAll(Sort sort) {
+
+ return elasticsearchOperations.find(Query.findAll().addSort(sort), entityInformation.getJavaType(),
+ entityInformation.getIndexName(), entityInformation.getType());
+ }
+
+ @Override
+ public Mono save(S entity) {
+
+ Assert.notNull(entity, "Entity must not be null!");
+ return elasticsearchOperations.save(entity, entityInformation.getIndexName(), entityInformation.getType());
+ }
+
+ @Override
+ public Flux saveAll(Iterable entities) {
+
+ Assert.notNull(entities, "Entities must not be null!");
+ return saveAll(Flux.fromIterable(entities));
+ }
+
+ @Override
+ public Flux saveAll(Publisher entityStream) {
+
+ Assert.notNull(entityStream, "EntityStream must not be null!");
+ return Flux.from(entityStream).flatMap(this::save);
+ }
+
+ @Override
+ public Mono findById(ID id) {
+
+ Assert.notNull(id, "Id must not be null!");
+ return elasticsearchOperations.findById(convertId(id), entityInformation.getJavaType(),
+ entityInformation.getIndexName(), entityInformation.getType());
+ }
+
+ @Override
+ public Mono findById(Publisher id) {
+
+ Assert.notNull(id, "Id must not be null!");
+ return Mono.from(id).flatMap(this::findById);
+ }
+
+ @Override
+ public Mono existsById(ID id) {
+
+ Assert.notNull(id, "Id must not be null!");
+ return elasticsearchOperations.exists(convertId(id), entityInformation.getJavaType(),
+ entityInformation.getIndexName(), entityInformation.getType());
+ }
+
+ @Override
+ public Mono existsById(Publisher id) {
+
+ Assert.notNull(id, "Id must not be null!");
+ return Mono.from(id).flatMap(this::existsById);
+ }
+
+ @Override
+ public Flux findAll() {
+
+ return elasticsearchOperations.find(Query.findAll(), entityInformation.getJavaType(),
+ entityInformation.getIndexName(), entityInformation.getType());
+ }
+
+ @Override
+ public Flux findAllById(Iterable ids) {
+
+ Assert.notNull(ids, "Ids must not be null!");
+
+ return Flux.fromIterable(ids).flatMap(this::findById);
+ }
+
+ @Override
+ public Flux findAllById(Publisher idStream) {
+
+ Assert.notNull(idStream, "IdStream must not be null!");
+ return Flux.from(idStream).buffer().flatMap(this::findAllById);
+ }
+
+ @Override
+ public Mono count() {
+
+ return elasticsearchOperations.count(Query.findAll(), entityInformation.getJavaType(),
+ entityInformation.getIndexName(), entityInformation.getType());
+ }
+
+ @Override
+ public Mono deleteById(ID id) {
+
+ Assert.notNull(id, "Id must not be null!");
+ return elasticsearchOperations
+ .deleteById(convertId(id), entityInformation.getJavaType(), entityInformation.getIndexName(),
+ entityInformation.getType()) //
+ .then();
+ }
+
+ @Override
+ public Mono deleteById(Publisher id) {
+
+ Assert.notNull(id, "Id must not be null!");
+ return Mono.from(id).flatMap(this::deleteById);
+ }
+
+ @Override
+ public Mono delete(T entity) {
+
+ Assert.notNull(entity, "Entity must not be null!");
+ return elasticsearchOperations.delete(entity, entityInformation.getIndexName(), entityInformation.getType()) //
+ .then();
+ }
+
+ @Override
+ public Mono deleteAll(Iterable extends T> entities) {
+
+ Assert.notNull(entities, "Entities must not be null!");
+ return deleteAll(Flux.fromIterable(entities));
+ }
+
+ @Override
+ public Mono deleteAll(Publisher extends T> entityStream) {
+
+ Assert.notNull(entityStream, "EntityStream must not be null!");
+ return Flux.from(entityStream).flatMap(this::delete).then();
+ }
+
+ @Override
+ public Mono deleteAll() {
+
+ return elasticsearchOperations
+ .deleteBy(Query.findAll(), entityInformation.getJavaType(), entityInformation.getIndexName(),
+ entityInformation.getType()) //
+ .then();
+ }
+
+ private String convertId(Object id) {
+ return elasticsearchOperations.getElasticsearchConverter().convertId(id);
+ }
+}
diff --git a/src/test/java/org/springframework/data/elasticsearch/TestUtils.java b/src/test/java/org/springframework/data/elasticsearch/TestUtils.java
index 7f61eb099..12af39ebe 100644
--- a/src/test/java/org/springframework/data/elasticsearch/TestUtils.java
+++ b/src/test/java/org/springframework/data/elasticsearch/TestUtils.java
@@ -17,14 +17,16 @@ package org.springframework.data.elasticsearch;
import lombok.SneakyThrows;
-import java.io.IOException;
import java.time.Duration;
import org.elasticsearch.ElasticsearchStatusException;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.get.GetRequest;
+import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.RestClients;
import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient;
@@ -84,6 +86,18 @@ public final class TestUtils {
}
}
+ @SneakyThrows
+ public static boolean isEmptyIndex(String indexName) {
+
+ try (RestHighLevelClient client = restHighLevelClient()) {
+
+ return 0L == client
+ .search(new SearchRequest(indexName)
+ .source(SearchSourceBuilder.searchSource().query(QueryBuilders.matchAllQuery())), RequestOptions.DEFAULT)
+ .getHits().getTotalHits();
+ }
+ }
+
public static OfType documentWithId(String id) {
return new DocumentLookup(id);
}
@@ -106,19 +120,16 @@ public final class TestUtils {
}
@Override
+ @SneakyThrows
public boolean existsIn(String index) {
GetRequest request = new GetRequest(index).id(id);
if (StringUtils.hasText(type)) {
request = request.type(type);
}
- try {
- return restHighLevelClient().get(request, RequestOptions.DEFAULT).isExists();
- } catch (IOException e) {
- e.printStackTrace();
+ try (RestHighLevelClient client = restHighLevelClient()) {
+ return client.get(request, RequestOptions.DEFAULT).isExists();
}
-
- return false;
}
@Override
diff --git a/src/test/java/org/springframework/data/elasticsearch/client/reactive/ReactiveElasticsearchClientTests.java b/src/test/java/org/springframework/data/elasticsearch/client/reactive/ReactiveElasticsearchClientTests.java
index 13bbc5be0..6b369628a 100644
--- a/src/test/java/org/springframework/data/elasticsearch/client/reactive/ReactiveElasticsearchClientTests.java
+++ b/src/test/java/org/springframework/data/elasticsearch/client/reactive/ReactiveElasticsearchClientTests.java
@@ -17,6 +17,7 @@ package org.springframework.data.elasticsearch.client.reactive;
import static org.assertj.core.api.Assertions.*;
+import org.springframework.data.elasticsearch.ElasticsearchException;
import reactor.test.StepVerifier;
import java.io.IOException;
@@ -135,6 +136,16 @@ public class ReactiveElasticsearchClientTests {
.verifyComplete();
}
+ @Test // DATAES-519
+ public void getOnNonExistingIndexShouldThrowException() {
+
+ client.get(new GetRequest(INDEX_I, TYPE_I, "nonono"))
+ .as(StepVerifier::create)
+ .expectError(ElasticsearchStatusException.class)
+ .verify();
+ }
+
+
@Test // DATAES-488
public void getShouldFetchDocumentById() {
diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplateTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplateTests.java
index 5cd3f073f..a2747a14c 100644
--- a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplateTests.java
+++ b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplateTests.java
@@ -177,6 +177,14 @@ public class ReactiveElasticsearchTemplateTests {
template.save(null);
}
+ @Test // DATAES-519
+ public void findByIdShouldCompleteWhenIndexDoesNotExist() {
+
+ template.findById("foo", SampleEntity.class, "no-such-index") //
+ .as(StepVerifier::create) //
+ .verifyComplete();
+ }
+
@Test // DATAES-504
public void findByIdShouldReturnEntity() {
@@ -245,6 +253,15 @@ public class ReactiveElasticsearchTemplateTests {
.verifyComplete();
}
+ @Test // DATAES-519
+ public void existsShouldReturnFalseWhenIndexDoesNotExist() {
+
+ template.exists("foo", SampleEntity.class, "no-such-index") //
+ .as(StepVerifier::create) //
+ .expectNext(false) //
+ .verifyComplete();
+ }
+
@Test // DATAES-504
public void existsShouldReturnTrueWhenFound() {
@@ -269,6 +286,14 @@ public class ReactiveElasticsearchTemplateTests {
.verifyComplete();
}
+ @Test // DATAES-519
+ public void findShouldCompleteWhenIndexDoesNotExist() {
+
+ template.find(new CriteriaQuery(Criteria.where("message").is("some message")), SampleEntity.class, "no-such-index") //
+ .as(StepVerifier::create) //
+ .verifyComplete();
+ }
+
@Test // DATAES-504
public void findShouldApplyCriteria() {
@@ -392,6 +417,15 @@ public class ReactiveElasticsearchTemplateTests {
.verifyComplete();
}
+ @Test // DATAES-519
+ public void countShouldReturnZeroWhenIndexDoesNotExist() {
+
+ template.count(SampleEntity.class) //
+ .as(StepVerifier::create) //
+ .expectNext(0L) //
+ .verifyComplete();
+ }
+
@Test // DATAES-504
public void countShouldReturnCountAllWhenGivenNoQuery() {
@@ -416,6 +450,14 @@ public class ReactiveElasticsearchTemplateTests {
.verifyComplete();
}
+ @Test // DATAES-519
+ public void deleteByIdShouldCompleteWhenIndexDoesNotExist() {
+
+ template.deleteById("does-not-exists", SampleEntity.class, "no-such-index") //
+ .as(StepVerifier::create)//
+ .verifyComplete();
+ }
+
@Test // DATAES-504
public void deleteByIdShouldRemoveExistingDocumentById() {
@@ -462,6 +504,18 @@ public class ReactiveElasticsearchTemplateTests {
.verifyComplete();
}
+ @Test // DATAES-519
+ @ElasticsearchVersion(asOf = "6.5.0")
+ public void deleteByQueryShouldReturnZeroWhenIndexDoesNotExist() {
+
+ CriteriaQuery query = new CriteriaQuery(new Criteria("message").contains("test"));
+
+ template.deleteBy(query, SampleEntity.class) //
+ .as(StepVerifier::create) //
+ .expectNext(0L) //
+ .verifyComplete();
+ }
+
@Test // DATAES-504
@ElasticsearchVersion(asOf = "6.5.0")
public void deleteByQueryShouldReturnNumberOfDeletedDocuments() {
diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoriesRegistrarTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoriesRegistrarTests.java
new file mode 100644
index 000000000..39d287f81
--- /dev/null
+++ b/src/test/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoriesRegistrarTests.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.config;
+
+import org.assertj.core.api.Assertions;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.elasticsearch.TestUtils;
+import org.springframework.data.elasticsearch.core.ReactiveElasticsearchTemplate;
+import org.springframework.data.elasticsearch.entities.SampleEntity;
+import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+/**
+ * @author Christoph Strobl
+ * @currentRead Fool's Fate - Robin Hobb
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration
+public class ReactiveElasticsearchRepositoriesRegistrarTests {
+
+ @Configuration
+ @EnableReactiveElasticsearchRepositories(considerNestedRepositories = true)
+ static class Config {
+
+ @Bean
+ public ReactiveElasticsearchTemplate reactiveElasticsearchTemplate() {
+ return new ReactiveElasticsearchTemplate(TestUtils.reactiveClient());
+ }
+ }
+
+ @Autowired ReactiveSampleEntityRepository repository;
+ @Autowired ApplicationContext context;
+
+ @Test // DATAES-519
+ public void testConfiguration() {
+
+ Assertions.assertThat(context).isNotNull();
+ Assertions.assertThat(repository).isNotNull();
+
+ }
+
+ interface ReactiveSampleEntityRepository extends ReactiveElasticsearchRepository {}
+
+}
diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoryConfigurationExtensionUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoryConfigurationExtensionUnitTests.java
new file mode 100644
index 000000000..12c67fa2c
--- /dev/null
+++ b/src/test/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoryConfigurationExtensionUnitTests.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.config;
+
+import static org.junit.Assert.*;
+
+import java.util.Collection;
+
+import org.junit.Test;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.core.env.Environment;
+import org.springframework.core.env.StandardEnvironment;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.type.StandardAnnotationMetadata;
+import org.springframework.data.elasticsearch.annotations.Document;
+import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository;
+import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource;
+import org.springframework.data.repository.config.RepositoryConfiguration;
+import org.springframework.data.repository.config.RepositoryConfigurationSource;
+import org.springframework.data.repository.reactive.ReactiveCrudRepository;
+
+/**
+ * @author Christoph Strobl
+ * @currentRead Fool's Fate - Robin Hobb
+ */
+public class ReactiveElasticsearchRepositoryConfigurationExtensionUnitTests {
+
+ StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(Config.class, true);
+ ResourceLoader loader = new PathMatchingResourcePatternResolver();
+ Environment environment = new StandardEnvironment();
+ BeanDefinitionRegistry registry = new DefaultListableBeanFactory();
+
+ RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata,
+ EnableReactiveElasticsearchRepositories.class, loader, environment, registry);
+
+ @Test // DATAES-519
+ public void isStrictMatchIfDomainTypeIsAnnotatedWithDocument() {
+
+ ReactiveElasticsearchRepositoryConfigurationExtension extension = new ReactiveElasticsearchRepositoryConfigurationExtension();
+ assertHasRepo(CrudRepositoryForAnnotatedType.class,
+ extension.getRepositoryConfigurations(configurationSource, loader, true));
+ }
+
+ @Test // DATAES-519
+ public void isStrictMatchIfRepositoryExtendsStoreSpecificBase() {
+
+ ReactiveElasticsearchRepositoryConfigurationExtension extension = new ReactiveElasticsearchRepositoryConfigurationExtension();
+ assertHasRepo(EsRepositoryForUnAnnotatedType.class,
+ extension.getRepositoryConfigurations(configurationSource, loader, true));
+ }
+
+ @Test // DATAES-519
+ public void isNotStrictMatchIfDomainTypeIsNotAnnotatedWithDocument() {
+
+ ReactiveElasticsearchRepositoryConfigurationExtension extension = new ReactiveElasticsearchRepositoryConfigurationExtension();
+ assertDoesNotHaveRepo(CrudRepositoryForUnAnnotatedType.class,
+ extension.getRepositoryConfigurations(configurationSource, loader, true));
+ }
+
+ private static void assertHasRepo(Class> repositoryInterface,
+ Collection> configs) {
+
+ for (RepositoryConfiguration> config : configs) {
+ if (config.getRepositoryInterface().equals(repositoryInterface.getName())) {
+ return;
+ }
+ }
+
+ fail("Expected to find config for repository interface ".concat(repositoryInterface.getName()).concat(" but got ")
+ .concat(configs.toString()));
+ }
+
+ private static void assertDoesNotHaveRepo(Class> repositoryInterface,
+ Collection> configs) {
+
+ for (RepositoryConfiguration> config : configs) {
+ if (config.getRepositoryInterface().equals(repositoryInterface.getName())) {
+ fail("Expected not to find config for repository interface ".concat(repositoryInterface.getName()));
+ }
+ }
+ }
+
+ @EnableReactiveElasticsearchRepositories(considerNestedRepositories = true)
+ static class Config {
+
+ }
+
+ @Document(indexName = "star-wars", type = "character")
+ static class SwCharacter {}
+
+ static class Store {}
+
+ interface CrudRepositoryForAnnotatedType extends ReactiveCrudRepository {}
+
+ interface CrudRepositoryForUnAnnotatedType extends ReactiveCrudRepository {}
+
+ interface EsRepositoryForUnAnnotatedType extends ReactiveElasticsearchRepository {}
+}
diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethodUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethodUnitTests.java
new file mode 100644
index 000000000..9c189d57e
--- /dev/null
+++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethodUnitTests.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.query;
+
+import static org.assertj.core.api.Assertions.*;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.lang.reflect.Method;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.dao.InvalidDataAccessApiUsageException;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
+import org.springframework.data.elasticsearch.entities.Person;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.data.repository.Repository;
+import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
+
+/**
+ * @author Christoph Strobl
+ * @currentRead Fool's Fate - Robin Hobb
+ */
+public class ReactiveElasticsearchQueryMethodUnitTests {
+
+ SimpleElasticsearchMappingContext mappingContext;
+
+ @Before
+ public void setUp() {
+ mappingContext = new SimpleElasticsearchMappingContext();
+ }
+
+ @Test // DATAES-519
+ public void detectsCollectionFromRepoTypeIfReturnTypeNotAssignable() throws Exception {
+
+ ReactiveElasticsearchQueryMethod queryMethod = queryMethod(NonReactiveRepository.class, "method");
+ ElasticsearchEntityMetadata> metadata = queryMethod.getEntityInformation();
+
+ assertThat(metadata.getJavaType()).isAssignableFrom(Person.class);
+ assertThat(metadata.getIndexName()).isEqualTo("test-index-person");
+ assertThat(metadata.getIndexTypeName()).isEqualTo("user");
+ }
+
+ @Test(expected = IllegalArgumentException.class) // DATAES-519
+ public void rejectsNullMappingContext() throws Exception {
+
+ Method method = PersonRepository.class.getMethod("findByName", String.class);
+
+ new ReactiveElasticsearchQueryMethod(method, new DefaultRepositoryMetadata(PersonRepository.class),
+ new SpelAwareProxyProjectionFactory(), null);
+ }
+
+ @Test(expected = IllegalStateException.class) // DATAES-519
+ public void rejectsMonoPageableResult() throws Exception {
+ queryMethod(PersonRepository.class, "findMonoByName", String.class, Pageable.class);
+ }
+
+ @Test(expected = InvalidDataAccessApiUsageException.class) // DATAES-519
+ public void throwsExceptionOnWrappedPage() throws Exception {
+ queryMethod(PersonRepository.class, "findMonoPageByName", String.class, Pageable.class);
+ }
+
+ @Test(expected = InvalidDataAccessApiUsageException.class) // DATAES-519
+ public void throwsExceptionOnWrappedSlice() throws Exception {
+ queryMethod(PersonRepository.class, "findMonoSliceByName", String.class, Pageable.class);
+ }
+
+ @Test // DATAES-519
+ public void allowsPageableOnFlux() throws Exception {
+ queryMethod(PersonRepository.class, "findByName", String.class, Pageable.class);
+ }
+
+ @Test // DATAES-519
+ public void fallsBackToRepositoryDomainTypeIfMethodDoesNotReturnADomainType() throws Exception {
+
+ ReactiveElasticsearchQueryMethod method = queryMethod(PersonRepository.class, "deleteByName", String.class);
+
+ assertThat(method.getEntityInformation().getJavaType()).isAssignableFrom(Person.class);
+ }
+
+ private ReactiveElasticsearchQueryMethod queryMethod(Class> repository, String name, Class>... parameters)
+ throws Exception {
+
+ Method method = repository.getMethod(name, parameters);
+ ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
+ return new ReactiveElasticsearchQueryMethod(method, new DefaultRepositoryMetadata(repository), factory,
+ mappingContext);
+ }
+
+ interface PersonRepository extends Repository {
+
+ Mono findMonoByName(String name, Pageable pageRequest);
+
+ Mono> findMonoPageByName(String name, Pageable pageRequest);
+
+ Mono> findMonoSliceByName(String name, Pageable pageRequest);
+
+ Flux findByName(String name);
+
+ Flux findByName(String name, Pageable pageRequest);
+
+ void deleteByName(String name);
+ }
+
+ interface NonReactiveRepository extends Repository {
+
+ List method();
+ }
+
+}
diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQueryUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQueryUnitTests.java
new file mode 100644
index 000000000..e8c03d0aa
--- /dev/null
+++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQueryUnitTests.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.query;
+
+import static org.assertj.core.api.Assertions.*;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.lang.reflect.Method;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.data.elasticsearch.annotations.Query;
+import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
+import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
+import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
+import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
+import org.springframework.data.elasticsearch.core.query.StringQuery;
+import org.springframework.data.elasticsearch.entities.Person;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.data.repository.Repository;
+import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
+import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+
+/**
+ * @author Christoph Strobl
+ * @currentRead Fool's Fate - Robin Hobb
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class ReactiveElasticsearchStringQueryUnitTests {
+
+ SpelExpressionParser PARSER = new SpelExpressionParser();
+ ElasticsearchConverter converter;
+
+ @Mock ReactiveElasticsearchOperations operations;
+
+ @Before
+ public void setUp() {
+ converter = new MappingElasticsearchConverter(new SimpleElasticsearchMappingContext());
+ }
+
+ @Test // DATAES-519
+ public void bindsSimplePropertyCorrectly() throws Exception {
+
+ ReactiveElasticsearchStringQuery elasticsearchStringQuery = createQueryForMethod("findByName", String.class);
+ StubParameterAccessor accesor = new StubParameterAccessor("Luke");
+
+ org.springframework.data.elasticsearch.core.query.Query query = elasticsearchStringQuery.createQuery(accesor);
+ StringQuery reference = new StringQuery("{ 'bool' : { 'must' : { 'term' : { 'name' : 'Luke' } } } }");
+
+ assertThat(query).isInstanceOf(StringQuery.class);
+ assertThat(((StringQuery) query).getSource()).isEqualTo(reference.getSource());
+ }
+
+ @Test // DATAES-519
+ @Ignore("TODO: fix spel query integration")
+ public void bindsExpressionPropertyCorrectly() throws Exception {
+
+ ReactiveElasticsearchStringQuery elasticsearchStringQuery = createQueryForMethod("findByNameWithExpression",
+ String.class);
+ StubParameterAccessor accesor = new StubParameterAccessor("Luke");
+
+ org.springframework.data.elasticsearch.core.query.Query query = elasticsearchStringQuery.createQuery(accesor);
+ StringQuery reference = new StringQuery("{ 'bool' : { 'must' : { 'term' : { 'name' : 'Luke' } } } }");
+
+ assertThat(query).isInstanceOf(StringQuery.class);
+ assertThat(((StringQuery) query).getSource()).isEqualTo(reference.getSource());
+ }
+
+ private ReactiveElasticsearchStringQuery createQueryForMethod(String name, Class>... parameters) throws Exception {
+
+ Method method = SampleRepository.class.getMethod(name, parameters);
+ ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
+ ReactiveElasticsearchQueryMethod queryMethod = new ReactiveElasticsearchQueryMethod(method,
+ new DefaultRepositoryMetadata(SampleRepository.class), factory, converter.getMappingContext());
+ return new ReactiveElasticsearchStringQuery(queryMethod, operations, PARSER,
+ QueryMethodEvaluationContextProvider.DEFAULT);
+ }
+
+ private interface SampleRepository extends Repository {
+
+ @Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '?0' } } } }")
+ Mono findByName(String name);
+
+ @Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '?#{[0]}' } } } }")
+ Flux findByNameWithExpression(String param0);
+ }
+}
diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/StubParameterAccessor.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/StubParameterAccessor.java
new file mode 100644
index 000000000..5778ddb34
--- /dev/null
+++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/StubParameterAccessor.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.query;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Optional;
+
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.repository.query.ParameterAccessor;
+
+/**
+ * Simple {@link ParameterAccessor} that returns the given parameters unfiltered.
+ *
+ * @author Christoph Strobl
+ * @currentRead Fool's Fate - Robin Hobb
+ */
+class StubParameterAccessor implements ElasticsearchParameterAccessor {
+
+ private final Object[] values;
+
+ @SuppressWarnings("unchecked")
+ public StubParameterAccessor(Object... values) {
+ this.values = values;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.query.ParameterAccessor#getPageable()
+ */
+ public Pageable getPageable() {
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.query.ParameterAccessor#getBindableValue(int)
+ */
+ public Object getBindableValue(int index) {
+ return values[index];
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.query.ParameterAccessor#hasBindableNullValue()
+ */
+ public boolean hasBindableNullValue() {
+ return false;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.query.ParameterAccessor#getSort()
+ */
+ public Sort getSort() {
+ return Sort.unsorted();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.query.ParameterAccessor#iterator()
+ */
+ public Iterator iterator() {
+ return Arrays.asList(values).iterator();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.elasticsearch.repository.query.ElasticsearchParameterAccessor#getValues()
+ */
+ @Override
+ public Object[] getValues() {
+ return this.values;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.query.ParameterAccessor#getDynamicProjection()
+ */
+ @Override
+ public Optional> getDynamicProjection() {
+ return Optional.empty();
+ }
+}
diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryTests.java
new file mode 100644
index 000000000..2c1cd912e
--- /dev/null
+++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryTests.java
@@ -0,0 +1,492 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.elasticsearch.repository.support;
+
+import static org.assertj.core.api.Assertions.*;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.elasticsearch.action.bulk.BulkRequest;
+import org.elasticsearch.action.index.IndexRequest;
+import org.elasticsearch.action.support.WriteRequest.RefreshPolicy;
+import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.client.RestHighLevelClient;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.reactivestreams.Publisher;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Sort.Order;
+import org.springframework.data.elasticsearch.TestUtils;
+import org.springframework.data.elasticsearch.annotations.Query;
+import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient;
+import org.springframework.data.elasticsearch.config.AbstractReactiveElasticsearchConfiguration;
+import org.springframework.data.elasticsearch.entities.SampleEntity;
+import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories;
+import org.springframework.data.repository.reactive.ReactiveCrudRepository;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author Christoph Strobl
+ * @currentRead Fool's Fate - Robin Hobb
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration
+public class SimpleReactiveElasticsearchRepositoryTests {
+
+ @Configuration
+ @EnableReactiveElasticsearchRepositories(considerNestedRepositories = true)
+ static class Config extends AbstractReactiveElasticsearchConfiguration {
+
+ @Override
+ public ReactiveElasticsearchClient reactiveElasticsearchClient() {
+ return TestUtils.reactiveClient();
+ }
+ }
+
+ static final String INDEX = "test-index-sample";
+ static final String TYPE = "test-type";
+
+ @Autowired ReactiveSampleEntityRepository repository;
+
+ @After
+ public void tearDown() {
+ TestUtils.deleteIndex(INDEX);
+ }
+
+ @Test // DATAES-519
+ public void saveShouldSaveSingleEntity() {
+
+ repository.save(SampleEntity.builder().build()) //
+ .as(StepVerifier::create) //
+ .consumeNextWith(it -> {
+ assertThat(TestUtils.documentWithId(it.getId()).ofType(TYPE).existsIn(INDEX)).isTrue();
+ }) //
+ .verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void saveShouldComputeMultipleEntities() {
+
+ repository
+ .saveAll(Arrays.asList(SampleEntity.builder().build(), SampleEntity.builder().build(),
+ SampleEntity.builder().build())) //
+ .as(StepVerifier::create) //
+ .consumeNextWith(it -> {
+ assertThat(TestUtils.documentWithId(it.getId()).ofType(TYPE).existsIn(INDEX)).isTrue();
+ }) //
+ .consumeNextWith(it -> {
+ assertThat(TestUtils.documentWithId(it.getId()).ofType(TYPE).existsIn(INDEX)).isTrue();
+ }) //
+ .consumeNextWith(it -> {
+ assertThat(TestUtils.documentWithId(it.getId()).ofType(TYPE).existsIn(INDEX)).isTrue();
+ }) //
+ .verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void findByIdShouldCompleteIfIndexDoesNotExist() {
+ repository.findById("id-two").as(StepVerifier::create).verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void findShouldRetrieveSingleEntityById() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").build(), //
+ SampleEntity.builder().id("id-two").build(), //
+ SampleEntity.builder().id("id-three").build());
+
+ repository.findById("id-two").as(StepVerifier::create)//
+ .consumeNextWith(it -> {
+ assertThat(it.getId()).isEqualTo("id-two");
+ }) //
+ .verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void findByIdShouldCompleteIfNothingFound() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").build(), //
+ SampleEntity.builder().id("id-two").build(), //
+ SampleEntity.builder().id("id-three").build());
+
+ repository.findById("does-not-exist").as(StepVerifier::create) //
+ .verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void findAllByIdByIdShouldCompleteIfIndexDoesNotExist() {
+ repository.findAllById(Arrays.asList("id-two", "id-two")).as(StepVerifier::create).verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void findAllByIdShouldRetrieveMatchingDocuments() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").build(), //
+ SampleEntity.builder().id("id-two").build(), //
+ SampleEntity.builder().id("id-three").build());
+
+ repository.findAllById(Arrays.asList("id-one", "id-two")) //
+ .as(StepVerifier::create)//
+ .expectNextCount(2) //
+ .verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void findAllByIdShouldCompleteWhenNothingFound() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").build(), //
+ SampleEntity.builder().id("id-two").build(), //
+ SampleEntity.builder().id("id-three").build());
+
+ repository.findAllById(Arrays.asList("can't", "touch", "this")) //
+ .as(StepVerifier::create)//
+ .verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void countShouldReturnZeroWhenIndexDoesNotExist() {
+ repository.count().as(StepVerifier::create).expectNext(0L).verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void countShouldCountDocuments() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").build(), //
+ SampleEntity.builder().id("id-two").build());
+
+ repository.count().as(StepVerifier::create).expectNext(2L).verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void existsByIdShouldReturnTrueIfExists() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").message("message").build(), //
+ SampleEntity.builder().id("id-two").message("test message").build(), //
+ SampleEntity.builder().id("id-three").message("test test").build());
+
+ repository.existsById("id-two") //
+ .as(StepVerifier::create) //
+ .expectNext(true) //
+ .verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void existsByIdShouldReturnFalseIfNotExists() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").message("message").build(), //
+ SampleEntity.builder().id("id-two").message("test message").build(), //
+ SampleEntity.builder().id("id-three").message("test test").build());
+
+ repository.existsById("wrecking ball") //
+ .as(StepVerifier::create) //
+ .expectNext(false) //
+ .verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void countShouldCountMatchingDocuments() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").message("message").build(), //
+ SampleEntity.builder().id("id-two").message("test message").build(), //
+ SampleEntity.builder().id("id-three").message("test test").build());
+
+ repository.countAllByMessage("test") //
+ .as(StepVerifier::create) //
+ .expectNext(2L) //
+ .verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void existsShouldReturnTrueIfExists() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").message("message").build(), //
+ SampleEntity.builder().id("id-two").message("test message").build(), //
+ SampleEntity.builder().id("id-three").message("test test").build());
+
+ repository.existsAllByMessage("message") //
+ .as(StepVerifier::create) //
+ .expectNext(true) //
+ .verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void existsShouldReturnFalseIfNotExists() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").message("message").build(), //
+ SampleEntity.builder().id("id-two").message("test message").build(), //
+ SampleEntity.builder().id("id-three").message("test test").build());
+
+ repository.existsAllByMessage("these days") //
+ .as(StepVerifier::create) //
+ .expectNext(false) //
+ .verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void deleteByIdShouldCompleteIfNothingDeleted() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").build(), //
+ SampleEntity.builder().id("id-two").build());
+
+ repository.deleteById("does-not-exist").as(StepVerifier::create).verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void deleteByIdShouldCompleteWhenIndexDoesNotExist() {
+ repository.deleteById("does-not-exist").as(StepVerifier::create).verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void deleteByIdShouldDeleteEntry() {
+
+ SampleEntity toBeDeleted = SampleEntity.builder().id("id-two").build();
+ bulkIndex(SampleEntity.builder().id("id-one").build(), toBeDeleted);
+
+ repository.deleteById(toBeDeleted.getId()).as(StepVerifier::create).verifyComplete();
+
+ assertThat(TestUtils.documentWithId(toBeDeleted.getId()).ofType(TYPE).existsIn(INDEX)).isFalse();
+ }
+
+ @Test // DATAES-519
+ public void deleteShouldDeleteEntry() {
+
+ SampleEntity toBeDeleted = SampleEntity.builder().id("id-two").build();
+ bulkIndex(SampleEntity.builder().id("id-one").build(), toBeDeleted);
+
+ repository.delete(toBeDeleted).as(StepVerifier::create).verifyComplete();
+
+ assertThat(TestUtils.documentWithId(toBeDeleted.getId()).ofType(TYPE).existsIn(INDEX)).isFalse();
+ }
+
+ @Test // DATAES-519
+ public void deleteAllShouldDeleteGivenEntries() {
+
+ SampleEntity toBeDeleted = SampleEntity.builder().id("id-one").build();
+ SampleEntity hangInThere = SampleEntity.builder().id("id-two").build();
+ SampleEntity toBeDeleted2 = SampleEntity.builder().id("id-three").build();
+
+ bulkIndex(toBeDeleted, hangInThere, toBeDeleted2);
+
+ repository.deleteAll(Arrays.asList(toBeDeleted, toBeDeleted2)).as(StepVerifier::create).verifyComplete();
+
+ assertThat(TestUtils.documentWithId(toBeDeleted.getId()).ofType(TYPE).existsIn(INDEX)).isFalse();
+ assertThat(TestUtils.documentWithId(toBeDeleted2.getId()).ofType(TYPE).existsIn(INDEX)).isFalse();
+ assertThat(TestUtils.documentWithId(hangInThere.getId()).ofType(TYPE).existsIn(INDEX)).isTrue();
+ }
+
+ @Test // DATAES-519
+ public void deleteAllShouldDeleteAllEntries() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").build(), //
+ SampleEntity.builder().id("id-two").build(), //
+ SampleEntity.builder().id("id-three").build());
+
+ repository.deleteAll().as(StepVerifier::create).verifyComplete();
+
+ assertThat(TestUtils.isEmptyIndex(INDEX)).isTrue();
+ }
+
+ @Test // DATAES-519
+ public void derivedFinderMethodShouldBeExecutedCorrectly() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").message("message").build(), //
+ SampleEntity.builder().id("id-two").message("test message").build(), //
+ SampleEntity.builder().id("id-three").message("test test").build());
+
+ repository.findAllByMessageLike("test") //
+ .as(StepVerifier::create) //
+ .expectNextCount(2) //
+ .verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void derivedFinderMethodShouldBeExecutedCorrectlyWhenGivenPublisher() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").message("message").build(), //
+ SampleEntity.builder().id("id-two").message("test message").build(), //
+ SampleEntity.builder().id("id-three").message("test test").build());
+
+ repository.findAllByMessage(Mono.just("test")) //
+ .as(StepVerifier::create) //
+ .expectNextCount(2) //
+ .verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void derivedFinderWithDerivedSortMethodShouldBeExecutedCorrectly() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").message("test").rate(3).build(), //
+ SampleEntity.builder().id("id-two").message("test test").rate(1).build(), //
+ SampleEntity.builder().id("id-three").message("test test").rate(2).build());
+
+ repository.findAllByMessageLikeOrderByRate("test") //
+ .as(StepVerifier::create) //
+ .consumeNextWith(it -> assertThat(it.getId()).isEqualTo("id-two")) //
+ .consumeNextWith(it -> assertThat(it.getId()).isEqualTo("id-three")) //
+ .consumeNextWith(it -> assertThat(it.getId()).isEqualTo("id-one")) //
+ .verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void derivedFinderMethodWithSortParameterShouldBeExecutedCorrectly() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").message("test").rate(3).build(), //
+ SampleEntity.builder().id("id-two").message("test test").rate(1).build(), //
+ SampleEntity.builder().id("id-three").message("test test").rate(2).build());
+
+ repository.findAllByMessage("test", Sort.by(Order.asc("rate"))) //
+ .as(StepVerifier::create) //
+ .consumeNextWith(it -> assertThat(it.getId()).isEqualTo("id-two")) //
+ .consumeNextWith(it -> assertThat(it.getId()).isEqualTo("id-three")) //
+ .consumeNextWith(it -> assertThat(it.getId()).isEqualTo("id-one")) //
+ .verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void derivedFinderMethodWithPageableParameterShouldBeExecutedCorrectly() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").message("test").rate(3).build(), //
+ SampleEntity.builder().id("id-two").message("test test").rate(1).build(), //
+ SampleEntity.builder().id("id-three").message("test test").rate(2).build());
+
+ repository.findAllByMessage("test", PageRequest.of(0, 2, Sort.by(Order.asc("rate")))) //
+ .as(StepVerifier::create) //
+ .consumeNextWith(it -> assertThat(it.getId()).isEqualTo("id-two")) //
+ .consumeNextWith(it -> assertThat(it.getId()).isEqualTo("id-three")) //
+ .verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void derivedFinderMethodReturningMonoShouldBeExecutedCorrectly() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").message("message").build(), //
+ SampleEntity.builder().id("id-two").message("test message").build(), //
+ SampleEntity.builder().id("id-three").message("test test").build());
+
+ repository.findFirstByMessageLike("test") //
+ .as(StepVerifier::create) //
+ .expectNextCount(1) //
+ .verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void annotatedFinderMethodShouldBeExecutedCorrectly() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").message("message").build(), //
+ SampleEntity.builder().id("id-two").message("test message").build(), //
+ SampleEntity.builder().id("id-three").message("test test").build());
+
+ repository.findAllViaAnnotatedQueryByMessageLike("test") //
+ .as(StepVerifier::create) //
+ .expectNextCount(2) //
+ .verifyComplete();
+ }
+
+ @Test // DATAES-519
+ public void derivedDeleteMethodShouldBeExecutedCorrectly() {
+
+ bulkIndex(SampleEntity.builder().id("id-one").message("message").build(), //
+ SampleEntity.builder().id("id-two").message("test message").build(), //
+ SampleEntity.builder().id("id-three").message("test test").build());
+
+ repository.deleteAllByMessage("message") //
+ .as(StepVerifier::create) //
+ .expectNext(2L) //
+ .verifyComplete();
+
+ assertThat(TestUtils.documentWithId("id-one").ofType(TYPE).existsIn(INDEX)).isFalse();
+ assertThat(TestUtils.documentWithId("id-two").ofType(TYPE).existsIn(INDEX)).isFalse();
+ assertThat(TestUtils.documentWithId("id-three").ofType(TYPE).existsIn(INDEX)).isTrue();
+ }
+
+ IndexRequest indexRequest(Map source, String index, String type) {
+
+ return new IndexRequest(index, type) //
+ .id(source.containsKey("id") ? source.get("id").toString() : UUID.randomUUID().toString()) //
+ .source(source) //
+ .create(true);
+ }
+
+ IndexRequest indexRequestFrom(SampleEntity entity) {
+
+ Map target = new LinkedHashMap<>();
+
+ if (StringUtils.hasText(entity.getId())) {
+ target.put("id", entity.getId());
+ }
+
+ if (StringUtils.hasText(entity.getType())) {
+ target.put("type", entity.getType());
+ }
+
+ if (StringUtils.hasText(entity.getMessage())) {
+ target.put("message", entity.getMessage());
+ }
+
+ target.put("rate", entity.getRate());
+ target.put("available", entity.isAvailable());
+
+ return indexRequest(target, INDEX, TYPE);
+ }
+
+ void bulkIndex(SampleEntity... entities) {
+
+ BulkRequest request = new BulkRequest();
+ Arrays.stream(entities).forEach(it -> request.add(indexRequestFrom(it)));
+
+ try (RestHighLevelClient client = TestUtils.restHighLevelClient()) {
+ client.bulk(request.setRefreshPolicy(RefreshPolicy.IMMEDIATE), RequestOptions.DEFAULT);
+ } catch (Exception e) {}
+ }
+
+ interface ReactiveSampleEntityRepository extends ReactiveCrudRepository {
+
+ Flux findAllByMessageLike(String message);
+
+ Flux findAllByMessageLikeOrderByRate(String message);
+
+ Flux findAllByMessage(String message, Sort sort);
+
+ Flux findAllByMessage(String message, Pageable pageable);
+
+ Flux findAllByMessage(Publisher message);
+
+ @Query("{ \"bool\" : { \"must\" : { \"term\" : { \"message\" : \"?0\" } } } }")
+ Flux findAllViaAnnotatedQueryByMessageLike(String message);
+
+ Mono findFirstByMessageLike(String message);
+
+ Mono countAllByMessage(String message);
+
+ Mono existsAllByMessage(String message);
+
+ Mono deleteAllByMessage(String message);
+ }
+}