DATAES-504 - Add ReactiveElasticsearchOperations & ReactiveElasticsearchTemplate

ReactiveElasticsearchOperations is the gateway to executing high level commands against an Elasticsearch cluster using the ReactiveElasticsearchClient.

The ReactiveElasticsearchTemplate is the default implementation of ReactiveElasticsearchOperations and offers the following set of features.

* Read/Write mapping support for domain types.
* A rich query and criteria api.
* Resource management and Exception translation.

To get started the ReactiveElasticsearchTemplate needs to know about the actual client to work with.
The easiest way of setting up the ReactiveElasticsearchTemplate is via AbstractReactiveElasticsearchConfiguration providing
dedicated configuration method hooks for base package, the initial entity set etc.

@Configuration
public class Config extends AbstractReactiveElasticsearchConfiguration {

    @Bean
    @Override
    public ReactiveElasticsearchClient reactiveElasticsearchClient() {
        // ...
    }
}

NOTE: If applicable set default HttpHeaders via the ClientConfiguration of the ReactiveElasticsearchClient.

TIP: If needed the ReactiveElasticsearchTemplate can be configured with default RefreshPolicy and IndicesOptions that get applied to the related requests by overriding the defaults of refreshPolicy() and indicesOptions().

The ReactiveElasticsearchTemplate lets you save, find and delete your domain objects and map those objects to documents stored in Elasticsearch.

@Document(indexName = "marvel", type = "characters")
public class Person {

    private @Id String id;
    private String name;
    private int age;

    // Getter/Setter omitted...
}

template.save(new Person("Bruce Banner", 42)) // save a new document
    .doOnNext(System.out::println)
    .flatMap(person -> template.findById(person.id, Person.class)) // then go find it
    .doOnNext(System.out::println)
    .flatMap(person -> template.delete(person)) // just to remove remove it again
    .doOnNext(System.out::println)
    .flatMap(id -> template.count(Person.class)) // so we've got nothing at the end
    .doOnNext(System.out::println)
    .subscribe(); // yeah :)

The above outputs the following sequence on the console.

> Person(id=QjWCWWcBXiLAnp77ksfR, name=Bruce Banner, age=42)
> Person(id=QjWCWWcBXiLAnp77ksfR, name=Bruce Banner, age=42)
> QjWCWWcBXiLAnp77ksfR
> 0

Original Pull Request: #229
This commit is contained in:
Christoph Strobl
2018-11-23 15:00:59 +01:00
parent a39c34058b
commit ba890cb7eb
22 changed files with 2666 additions and 251 deletions

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2018 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 java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Christoph Strobl
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface ElasticsearchVersion {
/**
* Inclusive lower bound of Elasticsearch server range.
*
* @return {@code 0.0.0} by default.
*/
String asOf() default "0.0.0";
/**
* Exclusive upper bound of Elasticsearch server range.
*
* @return {@code 9999.9999.9999} by default.
*/
String until() default "9999.9999.9999";
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2018 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 java.util.concurrent.atomic.AtomicReference;
import org.junit.AssumptionViolatedException;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.util.Version;
/**
* @author Christoph Strobl
*/
public class ElasticsearchVersionRule implements TestRule {
private static final Logger logger = LoggerFactory.getLogger(ElasticsearchVersionRule.class);
private static final Version ANY = new Version(9999, 9999, 9999);
private static final Version DEFAULT_HIGH = ANY;
private static final Version DEFAULT_LOW = new Version(0, 0, 0);
private final static AtomicReference<Version> currentVersion = new AtomicReference<>(null);
private final Version minVersion;
private final Version maxVersion;
public ElasticsearchVersionRule(Version min, Version max) {
this.minVersion = min;
this.maxVersion = max;
}
public static ElasticsearchVersionRule any() {
return new ElasticsearchVersionRule(ANY, ANY);
}
public static ElasticsearchVersionRule atLeast(Version minVersion) {
return new ElasticsearchVersionRule(minVersion, DEFAULT_HIGH);
}
public static ElasticsearchVersionRule atMost(Version maxVersion) {
return new ElasticsearchVersionRule(DEFAULT_LOW, maxVersion);
}
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
if (!getCurrentVersion().equals(ANY)) {
Version minVersion = ElasticsearchVersionRule.this.minVersion.equals(ANY) ? DEFAULT_LOW
: ElasticsearchVersionRule.this.minVersion;
Version maxVersion = ElasticsearchVersionRule.this.maxVersion.equals(ANY) ? DEFAULT_HIGH
: ElasticsearchVersionRule.this.maxVersion;
if (description.getAnnotation(ElasticsearchVersion.class) != null) {
ElasticsearchVersion version = description.getAnnotation(ElasticsearchVersion.class);
if (version != null) {
Version expectedMinVersion = Version.parse(version.asOf());
if (!expectedMinVersion.equals(ANY) && !expectedMinVersion.equals(DEFAULT_LOW)) {
minVersion = expectedMinVersion;
}
Version expectedMaxVersion = Version.parse(version.until());
if (!expectedMaxVersion.equals(ANY) && !expectedMaxVersion.equals(DEFAULT_HIGH)) {
maxVersion = expectedMaxVersion;
}
}
}
validateVersion(minVersion, maxVersion);
}
base.evaluate();
}
};
}
private void validateVersion(Version min, Version max) {
if (getCurrentVersion().isLessThan(min) || getCurrentVersion().isGreaterThanOrEqualTo(max)) {
throw new AssumptionViolatedException(String
.format("Expected Elasticsearch server to be in range (%s, %s] but found %s", min, max, currentVersion));
}
}
private Version getCurrentVersion() {
if (currentVersion.get() == null) {
Version current = fetchCurrentVersion();
if (currentVersion.compareAndSet(null, current)) {
logger.info("Running Elasticsearch " + current);
}
}
return currentVersion.get();
}
private Version fetchCurrentVersion() {
return TestUtils.serverVersion();
}
@Override
public String toString() {
return getCurrentVersion().toString();
}
}

