diff --git a/README.md b/README.md index 1b8fc430..4e6294ec 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ We have separate folders for the samples of individual modules: ## Spring Data Elasticsearch * `example` - Example how to use basic text search, geo-spatial search and facets. +* `reactive` - Example how to use reactive client, template and repository features. ## Spring Data Neo4j diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml index 116ed244..3dd6360b 100644 --- a/elasticsearch/pom.xml +++ b/elasticsearch/pom.xml @@ -17,6 +17,7 @@ example + reactive @@ -30,13 +31,13 @@ spring-data-next-releasetrain - 6.6.0 + 6.6.1 spring-data-next-releasetrain-released - 6.6.0 + 6.6.1 diff --git a/elasticsearch/reactive/.gitignore b/elasticsearch/reactive/.gitignore new file mode 100644 index 00000000..e50bef9c --- /dev/null +++ b/elasticsearch/reactive/.gitignore @@ -0,0 +1,13 @@ +.project +.classpath +.springBeans +.settings/ +target/ + +#IntelliJ Stuff +.idea +*.iml + +##ignore local node data files for unit tests +/data +/.DS_Store diff --git a/elasticsearch/reactive/README.md b/elasticsearch/reactive/README.md new file mode 100644 index 00000000..1a27d07d --- /dev/null +++ b/elasticsearch/reactive/README.md @@ -0,0 +1,58 @@ +# Spring Data Elasticsearch - Reactive Examples + +The `ReactiveElasticsearchClient` is a non official driver based on `WebClient`. +It uses the request/response objects provided by the Elasticsearch core project. +Calls are directly operated on the reactive stack, not wrapping async (thread pool bound) responses into reactive types. + +````java +class ApplicationConfiguration extends AbstractReactiveElasticsearchConfiguration { + + @Bean + @Override + public ReactiveElasticsearchClient reactiveElasticsearchClient() { + return ReactiveRestClients.create(ClientConfiguration.localhost()); + } +} +```` + +The `ReactiveElasticsearchClient` can be used with the `ReactiveElasticsearchOperations` and `ReactiveElasticsearchRepository`. + +```java +@Autowired ReactiveElasticsearchOperations operations; + +// ... + +CriteriaQuery query = new CriteriaQuery("keywords").contains("java"); + +Flux result = operations.find(query, Conference.class); +``` + +```java +interface ConferenceRepository extends ReactiveCrudRepository { + + Flux findAllByKeywordsContains(String keyword); +} + +// ... + +@Autowired ConferenceRepository repository; + +// ... + +Flux result = repository.findAllByKeywordsContains("java"); +``` + + +**Requirements:** + + * [Maven](http://maven.apache.org/download.cgi) + * [Elasticsearch](https://www.elastic.co/de/downloads/elasticsearch) + +**Running Elasticsearch** + +```bash +$ cd elasticsearch +$ ./bin/elasticsearch +``` + + diff --git a/elasticsearch/reactive/pom.xml b/elasticsearch/reactive/pom.xml new file mode 100644 index 00000000..b24fac9b --- /dev/null +++ b/elasticsearch/reactive/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + org.springframework + spring-data-elasticsearch-reactive-example + + Spring Data Elasticsearch - Reactive Example + Sample projects for Reactive Spring Data Elasticsearch + https://github.com/spring-projects/spring-data-elasticsearch + + + org.springframework.data.examples + spring-data-elasticsearch-examples + 2.0.0.BUILD-SNAPSHOT + + + + + + io.projectreactor + reactor-test + + + + io.projectreactor + reactor-test + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + diff --git a/elasticsearch/reactive/src/main/java/example/springdata/elasticsearch/conference/ApplicationConfiguration.java b/elasticsearch/reactive/src/main/java/example/springdata/elasticsearch/conference/ApplicationConfiguration.java new file mode 100644 index 00000000..81c18a7d --- /dev/null +++ b/elasticsearch/reactive/src/main/java/example/springdata/elasticsearch/conference/ApplicationConfiguration.java @@ -0,0 +1,88 @@ +/* + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.elasticsearch.conference; + +import javax.annotation.PostConstruct; +import java.io.IOException; +import java.util.Arrays; + +import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; +import org.elasticsearch.client.RequestOptions; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.support.DefaultConversionService; +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.elasticsearch.config.AbstractReactiveElasticsearchConfiguration; +import org.springframework.data.elasticsearch.core.ElasticsearchEntityMapper; +import org.springframework.data.elasticsearch.core.EntityMapper; +import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.core.geo.GeoPoint; +import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; +import reactor.test.StepVerifier; + +/** + * @author Christoph Strobl + */ +@Configuration +@EnableReactiveElasticsearchRepositories +class ApplicationConfiguration extends AbstractReactiveElasticsearchConfiguration { + + @Autowired ReactiveElasticsearchOperations operations; + @Autowired ConferenceRepository repository; + + @Bean + @Override + public ReactiveElasticsearchClient reactiveElasticsearchClient() { + return ReactiveRestClients.create(ClientConfiguration.localhost()); + } + + @Override + public EntityMapper entityMapper() { + return new ElasticsearchEntityMapper(elasticsearchMappingContext(), + new DefaultConversionService()); + } + + @PostConstruct + public void insertDataSample() { + + try { + RestClients.create(ClientConfiguration.localhost()).rest().indices().create(new CreateIndexRequest("conference-index"), RequestOptions.DEFAULT); + } catch (IOException | ElasticsearchStatusException e) { + // just ignore it + } + + // Remove all documents + repository.deleteAll().subscribe(); + + // Save data sample + repository.save(Conference.builder().date("2014-11-06").name("Spring eXchange 2014 - London") + .keywords(Arrays.asList("java", "spring")).location(new GeoPoint(51.500152D, -0.126236D)).build()).then().as(StepVerifier::create).verifyComplete(); + repository.save(Conference.builder().date("2014-12-07").name("Scala eXchange 2014 - London") + .keywords(Arrays.asList("scala", "play", "java")).location(new GeoPoint(51.500152D, -0.126236D)).build()).then().as(StepVerifier::create).verifyComplete(); + repository.save(Conference.builder().date("2014-11-20").name("Elasticsearch 2014 - Berlin") + .keywords(Arrays.asList("java", "elasticsearch", "kibana")).location(new GeoPoint(52.5234051D, 13.4113999)) + .build()).then().as(StepVerifier::create).verifyComplete(); + repository.save(Conference.builder().date("2014-11-12").name("AWS London 2014") + .keywords(Arrays.asList("cloud", "aws")).location(new GeoPoint(51.500152D, -0.126236D)).build()).then().as(StepVerifier::create).verifyComplete(); + repository.save(Conference.builder().date("2014-10-04").name("JDD14 - Cracow") + .keywords(Arrays.asList("java", "spring")).location(new GeoPoint(50.0646501D, 19.9449799)).build()).then().as(StepVerifier::create).verifyComplete(); + } +} diff --git a/elasticsearch/reactive/src/main/java/example/springdata/elasticsearch/conference/Conference.java b/elasticsearch/reactive/src/main/java/example/springdata/elasticsearch/conference/Conference.java new file mode 100644 index 00000000..d8f256ae --- /dev/null +++ b/elasticsearch/reactive/src/main/java/example/springdata/elasticsearch/conference/Conference.java @@ -0,0 +1,43 @@ +/* + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.elasticsearch.conference; + +import static org.springframework.data.elasticsearch.annotations.FieldType.*; + +import java.util.List; + +import lombok.Builder; +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.core.geo.GeoPoint; + +/** + * @auhtor Christoph Strobl + */ +@Data +@Builder +@Document(indexName = "conference-index", type = "geo-class-point-type", shards = 1, replicas = 0, + refreshInterval = "-1") +public class Conference { + + private @Id String id; + private String name; + private @Field(type = Date) String date; + private GeoPoint location; + private List keywords; +} diff --git a/elasticsearch/reactive/src/main/java/example/springdata/elasticsearch/conference/ConferenceRepository.java b/elasticsearch/reactive/src/main/java/example/springdata/elasticsearch/conference/ConferenceRepository.java new file mode 100644 index 00000000..9ecc9710 --- /dev/null +++ b/elasticsearch/reactive/src/main/java/example/springdata/elasticsearch/conference/ConferenceRepository.java @@ -0,0 +1,27 @@ +/* + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.elasticsearch.conference; + +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import reactor.core.publisher.Flux; + +/** + * @author Christoph Strobl + */ +interface ConferenceRepository extends ReactiveCrudRepository { + + Flux findAllByKeywordsContainsAndDateAfter(String keyword, String Date); +} diff --git a/elasticsearch/reactive/src/main/resources/application.properties b/elasticsearch/reactive/src/main/resources/application.properties new file mode 100644 index 00000000..1982f98a --- /dev/null +++ b/elasticsearch/reactive/src/main/resources/application.properties @@ -0,0 +1,3 @@ +# Uncomment both entries to connect with local elasticsearch cluster +#spring.data.elasticsearch.clusterName=elasticsearch +#spring.data.elasticsearch.clusterNodes=localhost:9300 diff --git a/elasticsearch/reactive/src/test/java/example/springdata/elasticsearch/conference/ReactiveElasticsearchOperationsTest.java b/elasticsearch/reactive/src/test/java/example/springdata/elasticsearch/conference/ReactiveElasticsearchOperationsTest.java new file mode 100644 index 00000000..788c950d --- /dev/null +++ b/elasticsearch/reactive/src/test/java/example/springdata/elasticsearch/conference/ReactiveElasticsearchOperationsTest.java @@ -0,0 +1,76 @@ +/* + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.elasticsearch.conference; + +import static org.assertj.core.api.Assertions.*; + +import example.springdata.elasticsearch.util.ElasticsearchAvailable; +import reactor.test.StepVerifier; + +import java.text.ParseException; +import java.text.SimpleDateFormat; + +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.core.query.Criteria; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Test case to show Spring Data Elasticsearch functionality. + * + * @author Christoph Strobl + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = ApplicationConfiguration.class) +public class ReactiveElasticsearchOperationsTest { + + public static @ClassRule ElasticsearchAvailable elasticsearchAvailable = ElasticsearchAvailable.onLocalhost(); + + private static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); + + @Autowired ReactiveElasticsearchOperations operations; + + @Test + public void textSearch() { + + String expectedDate = "2014-10-29"; + String expectedWord = "java"; + CriteriaQuery query = new CriteriaQuery( + new Criteria("keywords").contains(expectedWord).and("date").greaterThanEqual(expectedDate)); + + operations.find(query, Conference.class) // + .as(StepVerifier::create) // + .consumeNextWith(it -> verify(it, expectedWord, expectedDate)) // + .consumeNextWith(it -> verify(it, expectedWord, expectedDate)) // + .consumeNextWith(it -> verify(it, expectedWord, expectedDate)) // + .verifyComplete(); + } + + void verify(Conference it, String expectedWord, String expectedDate) { + + assertThat(it.getKeywords()).contains(expectedWord); + try { + assertThat(format.parse(it.getDate())).isAfter(format.parse(expectedDate)); + } catch (ParseException e) { + fail("o_O", e); + } + } +} diff --git a/elasticsearch/reactive/src/test/java/example/springdata/elasticsearch/conference/ReactiveElasticsearchRepositoryTest.java b/elasticsearch/reactive/src/test/java/example/springdata/elasticsearch/conference/ReactiveElasticsearchRepositoryTest.java new file mode 100644 index 00000000..1e162262 --- /dev/null +++ b/elasticsearch/reactive/src/test/java/example/springdata/elasticsearch/conference/ReactiveElasticsearchRepositoryTest.java @@ -0,0 +1,71 @@ +/* + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.elasticsearch.conference; + +import static org.assertj.core.api.Assertions.*; + +import example.springdata.elasticsearch.util.ElasticsearchAvailable; +import reactor.test.StepVerifier; + +import java.text.ParseException; +import java.text.SimpleDateFormat; + +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Test case to show reactive Spring Data Elasticsearch repository functionality. + * + * @author Christoph Strobl + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = ApplicationConfiguration.class) +public class ReactiveElasticsearchRepositoryTest { + + public static @ClassRule ElasticsearchAvailable elasticsearchAvailable = ElasticsearchAvailable.onLocalhost(); + + private static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); + + @Autowired ConferenceRepository repository; + + @Test + public void textSearch() { + + String expectedDate = "2014-10-29"; + String expectedWord = "java"; + + repository.findAllByKeywordsContainsAndDateAfter(expectedWord, expectedDate) // + .as(StepVerifier::create) // + .consumeNextWith(it -> verify(it, expectedWord, expectedDate)) // + .consumeNextWith(it -> verify(it, expectedWord, expectedDate)) // + .consumeNextWith(it -> verify(it, expectedWord, expectedDate)) // + .verifyComplete(); + } + + void verify(Conference it, String expectedWord, String expectedDate) { + + assertThat(it.getKeywords()).contains(expectedWord); + try { + assertThat(format.parse(it.getDate())).isAfter(format.parse(expectedDate)); + } catch (ParseException e) { + fail("o_O", e); + } + } +} diff --git a/elasticsearch/reactive/src/test/java/example/springdata/elasticsearch/util/ElasticsearchAvailable.java b/elasticsearch/reactive/src/test/java/example/springdata/elasticsearch/util/ElasticsearchAvailable.java new file mode 100644 index 00000000..fc1375b8 --- /dev/null +++ b/elasticsearch/reactive/src/test/java/example/springdata/elasticsearch/util/ElasticsearchAvailable.java @@ -0,0 +1,70 @@ +/* + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.elasticsearch.util; + +import java.io.IOException; + +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.assertj.core.api.Assumptions; +import org.junit.AssumptionViolatedException; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * @author Christoph Strobl + */ +public class ElasticsearchAvailable implements TestRule { + + private final String url; + + private ElasticsearchAvailable(String url) { + this.url = url; + } + + public static ElasticsearchAvailable onLocalhost() { + return new ElasticsearchAvailable("http://localhost:9200"); + } + + @Override + public Statement apply(Statement base, Description description) { + + return new Statement() { + + @Override + public void evaluate() throws Throwable { + + checkServerRunning(); + base.evaluate(); + } + }; + } + + private void checkServerRunning() { + + try (CloseableHttpClient client = HttpClientBuilder.create().build()) { + CloseableHttpResponse response = client.execute(new HttpHead(url)); + if (response != null && response.getStatusLine() != null) { + Assumptions.assumeThat(response.getStatusLine().getStatusCode()).isEqualTo(200); + } + } catch (IOException e) { + throw new AssumptionViolatedException(String.format("Elasticsearch Server seems to be down. %s", e.getMessage())); + } + } +}