View File

@@ -17,15 +17,20 @@ package org.springframework.data.elasticsearch;
import lombok.SneakyThrows;
import java.io.IOException;
import org.elasticsearch.ElasticsearchStatusException;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.RestClients;
import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient;
import org.springframework.data.elasticsearch.client.reactive.ReactiveRestClients;
import org.springframework.data.util.Version;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* @author Christoph Strobl
@@ -44,6 +49,18 @@ public final class TestUtils {
return ReactiveRestClients.create(ClientConfiguration.create("localhost:9200"));
}
public static Version serverVersion() {
try (RestHighLevelClient client = restHighLevelClient()) {
org.elasticsearch.Version version = client.info(RequestOptions.DEFAULT).getVersion();
return new Version(version.major, version.minor, version.revision);
} catch (Exception e) {
return new Version(0, 0, 0);
}
}
@SneakyThrows
public static void deleteIndex(String... indexes) {
@@ -62,4 +79,48 @@ public final class TestUtils {
}
}
}
public static OfType documentWithId(String id) {
return new DocumentLookup(id);
}
public interface ExistsIn {
boolean existsIn(String index);
}
public interface OfType extends ExistsIn {
ExistsIn ofType(String type);
}
private static class DocumentLookup implements OfType {
private String id;
private String type;
public DocumentLookup(String id) {
this.id = id;
}
@Override
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();
}
return false;
}
@Override
public ExistsIn ofType(String type) {
this.type = type;
return this;
}
}
}

View File

@@ -17,6 +17,9 @@ package org.springframework.data.elasticsearch.client.reactive;
import static org.assertj.core.api.Assertions.*;
import org.junit.Rule;
import org.springframework.data.elasticsearch.ElasticsearchVersion;
import org.springframework.data.elasticsearch.ElasticsearchVersionRule;
import reactor.test.StepVerifier;
import java.io.IOException;
@@ -37,6 +40,7 @@ import org.elasticsearch.action.support.WriteRequest.RefreshPolicy;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.junit.After;
@@ -58,6 +62,8 @@ import org.springframework.test.context.junit4.SpringRunner;
@ContextConfiguration("classpath:infrastructure.xml")
public class ReactiveElasticsearchClientTests {
public @Rule ElasticsearchVersionRule elasticsearchVersion = ElasticsearchVersionRule.any();
static final String INDEX_I = "idx-1-reactive-client-tests";
static final String INDEX_II = "idx-2-reactive-client-tests";
@@ -413,6 +419,43 @@ public class ReactiveElasticsearchClientTests {
.verifyComplete();
}
@Test // DATAES-488
@ElasticsearchVersion(asOf = "6.5.0")
public void deleteByShouldRemoveExistingDocument() {
String id = addSourceDocument().ofType(TYPE_I).to(INDEX_I);
DeleteByQueryRequest request = new DeleteByQueryRequest(INDEX_I) //
.setDocTypes(TYPE_I) //
.setQuery(QueryBuilders.boolQuery().must(QueryBuilders.termQuery("_id", id)));
client.deleteBy(request) //
.as(StepVerifier::create) //
.consumeNextWith(it -> {
assertThat(it.getDeleted()).isEqualTo(1);
}) //
.verifyComplete();
}
@Test // DATAES-488
@ElasticsearchVersion(asOf = "6.5.0")
public void deleteByEmitResultWhenNothingRemoved() {
addSourceDocument().ofType(TYPE_I).to(INDEX_I);
DeleteByQueryRequest request = new DeleteByQueryRequest(INDEX_I) //
.setDocTypes(TYPE_I) //
.setQuery(QueryBuilders.boolQuery().must(QueryBuilders.termQuery("_id", "it-was-not-me")));
client.deleteBy(request) //
.as(StepVerifier::create) //
.consumeNextWith(it -> {
assertThat(it.getDeleted()).isEqualTo(0);
}) //
.verifyComplete();
}
AddToIndexOfType addSourceDocument() {
return add(DOC_SOURCE);
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2018 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.config;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.Collection;
import java.util.Collections;
import org.apache.commons.lang.ClassUtils;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
/**
* @author Christoph Strobl
*/
public class ElasticsearchConfigurationSupportUnitTests {
@Test // DATAES-504
public void usesConfigClassPackageAsBaseMappingPackage() throws ClassNotFoundException {
ElasticsearchConfigurationSupport configuration = new StubConfig();
assertThat(configuration.getMappingBasePackages()).contains(ClassUtils.getPackageName(StubConfig.class));
assertThat(configuration.getInitialEntitySet()).contains(Entity.class);
}
@Test // DATAES-504
public void doesNotScanOnEmptyBasePackage() throws ClassNotFoundException {
ElasticsearchConfigurationSupport configuration = new StubConfig() {
@Override
protected Collection<String> getMappingBasePackages() {
return Collections.emptySet();
}
};
assertThat(configuration.getInitialEntitySet()).isEmpty();
}
@Test // DATAES-504
public void containsMappingContext() {
AbstractApplicationContext context = new AnnotationConfigApplicationContext(StubConfig.class);
assertThat(context.getBean(SimpleElasticsearchMappingContext.class)).isNotNull();
}
@Test // DATAES-504
public void containsElasticsearchConverter() {
AbstractApplicationContext context = new AnnotationConfigApplicationContext(StubConfig.class);
assertThat(context.getBean(ElasticsearchConverter.class)).isNotNull();
}
@Test // DATAES-504
public void restConfigContainsElasticsearchTemplate() {
AbstractApplicationContext context = new AnnotationConfigApplicationContext(RestConfig.class);
assertThat(context.getBean(ElasticsearchRestTemplate.class)).isNotNull();
}
@Test // DATAES-504
public void reactiveConfigContainsReactiveElasticsearchTemplate() {
AbstractApplicationContext context = new AnnotationConfigApplicationContext(ReactiveRestConfig.class);
assertThat(context.getBean(ReactiveElasticsearchTemplate.class)).isNotNull();
}
@Configuration
static class StubConfig extends ElasticsearchConfigurationSupport {
}
@Configuration
static class ReactiveRestConfig extends AbstractReactiveElasticsearchConfiguration {
@Override
public ReactiveElasticsearchClient reactiveElasticsearchClient() {
return mock(ReactiveElasticsearchClient.class);
}
}
@Configuration
static class RestConfig extends AbstractElasticsearchConfiguration {
@Override
public RestHighLevelClient elasticsearchClient() {
return mock(RestHighLevelClient.class);
}
}
@Document(indexName = "config-support-tests")
static class Entity {}
}

View File

@@ -15,24 +15,42 @@
*/
package org.springframework.data.elasticsearch.core;
import static org.apache.commons.lang.RandomStringUtils.*;
import static org.assertj.core.api.Assertions.*;
import static org.elasticsearch.index.query.QueryBuilders.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.junit.Rule;
import org.springframework.data.elasticsearch.ElasticsearchVersion;
import org.springframework.data.elasticsearch.ElasticsearchVersionRule;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.net.ConnectException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.TestUtils;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.IndexQueryBuilder;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.entities.SampleEntity;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.StringUtils;
/**
* @author Christoph Strobl
@@ -42,16 +60,20 @@ import org.springframework.test.context.junit4.SpringRunner;
@ContextConfiguration("classpath:infrastructure.xml")
public class ReactiveElasticsearchTemplateTests {
public @Rule ElasticsearchVersionRule elasticsearchVersion = ElasticsearchVersionRule.any();
static final String DEFAULT_INDEX = "test-index-sample";
static final String ALTERNATE_INDEX = "reactive-template-tests-alternate-index";
private ElasticsearchRestTemplate restTemplate;
private ReactiveElasticsearchTemplate template;
@Before
public void setUp() {
TestUtils.deleteIndex(DEFAULT_INDEX, ALTERNATE_INDEX);
restTemplate = new ElasticsearchRestTemplate(TestUtils.restHighLevelClient());
TestUtils.deleteIndex("test-index-sample");
restTemplate.createIndex(SampleEntity.class);
restTemplate.putMapping(SampleEntity.class);
restTemplate.refresh(SampleEntity.class);
@@ -59,14 +81,35 @@ public class ReactiveElasticsearchTemplateTests {
template = new ReactiveElasticsearchTemplate(TestUtils.reactiveClient());
}
@Test // DATAES-488
public void indexWithIdShouldWork() {
@Test // DATAES-504
public void executeShouldProvideResource() {
String documentId = randomNumeric(5);
SampleEntity sampleEntity = SampleEntity.builder().id(documentId).message("foo bar")
.version(System.currentTimeMillis()).build();
Mono.from(template.execute(client -> client.ping())) //
.as(StepVerifier::create) //
.expectNext(true) //
.verifyComplete();
}
template.index(sampleEntity).as(StepVerifier::create).expectNextCount(1).verifyComplete();
@Test // DATAES-504
public void executeShouldConvertExceptions() {
Mono.from(template.execute(client -> {
throw new RuntimeException(new ConnectException("we're doomed"));
})) //
.as(StepVerifier::create) //
.expectError(DataAccessResourceFailureException.class) //
.verify();
}
@Test // DATAES-504
public void insertWithIdShouldWork() {
SampleEntity sampleEntity = randomEntity("foo bar");
template.save(sampleEntity)//
.as(StepVerifier::create)//
.expectNextCount(1)//
.verifyComplete();
restTemplate.refresh(SampleEntity.class);
@@ -75,78 +118,388 @@ public class ReactiveElasticsearchTemplateTests {
assertThat(result).hasSize(1);
}
@Test // DATAES-488
public void getShouldReturnEntity() {
@Test // DATAES-504
public void insertWithAutogeneratedIdShouldUpdateEntityId() {
String documentId = randomNumeric(5);
SampleEntity sampleEntity = SampleEntity.builder().id(documentId).message("some message")
.version(System.currentTimeMillis()).build();
SampleEntity sampleEntity = SampleEntity.builder().message("wohoo").build();
IndexQuery indexQuery = getIndexQuery(sampleEntity);
restTemplate.index(indexQuery);
restTemplate.refresh(SampleEntity.class);
template.save(sampleEntity) //
.as(StepVerifier::create) //
.consumeNextWith(it -> {
template.get(documentId, SampleEntity.class) //
assertThat(it.getId()).isNotNull();
restTemplate.refresh(SampleEntity.class);
assertThat(TestUtils.documentWithId(it.getId()).existsIn(DEFAULT_INDEX)).isTrue();
}) //
.verifyComplete();
}
@Test // DATAES-504
public void insertWithExplicitIndexNameShouldOverwriteMetadata() {
SampleEntity sampleEntity = randomEntity("in another index");
template.save(sampleEntity, ALTERNATE_INDEX).as(StepVerifier::create)//
.expectNextCount(1)//
.verifyComplete();
restTemplate.refresh(DEFAULT_INDEX);
restTemplate.refresh(ALTERNATE_INDEX);
assertThat(TestUtils.documentWithId(sampleEntity.getId()).existsIn(DEFAULT_INDEX)).isFalse();
assertThat(TestUtils.documentWithId(sampleEntity.getId()).existsIn(ALTERNATE_INDEX)).isTrue();
}
@Test // DATAES-504
public void insertShouldAcceptPlainMapStructureAsSource() {
Map<String, Object> map = Collections.singletonMap("foo", "bar");
template.save(map, ALTERNATE_INDEX, "singleton-map") //
.as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
}
@Test(expected = IllegalArgumentException.class) // DATAES-504
public void insertShouldErrorOnNullEntity() {
template.save(null);
}
@Test // DATAES-504
public void findByIdShouldReturnEntity() {
SampleEntity sampleEntity = randomEntity("some message");
index(sampleEntity);
template.findById(sampleEntity.getId(), SampleEntity.class) //
.as(StepVerifier::create) //
.expectNext(sampleEntity) //
.verifyComplete();
}
@Test // DATAES-488
public void getForNothing() {
@Test // DATAES-504
public void findByIdWhenIdIsAutogeneratedShouldHaveIdSetCorrectly() {
String documentId = randomNumeric(5);
SampleEntity sampleEntity = SampleEntity.builder().id(documentId).message("some message")
.version(System.currentTimeMillis()).build();
SampleEntity sampleEntity = new SampleEntity();
sampleEntity.setMessage("some message");
IndexQuery indexQuery = getIndexQuery(sampleEntity);
restTemplate.index(indexQuery);
restTemplate.refresh(SampleEntity.class);
index(sampleEntity);
template.get("foo", SampleEntity.class) //
assertThat(sampleEntity.getId()).isNotNull();
template.findById(sampleEntity.getId(), SampleEntity.class) //
.as(StepVerifier::create) //
.consumeNextWith(it -> assertThat(it.getId()).isEqualTo(sampleEntity.getId())) //
.verifyComplete();
}
@Test // DATAES-504
public void findByIdShouldCompleteWhenNotingFound() {
SampleEntity sampleEntity = randomEntity("some message");
index(sampleEntity);
template.findById("foo", SampleEntity.class) //
.as(StepVerifier::create) //
.verifyComplete();
}
@Test // DATAES-488
public void findShouldApplyCriteria() {
@Test(expected = IllegalArgumentException.class) // DATAES-504
public void findByIdShouldErrorForNullId() {
template.findById(null, SampleEntity.class);
}
String documentId = randomNumeric(5);
SampleEntity sampleEntity = SampleEntity.builder().id(documentId).message("some message")
.version(System.currentTimeMillis()).build();
@Test // DATAES-504
public void findByIdWithExplicitIndexNameShouldOverwriteMetadata() {
SampleEntity sampleEntity = randomEntity("some message");
IndexQuery indexQuery = getIndexQuery(sampleEntity);
indexQuery.setIndexName(ALTERNATE_INDEX);
restTemplate.index(indexQuery);
restTemplate.refresh(SampleEntity.class);
restTemplate.refresh(DEFAULT_INDEX);
restTemplate.refresh(ALTERNATE_INDEX);
template.findById(sampleEntity.getId(), SampleEntity.class) //
.as(StepVerifier::create) //
.verifyComplete();
template.findById(sampleEntity.getId(), SampleEntity.class, ALTERNATE_INDEX) //
.as(StepVerifier::create)//
.expectNextCount(1) //
.verifyComplete();
}
@Test // DATAES-504
public void existsShouldReturnTrueWhenFound() {
SampleEntity sampleEntity = randomEntity("some message");
index(sampleEntity);
template.exists(sampleEntity.getId(), SampleEntity.class) //
.as(StepVerifier::create) //
.expectNext(true) //
.verifyComplete();
}
@Test // DATAES-504
public void existsShouldReturnFalseWhenNotFound() {
SampleEntity sampleEntity = randomEntity("some message");
index(sampleEntity);
template.exists("foo", SampleEntity.class) //
.as(StepVerifier::create) //
.expectNext(false) //
.verifyComplete();
}
@Test // DATAES-504
public void findShouldApplyCriteria() {
SampleEntity sampleEntity = randomEntity("some message");
index(sampleEntity);
CriteriaQuery criteriaQuery = new CriteriaQuery(Criteria.where("message").is("some message"));
template.query(criteriaQuery, SampleEntity.class) //
template.find(criteriaQuery, SampleEntity.class) //
.as(StepVerifier::create) //
.expectNext(sampleEntity) //
.verifyComplete();
}
@Test // DATAES-488
@Test // DATAES-504
public void findShouldReturnEmptyFluxIfNothingFound() {
String documentId = randomNumeric(5);
SampleEntity sampleEntity = SampleEntity.builder().id(documentId).message("some message")
.version(System.currentTimeMillis()).build();
IndexQuery indexQuery = getIndexQuery(sampleEntity);
restTemplate.index(indexQuery);
restTemplate.refresh(SampleEntity.class);
SampleEntity sampleEntity = randomEntity("some message");
index(sampleEntity);
CriteriaQuery criteriaQuery = new CriteriaQuery(Criteria.where("message").is("foo"));
template.query(criteriaQuery, SampleEntity.class) //
template.find(criteriaQuery, SampleEntity.class) //
.as(StepVerifier::create) //
.verifyComplete();
}
@Test // DATAES-504
public void shouldAllowStringBasedQuery() {
index(randomEntity("test message"), randomEntity("test test"), randomEntity("some message"));
template.find(new StringQuery(matchAllQuery().toString()), SampleEntity.class) //
.as(StepVerifier::create) //
.expectNextCount(3) //
.verifyComplete();
}
@Test // DATAES-504
public void shouldExecuteGivenCriteriaQuery() {
SampleEntity shouldMatch = randomEntity("test message");
SampleEntity shouldNotMatch = randomEntity("the dog ate my homework");
index(shouldMatch, shouldNotMatch);
CriteriaQuery query = new CriteriaQuery(new Criteria("message").contains("test"));
template.find(query, SampleEntity.class) //
.as(StepVerifier::create) //
.expectNext(shouldMatch) //
.verifyComplete();
}
@Test // DATAES-504
public void shouldReturnListForGivenCriteria() {
SampleEntity sampleEntity1 = randomEntity("test message");
SampleEntity sampleEntity2 = randomEntity("test test");
SampleEntity sampleEntity3 = randomEntity("some message");
index(sampleEntity1, sampleEntity2, sampleEntity3);
CriteriaQuery query = new CriteriaQuery(
new Criteria("message").contains("some").and("message").contains("message"));
template.find(query, SampleEntity.class) //
.as(StepVerifier::create) //
.expectNext(sampleEntity3) //
.verifyComplete();
}
@Test // DATAES-504
public void shouldReturnProjectedTargetEntity() {
SampleEntity sampleEntity1 = randomEntity("test message");
SampleEntity sampleEntity2 = randomEntity("test test");
SampleEntity sampleEntity3 = randomEntity("some message");
index(sampleEntity1, sampleEntity2, sampleEntity3);
CriteriaQuery query = new CriteriaQuery(
new Criteria("message").contains("some").and("message").contains("message"));
template.find(query, SampleEntity.class, Message.class) //
.as(StepVerifier::create) //
.expectNext(new Message(sampleEntity3.getMessage())) //
.verifyComplete();
}
@Test // DATAES-504
public void countShouldReturnCountAllWhenGivenNoQuery() {
index(randomEntity("test message"), randomEntity("test test"), randomEntity("some message"));
template.count(SampleEntity.class) //
.as(StepVerifier::create) //
.expectNext(3L) //
.verifyComplete();
}
@Test // DATAES-504
public void countShouldReturnCountMatchingDocuments() {
index(randomEntity("test message"), randomEntity("test test"), randomEntity("some message"));
CriteriaQuery query = new CriteriaQuery(new Criteria("message").contains("test"));
template.count(query, SampleEntity.class) //
.as(StepVerifier::create) //
.expectNext(2L) //
.verifyComplete();
}
@Test // DATAES-504
public void deleteByIdShouldRemoveExistingDocumentById() {
SampleEntity sampleEntity = randomEntity("test message");
index(sampleEntity);
template.deleteById(sampleEntity.getId(), SampleEntity.class) //
.as(StepVerifier::create)//
.expectNext(sampleEntity.getId()) //
.verifyComplete();
}
@Test // DATAES-504
public void deleteShouldRemoveExistingDocumentByIdUsingIndexName() {
SampleEntity sampleEntity = randomEntity("test message");
index(sampleEntity);
template.deleteById(sampleEntity.getId(), DEFAULT_INDEX, "test-type") //
.as(StepVerifier::create)//
.expectNext(sampleEntity.getId()) //
.verifyComplete();
}
@Test // DATAES-504
public void deleteShouldRemoveExistingDocument() {
SampleEntity sampleEntity = randomEntity("test message");
index(sampleEntity);
template.delete(sampleEntity) //
.as(StepVerifier::create)//
.expectNext(sampleEntity.getId()) //
.verifyComplete();
}
@Test // DATAES-504
public void deleteByIdShouldCompleteWhenNothingDeleted() {
SampleEntity sampleEntity = randomEntity("test message");
template.delete(sampleEntity) //
.as(StepVerifier::create)//
.verifyComplete();
}
@Test // DATAES-504
@ElasticsearchVersion(asOf = "6.5.0")
public void deleteByQueryShouldReturnNumberOfDeletedDocuments() {
index(randomEntity("test message"), randomEntity("test test"), randomEntity("some message"));
CriteriaQuery query = new CriteriaQuery(new Criteria("message").contains("test"));
template.deleteBy(query, SampleEntity.class) //
.as(StepVerifier::create) //
.expectNext(2L) //
.verifyComplete();
}
@Test // DATAES-504
@ElasticsearchVersion(asOf = "6.5.0")
public void deleteByQueryShouldReturnZeroIfNothingDeleted() {
index(randomEntity("test message"));
CriteriaQuery query = new CriteriaQuery(new Criteria("message").contains("luke"));
template.deleteBy(query, SampleEntity.class) //
.as(StepVerifier::create) //
.expectNext(0L) //
.verifyComplete();
}
@Data
@Document(indexName = "marvel", type = "characters")
static class Person {
private @Id String id;
private String name;
private int age;
public Person() {}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
// TODO: check field mapping !!!
// --> JUST some helpers
private SampleEntity randomEntity(String message) {
return SampleEntity.builder() //
.id(UUID.randomUUID().toString()) //
.message(StringUtils.hasText(message) ? message : "test message") //
.version(System.currentTimeMillis()).build();
}
private IndexQuery getIndexQuery(SampleEntity sampleEntity) {
return new IndexQueryBuilder().withId(sampleEntity.getId()).withObject(sampleEntity)
.withVersion(sampleEntity.getVersion()).build();
}
private List<IndexQuery> getIndexQueries(SampleEntity... sampleEntities) {
return Arrays.stream(sampleEntities).map(this::getIndexQuery).collect(Collectors.toList());
}
private void index(SampleEntity... entities) {
if (entities.length == 1) {
restTemplate.index(getIndexQuery(entities[0]));
} else {
restTemplate.bulkIndex(getIndexQueries(entities));
}
restTemplate.refresh(SampleEntity.class);
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Message {
String message;
}
}

View File

@@ -0,0 +1,204 @@
/*
* Copyright 2018 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.core;
import static org.assertj.core.api.Assertions.*;
import static org.elasticsearch.action.search.SearchRequest.*;
import static org.mockito.Mockito.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.Collections;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.support.IndicesOptions;
import org.elasticsearch.action.support.WriteRequest.RefreshPolicy;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.entities.SampleEntity;
/**
* @author Christoph Strobl
* @currentRead Fool's Fate - Robin Hobb
*/
public class ReactiveElasticsearchTemplateUnitTests {
@Rule //
public MockitoRule rule = MockitoJUnit.rule();
@Mock ReactiveElasticsearchClient client;
ReactiveElasticsearchTemplate template;
@Before
public void setUp() {
template = new ReactiveElasticsearchTemplate(client);
}
@Test // DATAES-504
public void insertShouldUseDefaultRefreshPolicy() {
ArgumentCaptor<IndexRequest> captor = ArgumentCaptor.forClass(IndexRequest.class);
when(client.index(captor.capture())).thenReturn(Mono.empty());
template.save(Collections.singletonMap("key", "value"), "index", "type") //
.as(StepVerifier::create) //
.verifyComplete();
assertThat(captor.getValue().getRefreshPolicy()).isEqualTo(RefreshPolicy.IMMEDIATE);
}
@Test // DATAES-504
public void insertShouldApplyRefreshPolicy() {
ArgumentCaptor<IndexRequest> captor = ArgumentCaptor.forClass(IndexRequest.class);
when(client.index(captor.capture())).thenReturn(Mono.empty());
template.setRefreshPolicy(RefreshPolicy.WAIT_UNTIL);
template.save(Collections.singletonMap("key", "value"), "index", "type") //
.as(StepVerifier::create) //
.verifyComplete();
assertThat(captor.getValue().getRefreshPolicy()).isEqualTo(RefreshPolicy.WAIT_UNTIL);
}
@Test // DATAES-504
public void findShouldFallBackToDefaultIndexOptionsIfNotSet() {
ArgumentCaptor<SearchRequest> captor = ArgumentCaptor.forClass(SearchRequest.class);
when(client.search(captor.capture())).thenReturn(Flux.empty());
template.find(new CriteriaQuery(new Criteria("*")), SampleEntity.class) //
.as(StepVerifier::create) //
.verifyComplete();
assertThat(captor.getValue().indicesOptions()).isEqualTo(DEFAULT_INDICES_OPTIONS);
}
@Test // DATAES-504
public void findShouldApplyIndexOptionsIfSet() {
ArgumentCaptor<SearchRequest> captor = ArgumentCaptor.forClass(SearchRequest.class);
when(client.search(captor.capture())).thenReturn(Flux.empty());
template.setIndicesOptions(IndicesOptions.LENIENT_EXPAND_OPEN);
template.find(new CriteriaQuery(new Criteria("*")), SampleEntity.class) //
.as(StepVerifier::create) //
.verifyComplete();
assertThat(captor.getValue().indicesOptions()).isEqualTo(IndicesOptions.LENIENT_EXPAND_OPEN);
}
@Test // DATAES-504
public void deleteShouldUseDefaultRefreshPolicy() {
ArgumentCaptor<DeleteRequest> captor = ArgumentCaptor.forClass(DeleteRequest.class);
when(client.delete(captor.capture())).thenReturn(Mono.empty());
template.deleteById("id", "index", "type") //
.as(StepVerifier::create) //
.verifyComplete();
assertThat(captor.getValue().getRefreshPolicy()).isEqualTo(RefreshPolicy.IMMEDIATE);
}
@Test // DATAES-504
public void deleteShouldApplyRefreshPolicy() {
ArgumentCaptor<DeleteRequest> captor = ArgumentCaptor.forClass(DeleteRequest.class);
when(client.delete(captor.capture())).thenReturn(Mono.empty());
template.setRefreshPolicy(RefreshPolicy.WAIT_UNTIL);
template.deleteById("id", "index", "type") //
.as(StepVerifier::create) //
.verifyComplete();
assertThat(captor.getValue().getRefreshPolicy()).isEqualTo(RefreshPolicy.WAIT_UNTIL);
}
@Test // DATAES-504
public void deleteByShouldUseDefaultRefreshPolicy() {
ArgumentCaptor<DeleteByQueryRequest> captor = ArgumentCaptor.forClass(DeleteByQueryRequest.class);
when(client.deleteBy(captor.capture())).thenReturn(Mono.empty());
template.deleteBy(new StringQuery(QueryBuilders.matchAllQuery().toString()), Object.class, "index", "type") //
.as(StepVerifier::create) //
.verifyComplete();
assertThat(captor.getValue().isRefresh()).isTrue();
}
@Test // DATAES-504
public void deleteByShouldApplyRefreshPolicy() {
ArgumentCaptor<DeleteByQueryRequest> captor = ArgumentCaptor.forClass(DeleteByQueryRequest.class);
when(client.deleteBy(captor.capture())).thenReturn(Mono.empty());
template.setRefreshPolicy(RefreshPolicy.NONE);
template.deleteBy(new StringQuery(QueryBuilders.matchAllQuery().toString()), Object.class, "index", "type") //
.as(StepVerifier::create) //
.verifyComplete();
assertThat(captor.getValue().isRefresh()).isFalse();
}
@Test // DATAES-504
public void deleteByShouldApplyIndicesOptions() {
ArgumentCaptor<DeleteByQueryRequest> captor = ArgumentCaptor.forClass(DeleteByQueryRequest.class);
when(client.deleteBy(captor.capture())).thenReturn(Mono.empty());
template.deleteBy(new StringQuery(QueryBuilders.matchAllQuery().toString()), Object.class, "index", "type") //
.as(StepVerifier::create) //
.verifyComplete();
assertThat(captor.getValue().indicesOptions()).isEqualTo(DEFAULT_INDICES_OPTIONS);
}
@Test // DATAES-504
public void deleteByShouldApplyIndicesOptionsIfSet() {
ArgumentCaptor<DeleteByQueryRequest> captor = ArgumentCaptor.forClass(DeleteByQueryRequest.class);
when(client.deleteBy(captor.capture())).thenReturn(Mono.empty());
template.setIndicesOptions(IndicesOptions.LENIENT_EXPAND_OPEN);
template.deleteBy(new StringQuery(QueryBuilders.matchAllQuery().toString()), Object.class, "index", "type") //
.as(StepVerifier::create) //
.verifyComplete();
assertThat(captor.getValue().indicesOptions()).isEqualTo(IndicesOptions.LENIENT_EXPAND_OPEN);
}
}