mirror of
https://github.com/spring-projects/spring-data-elasticsearch.git
synced 2025-08-13 13:03:29 +00:00
Compare commits
30 Commits
main
...
3.2.3.RELE
Author | SHA1 | Date | |
---|---|---|---|
|
060cab76d5 | ||
|
afb8a35eac | ||
|
1c0dd71020 | ||
|
1633668d7f | ||
|
6756f792c8 | ||
|
bae4db8a7f | ||
|
c47fd2cfce | ||
|
cb08bb7196 | ||
|
f3500623ff | ||
|
4c4cbed43b | ||
|
23fb5689b7 | ||
|
b551466f94 | ||
|
0a7af69d69 | ||
|
2cf42a4763 | ||
|
6f0d1ee9e7 | ||
|
f82dd229d9 | ||
|
3b833f6f63 | ||
|
2517fd5c90 | ||
|
4e572679dd | ||
|
de9c664d1e | ||
|
8187c5362a | ||
|
a8eb260bbd | ||
|
bb944f595f | ||
|
598626238b | ||
|
a451f8dca4 | ||
|
4fd070c332 | ||
|
091413dd69 | ||
|
ff74425132 | ||
|
6f4d1dcc57 | ||
|
9713e33fed |
8
Jenkinsfile
vendored
8
Jenkinsfile
vendored
@ -3,7 +3,7 @@ pipeline {
|
||||
|
||||
triggers {
|
||||
pollSCM 'H/10 * * * *'
|
||||
upstream(upstreamProjects: "spring-data-commons/master", threshold: hudson.model.Result.SUCCESS)
|
||||
upstream(upstreamProjects: "spring-data-commons/2.2.x", threshold: hudson.model.Result.SUCCESS)
|
||||
}
|
||||
|
||||
options {
|
||||
@ -15,7 +15,7 @@ pipeline {
|
||||
stage("Test") {
|
||||
when {
|
||||
anyOf {
|
||||
branch 'master'
|
||||
branch '3.2.x'
|
||||
not { triggeredBy 'UpstreamCause' }
|
||||
}
|
||||
}
|
||||
@ -39,7 +39,7 @@ pipeline {
|
||||
stage('Release to artifactory') {
|
||||
when {
|
||||
anyOf {
|
||||
branch 'master'
|
||||
branch '3.2.x'
|
||||
not { triggeredBy 'UpstreamCause' }
|
||||
}
|
||||
}
|
||||
@ -70,7 +70,7 @@ pipeline {
|
||||
}
|
||||
stage('Publish documentation') {
|
||||
when {
|
||||
branch 'master'
|
||||
branch '3.2.x'
|
||||
}
|
||||
agent {
|
||||
docker {
|
||||
|
46
pom.xml
46
pom.xml
@ -5,12 +5,12 @@
|
||||
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-elasticsearch</artifactId>
|
||||
<version>3.2.0.RELEASE</version>
|
||||
<version>3.2.3.RELEASE</version>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.data.build</groupId>
|
||||
<artifactId>spring-data-parent</artifactId>
|
||||
<version>2.2.0.RELEASE</version>
|
||||
<version>2.2.3.RELEASE</version>
|
||||
</parent>
|
||||
|
||||
<name>Spring Data Elasticsearch</name>
|
||||
@ -19,9 +19,9 @@
|
||||
|
||||
<properties>
|
||||
<commonslang>2.6</commonslang>
|
||||
<elasticsearch>6.8.1</elasticsearch>
|
||||
<elasticsearch>6.8.4</elasticsearch>
|
||||
<log4j>2.9.1</log4j>
|
||||
<springdata.commons>2.2.0.RELEASE</springdata.commons>
|
||||
<springdata.commons>2.2.3.RELEASE</springdata.commons>
|
||||
<netty>4.1.39.Final</netty>
|
||||
<java-module-name>spring.data.elasticsearch</java-module-name>
|
||||
</properties>
|
||||
@ -248,6 +248,24 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.tomakehurst</groupId>
|
||||
<artifactId>wiremock-jre8</artifactId>
|
||||
<version>2.25.1</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<!-- these exclusions are needed because of Elasticsearch JarHell-->
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.ow2.asm</groupId>
|
||||
<artifactId>asm</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Upgrade xbean to 4.5 to prevent incompatibilities due to ASM versions -->
|
||||
<dependency>
|
||||
<groupId>org.apache.xbean</groupId>
|
||||
@ -258,8 +276,8 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>servlet-api</artifactId>
|
||||
<version>3.0-alpha-1</version>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
@ -267,14 +285,6 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.asciidoctor</groupId>
|
||||
<artifactId>asciidoctor-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<!--
|
||||
please do not remove this configuration for surefire - we need that to avoid issue with jar hell
|
||||
-->
|
||||
@ -292,6 +302,14 @@
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.asciidoctor</groupId>
|
||||
<artifactId>asciidoctor-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
@ -35,8 +35,8 @@ The following table shows the Elasticsearch versions that are used by Spring Dat
|
||||
[cols="^,^,^,^",options="header"]
|
||||
|===
|
||||
|Spring Data Release Train |Spring Data Elasticsearch |Elasticsearch | Spring Boot
|
||||
|Moorefootnote:cdv[Currently in development] |3.2.xfootnote:cdv[]|6.8.1 / 7.xfootnote:hrc[via the <<elasticsearch.clients.rest,high-level REST client>>]|2.2.0footnote:cdv[]
|
||||
|Lovelace |3.1.x |6.2.2 / 7.xfootnote:hrc[]|2.1.x
|
||||
|Moore |3.2.x |6.8.4 |2.2.x
|
||||
|Lovelace |3.1.x |6.2.2|2.1.x
|
||||
|Kayfootnote:oom[Out of maintenance]|3.0.xfootnote:oom[] |5.5.0 |2.0.xfootnote:oom[]
|
||||
|Ingallsfootnote:oom[]|2.1.xfootnote:oom[] |2.4.0 |1.5.xfootnote:oom[]
|
||||
|===
|
||||
|
@ -21,6 +21,7 @@ import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@ -30,6 +31,7 @@ import org.springframework.http.HttpHeaders;
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Henrique Amaral
|
||||
* @since 3.2
|
||||
*/
|
||||
public interface ClientConfiguration {
|
||||
@ -118,6 +120,13 @@ public interface ClientConfiguration {
|
||||
*/
|
||||
Optional<SSLContext> getSslContext();
|
||||
|
||||
/**
|
||||
* Returns the {@link HostnameVerifier} to use. Can be {@link Optional#empty()} if unconfigured.
|
||||
*
|
||||
* @return the {@link HostnameVerifier} to use. Can be {@link Optional#empty()} if unconfigured.
|
||||
*/
|
||||
Optional<HostnameVerifier> getHostNameVerifier();
|
||||
|
||||
/**
|
||||
* Returns the {@link java.time.Duration connect timeout}.
|
||||
*
|
||||
@ -135,6 +144,14 @@ public interface ClientConfiguration {
|
||||
*/
|
||||
Duration getSocketTimeout();
|
||||
|
||||
/**
|
||||
* returns an optionally set proxy in the form host:port
|
||||
*
|
||||
* @return the optional proxy
|
||||
* @since 4.0
|
||||
*/
|
||||
Optional<String> getProxy();
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@ -201,6 +218,16 @@ public interface ClientConfiguration {
|
||||
* @return the {@link TerminalClientConfigurationBuilder}.
|
||||
*/
|
||||
TerminalClientConfigurationBuilder usingSsl(SSLContext sslContext);
|
||||
|
||||
/**
|
||||
* Connect via {@literal https} using the givens {@link SSLContext} and HostnameVerifier {@link HostnameVerifier}
|
||||
* .<br />
|
||||
* <strong>NOTE</strong> You need to leave out the protocol in
|
||||
* {@link ClientConfigurationBuilderWithRequiredEndpoint#connectedTo(String)}.
|
||||
*
|
||||
* @return the {@link TerminalClientConfigurationBuilder}.
|
||||
*/
|
||||
TerminalClientConfigurationBuilder usingSsl(SSLContext sslContext, HostnameVerifier hostnameVerifier);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -267,6 +294,12 @@ public interface ClientConfiguration {
|
||||
*/
|
||||
TerminalClientConfigurationBuilder withBasicAuth(String username, String password);
|
||||
|
||||
/**
|
||||
* @param proxy a proxy formatted as String {@literal host:port}.
|
||||
* @return the {@link MaybeSecureClientConfigurationBuilder}.
|
||||
*/
|
||||
MaybeSecureClientConfigurationBuilder withProxy(String proxy);
|
||||
|
||||
/**
|
||||
* Build the {@link ClientConfiguration} object.
|
||||
*
|
||||
|
@ -22,6 +22,7 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationBuilderWithRequiredEndpoint;
|
||||
@ -37,6 +38,7 @@ import org.springframework.util.Assert;
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Henrique Amaral
|
||||
* @since 3.2
|
||||
*/
|
||||
class ClientConfigurationBuilder
|
||||
@ -46,10 +48,12 @@ class ClientConfigurationBuilder
|
||||
private HttpHeaders headers = HttpHeaders.EMPTY;
|
||||
private boolean useSsl;
|
||||
private @Nullable SSLContext sslContext;
|
||||
private @Nullable HostnameVerifier hostnameVerifier;
|
||||
private Duration connectTimeout = Duration.ofSeconds(10);
|
||||
private Duration soTimeout = Duration.ofSeconds(5);
|
||||
private String username;
|
||||
private String password;
|
||||
private String proxy;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
@ -78,6 +82,13 @@ class ClientConfigurationBuilder
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MaybeSecureClientConfigurationBuilder withProxy(String proxy) {
|
||||
Assert.hasLength(proxy, "proxy must not be null or empty");
|
||||
this.proxy = proxy;
|
||||
return this;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.client.ClientConfiguration.MaybeSecureClientConfigurationBuilder#usingSsl()
|
||||
@ -103,6 +114,22 @@ class ClientConfigurationBuilder
|
||||
return this;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.client.ClientConfiguration.MaybeSecureClientConfigurationBuilder#usingSsl(javax.net.ssl.SSLContext, javax.net.ssl.HostnameVerifier)
|
||||
*/
|
||||
@Override
|
||||
public TerminalClientConfigurationBuilder usingSsl(SSLContext sslContext, HostnameVerifier hostnameVerifier) {
|
||||
|
||||
Assert.notNull(sslContext, "SSL Context must not be null");
|
||||
Assert.notNull(hostnameVerifier, "Host Name Verifier must not be null");
|
||||
|
||||
this.useSsl = true;
|
||||
this.sslContext = sslContext;
|
||||
this.hostnameVerifier = hostnameVerifier;
|
||||
return this;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.client.ClientConfiguration.TerminalClientConfigurationBuilder#withDefaultHeaders(org.springframework.http.HttpHeaders)
|
||||
@ -170,8 +197,8 @@ class ClientConfigurationBuilder
|
||||
headers.setBasicAuth(username, password);
|
||||
}
|
||||
|
||||
return new DefaultClientConfiguration(this.hosts, this.headers, this.useSsl, this.sslContext, this.soTimeout,
|
||||
this.connectTimeout);
|
||||
return new DefaultClientConfiguration(hosts, headers, useSsl, sslContext, soTimeout, connectTimeout,
|
||||
hostnameVerifier, proxy);
|
||||
}
|
||||
|
||||
private static InetSocketAddress parse(String hostAndPort) {
|
||||
|
@ -22,6 +22,7 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@ -42,9 +43,12 @@ class DefaultClientConfiguration implements ClientConfiguration {
|
||||
private final @Nullable SSLContext sslContext;
|
||||
private final Duration soTimeout;
|
||||
private final Duration connectTimeout;
|
||||
private final @Nullable HostnameVerifier hostnameVerifier;
|
||||
private final String proxy;
|
||||
|
||||
DefaultClientConfiguration(List<InetSocketAddress> hosts, HttpHeaders headers, boolean useSsl,
|
||||
@Nullable SSLContext sslContext, Duration soTimeout, Duration connectTimeout) {
|
||||
@Nullable SSLContext sslContext, Duration soTimeout, Duration connectTimeout,
|
||||
@Nullable HostnameVerifier hostnameVerifier, String proxy) {
|
||||
|
||||
this.hosts = Collections.unmodifiableList(new ArrayList<>(hosts));
|
||||
this.headers = new HttpHeaders(headers);
|
||||
@ -52,6 +56,8 @@ class DefaultClientConfiguration implements ClientConfiguration {
|
||||
this.sslContext = sslContext;
|
||||
this.soTimeout = soTimeout;
|
||||
this.connectTimeout = connectTimeout;
|
||||
this.hostnameVerifier = hostnameVerifier;
|
||||
this.proxy = proxy;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -90,6 +96,15 @@ class DefaultClientConfiguration implements ClientConfiguration {
|
||||
return Optional.ofNullable(this.sslContext);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.client.ClientConfiguration#getHostNameVerifier()
|
||||
*/
|
||||
@Override
|
||||
public Optional<HostnameVerifier> getHostNameVerifier() {
|
||||
return Optional.ofNullable(this.hostnameVerifier);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.client.ClientConfiguration#getConnectTimeout()
|
||||
@ -108,4 +123,12 @@ class DefaultClientConfiguration implements ClientConfiguration {
|
||||
return this.soTimeout;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.client.ClientConfiguration#getProxy()
|
||||
*/
|
||||
@Override
|
||||
public Optional<String> getProxy() {
|
||||
return Optional.ofNullable(proxy);
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.apache.http.Header;
|
||||
@ -52,6 +53,7 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @author Henrique Amaral
|
||||
* @since 3.2
|
||||
*/
|
||||
public final class RestClients {
|
||||
@ -87,7 +89,9 @@ public final class RestClients {
|
||||
builder.setHttpClientConfigCallback(clientBuilder -> {
|
||||
|
||||
Optional<SSLContext> sslContext = clientConfiguration.getSslContext();
|
||||
Optional<HostnameVerifier> hostNameVerifier = clientConfiguration.getHostNameVerifier();
|
||||
sslContext.ifPresent(clientBuilder::setSSLContext);
|
||||
hostNameVerifier.ifPresent(clientBuilder::setSSLHostnameVerifier);
|
||||
|
||||
if (ClientLogger.isEnabled()) {
|
||||
|
||||
@ -114,6 +118,8 @@ public final class RestClients {
|
||||
|
||||
clientBuilder.setDefaultRequestConfig(requestConfigBuilder.build());
|
||||
|
||||
clientConfiguration.getProxy().map(HttpHost::create).ifPresent(clientBuilder::setProxy);
|
||||
|
||||
return clientBuilder;
|
||||
});
|
||||
|
||||
|
@ -47,7 +47,6 @@ import javax.net.ssl.SSLContext;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.elasticsearch.ElasticsearchStatusException;
|
||||
import org.elasticsearch.action.ActionRequest;
|
||||
import org.elasticsearch.action.ActionResponse;
|
||||
import org.elasticsearch.action.admin.indices.close.CloseIndexRequest;
|
||||
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
|
||||
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
|
||||
@ -58,6 +57,8 @@ import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
|
||||
import org.elasticsearch.action.admin.indices.open.OpenIndexRequest;
|
||||
import org.elasticsearch.action.admin.indices.refresh.RefreshRequest;
|
||||
import org.elasticsearch.action.admin.indices.refresh.RefreshResponse;
|
||||
import org.elasticsearch.action.bulk.BulkRequest;
|
||||
import org.elasticsearch.action.bulk.BulkResponse;
|
||||
import org.elasticsearch.action.delete.DeleteRequest;
|
||||
import org.elasticsearch.action.delete.DeleteResponse;
|
||||
import org.elasticsearch.action.get.GetRequest;
|
||||
@ -77,6 +78,8 @@ import org.elasticsearch.action.support.master.AcknowledgedResponse;
|
||||
import org.elasticsearch.action.update.UpdateRequest;
|
||||
import org.elasticsearch.action.update.UpdateResponse;
|
||||
import org.elasticsearch.client.Request;
|
||||
import org.elasticsearch.client.core.CountRequest;
|
||||
import org.elasticsearch.client.core.CountResponse;
|
||||
import org.elasticsearch.common.unit.TimeValue;
|
||||
import org.elasticsearch.common.xcontent.DeprecationHandler;
|
||||
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
|
||||
@ -122,6 +125,7 @@ import org.springframework.web.reactive.function.client.WebClient.RequestBodySpe
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @author Henrique Amaral
|
||||
* @since 3.2
|
||||
* @see ClientConfiguration
|
||||
* @see ReactiveRestClients
|
||||
@ -319,6 +323,17 @@ public class DefaultReactiveElasticsearchClient implements ReactiveElasticsearch
|
||||
.publishNext();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient#count(org.springframework.http.HttpHeaders, org.elasticsearch.action.search.SearchRequest)
|
||||
*/
|
||||
@Override
|
||||
public Mono<Long> count(HttpHeaders headers, CountRequest countRequest) {
|
||||
return sendRequest(countRequest, RequestCreator.count(), CountResponse.class, headers) //
|
||||
.map(CountResponse::getCount) //
|
||||
.next();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient#ping(org.springframework.http.HttpHeaders, org.elasticsearch.action.search.SearchRequest)
|
||||
@ -423,6 +438,16 @@ public class DefaultReactiveElasticsearchClient implements ReactiveElasticsearch
|
||||
.publishNext();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient#bulk(org.springframework.http.HttpHeaders, org.elasticsearch.action.bulk.BulkRequest)
|
||||
*/
|
||||
@Override
|
||||
public Mono<BulkResponse> bulk(HttpHeaders headers, BulkRequest bulkRequest) {
|
||||
return sendRequest(bulkRequest, RequestCreator.bulk(), BulkResponse.class, headers) //
|
||||
.publishNext();
|
||||
}
|
||||
|
||||
// --> INDICES
|
||||
|
||||
/*
|
||||
@ -553,13 +578,12 @@ public class DefaultReactiveElasticsearchClient implements ReactiveElasticsearch
|
||||
|
||||
// -->
|
||||
|
||||
private <Req extends ActionRequest, Resp extends ActionResponse> Flux<Resp> sendRequest(Req request,
|
||||
Function<Req, Request> converter, Class<Resp> responseType, HttpHeaders headers) {
|
||||
private <Req extends ActionRequest, Resp> Flux<Resp> sendRequest(Req request, Function<Req, Request> converter,
|
||||
Class<Resp> responseType, HttpHeaders headers) {
|
||||
return sendRequest(converter.apply(request), responseType, headers);
|
||||
}
|
||||
|
||||
private <AR extends ActionResponse> Flux<AR> sendRequest(Request request, Class<AR> responseType,
|
||||
HttpHeaders headers) {
|
||||
private <Resp> Flux<Resp> sendRequest(Request request, Class<Resp> responseType, HttpHeaders headers) {
|
||||
|
||||
String logId = ClientLogger.newLogId();
|
||||
|
||||
@ -742,6 +766,18 @@ public class DefaultReactiveElasticsearchClient implements ReactiveElasticsearch
|
||||
};
|
||||
}
|
||||
|
||||
static Function<BulkRequest, Request> bulk() {
|
||||
|
||||
return request -> {
|
||||
|
||||
try {
|
||||
return RequestConverters.bulk(request);
|
||||
} catch (IOException e) {
|
||||
throw new ElasticsearchException("Could not parse request", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// --> INDICES
|
||||
|
||||
static Function<GetIndexRequest, Request> indexExists() {
|
||||
@ -776,6 +812,10 @@ public class DefaultReactiveElasticsearchClient implements ReactiveElasticsearch
|
||||
return RequestConverters::flushIndex;
|
||||
}
|
||||
|
||||
static Function<CountRequest, Request> count() {
|
||||
return RequestConverters::count;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -30,6 +30,8 @@ import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
|
||||
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
|
||||
import org.elasticsearch.action.admin.indices.open.OpenIndexRequest;
|
||||
import org.elasticsearch.action.admin.indices.refresh.RefreshRequest;
|
||||
import org.elasticsearch.action.bulk.BulkRequest;
|
||||
import org.elasticsearch.action.bulk.BulkResponse;
|
||||
import org.elasticsearch.action.delete.DeleteRequest;
|
||||
import org.elasticsearch.action.delete.DeleteResponse;
|
||||
import org.elasticsearch.action.get.GetRequest;
|
||||
@ -41,6 +43,7 @@ import org.elasticsearch.action.search.SearchRequest;
|
||||
import org.elasticsearch.action.search.SearchResponse;
|
||||
import org.elasticsearch.action.update.UpdateRequest;
|
||||
import org.elasticsearch.action.update.UpdateResponse;
|
||||
import org.elasticsearch.client.core.CountRequest;
|
||||
import org.elasticsearch.index.get.GetResult;
|
||||
import org.elasticsearch.index.reindex.BulkByScrollResponse;
|
||||
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
|
||||
@ -58,6 +61,8 @@ import org.springframework.web.reactive.function.client.WebClient;
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Henrique Amaral
|
||||
* @since 3.2
|
||||
* @see ClientConfiguration
|
||||
* @see ReactiveRestClients
|
||||
@ -329,6 +334,47 @@ public interface ReactiveElasticsearchClient {
|
||||
*/
|
||||
Mono<DeleteResponse> delete(HttpHeaders headers, DeleteRequest deleteRequest);
|
||||
|
||||
/**
|
||||
* Execute a {@link SearchRequest} against the {@literal count} API.
|
||||
*
|
||||
* @param consumer new {@literal null}.
|
||||
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-count.html">Count API on
|
||||
* elastic.co</a>
|
||||
* @return the {@link Mono} emitting the count result.
|
||||
* @since 4.0
|
||||
*/
|
||||
default Mono<Long> count(Consumer<CountRequest> consumer) {
|
||||
|
||||
CountRequest countRequest = new CountRequest();
|
||||
consumer.accept(countRequest);
|
||||
return count(countRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a {@link SearchRequest} against the {@literal count} API.
|
||||
*
|
||||
* @param countRequest must not be {@literal null}.
|
||||
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-count.html">Count API on
|
||||
* elastic.co</a>
|
||||
* @return the {@link Mono} emitting the count result.
|
||||
* @since 4.0
|
||||
*/
|
||||
default Mono<Long> count(CountRequest countRequest) {
|
||||
return count(HttpHeaders.EMPTY, countRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a {@link SearchRequest} against the {@literal count} API.
|
||||
*
|
||||
* @param headers Use {@link HttpHeaders} to provide eg. authentication data. Must not be {@literal null}.
|
||||
* @param countRequest must not be {@literal null}.
|
||||
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-count.html">Count API on
|
||||
* elastic.co</a>
|
||||
* @return the {@link Mono} emitting the count result.
|
||||
* @since 4.0
|
||||
*/
|
||||
Mono<Long> count(HttpHeaders headers, CountRequest countRequest);
|
||||
|
||||
/**
|
||||
* Execute a {@link SearchRequest} against the {@literal search} API.
|
||||
*
|
||||
@ -430,6 +476,44 @@ public interface ReactiveElasticsearchClient {
|
||||
*/
|
||||
Mono<BulkByScrollResponse> deleteBy(HttpHeaders headers, DeleteByQueryRequest deleteRequest);
|
||||
|
||||
/**
|
||||
* Execute a {@link BulkRequest} against the {@literal bulk} API.
|
||||
*
|
||||
* @param consumer never {@literal null}.
|
||||
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html">Bulk API on
|
||||
* elastic.co</a>
|
||||
* @return a {@link Mono} emitting the emitting operation response.
|
||||
*/
|
||||
default Mono<BulkResponse> bulk(Consumer<BulkRequest> consumer) {
|
||||
|
||||
BulkRequest request = new BulkRequest();
|
||||
consumer.accept(request);
|
||||
return bulk(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a {@link BulkRequest} against the {@literal bulk} API.
|
||||
*
|
||||
* @param bulkRequest must not be {@literal null}.
|
||||
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html">Bulk API on
|
||||
* elastic.co</a>
|
||||
* @return a {@link Mono} emitting the emitting operation response.
|
||||
*/
|
||||
default Mono<BulkResponse> bulk(BulkRequest bulkRequest) {
|
||||
return bulk(HttpHeaders.EMPTY, bulkRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a {@link BulkRequest} against the {@literal bulk} API.
|
||||
*
|
||||
* @param headers Use {@link HttpHeaders} to provide eg. authentication data. Must not be {@literal null}.
|
||||
* @param bulkRequest must not be {@literal null}.
|
||||
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html">Bulk API on
|
||||
* elastic.co</a>
|
||||
* @return a {@link Mono} emitting operation response.
|
||||
*/
|
||||
Mono<BulkResponse> bulk(HttpHeaders headers, BulkRequest bulkRequest);
|
||||
|
||||
/**
|
||||
* Compose the actual command/s to run against Elasticsearch using the underlying {@link WebClient connection}.
|
||||
* {@link #execute(ReactiveElasticsearchClientCallback) Execute} selects an active server from the available ones and
|
||||
|
@ -60,6 +60,7 @@ import org.elasticsearch.action.update.UpdateRequest;
|
||||
import org.elasticsearch.client.Request;
|
||||
import org.elasticsearch.client.Requests;
|
||||
import org.elasticsearch.client.RethrottleRequest;
|
||||
import org.elasticsearch.client.core.CountRequest;
|
||||
import org.elasticsearch.cluster.health.ClusterHealthStatus;
|
||||
import org.elasticsearch.common.Priority;
|
||||
import org.elasticsearch.common.Strings;
|
||||
@ -98,6 +99,8 @@ import org.springframework.lang.Nullable;
|
||||
* <p>
|
||||
* Only intended for internal use.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Peter-Josef Meisch
|
||||
* @since 3.2
|
||||
*/
|
||||
public class RequestConverters {
|
||||
@ -386,6 +389,32 @@ public class RequestConverters {
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a count request.
|
||||
*
|
||||
* @param countRequest the search defining the data to be counted
|
||||
* @return Elasticsearch count request
|
||||
* @since 4.0
|
||||
*/
|
||||
public static Request count(CountRequest countRequest) {
|
||||
Request request = new Request(HttpMethod.POST.name(),
|
||||
endpoint(countRequest.indices(), countRequest.types(), "_count"));
|
||||
|
||||
Params params = new Params(request);
|
||||
addCountRequestParams(params, countRequest);
|
||||
|
||||
if (countRequest.source() != null) {
|
||||
request.setEntity(createEntity(countRequest.source(), REQUEST_BODY_CONTENT_TYPE));
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private static void addCountRequestParams(Params params, CountRequest countRequest) {
|
||||
params.withRouting(countRequest.routing());
|
||||
params.withPreference(countRequest.preference());
|
||||
params.withIndicesOptions(countRequest.indicesOptions());
|
||||
}
|
||||
|
||||
private static void addSearchRequestParams(Params params, SearchRequest searchRequest) {
|
||||
params.putParam("typed_keys", "true");
|
||||
params.withRouting(searchRequest.routing());
|
||||
|
@ -598,6 +598,7 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate
|
||||
if (elasticsearchQuery != null) {
|
||||
sourceBuilder.query(elasticsearchQuery);
|
||||
}
|
||||
sourceBuilder.size(0);
|
||||
countRequest.source(sourceBuilder);
|
||||
|
||||
try {
|
||||
@ -616,6 +617,7 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate
|
||||
if (elasticsearchFilter != null) {
|
||||
searchRequest.source().postFilter(elasticsearchFilter);
|
||||
}
|
||||
searchRequest.source().size(0);
|
||||
SearchResponse response;
|
||||
try {
|
||||
response = client.search(searchRequest, RequestOptions.DEFAULT);
|
||||
@ -960,6 +962,10 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate
|
||||
prepareSort(query, searchSourceBuilder, entity);
|
||||
}
|
||||
|
||||
if (query.getIndicesOptions() != null) {
|
||||
request.indicesOptions(query.getIndicesOptions());
|
||||
}
|
||||
|
||||
if (query instanceof SearchQuery) {
|
||||
SearchQuery searchQuery = (SearchQuery) query;
|
||||
|
||||
|
@ -508,6 +508,7 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate
|
||||
if (elasticsearchQuery != null) {
|
||||
countRequestBuilder.setQuery(elasticsearchQuery);
|
||||
}
|
||||
countRequestBuilder.setSize(0);
|
||||
return countRequestBuilder.execute().actionGet().getHits().getTotalHits();
|
||||
}
|
||||
|
||||
@ -521,6 +522,7 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate
|
||||
if (elasticsearchFilter != null) {
|
||||
searchRequestBuilder.setPostFilter(elasticsearchFilter);
|
||||
}
|
||||
searchRequestBuilder.setSize(0);
|
||||
return searchRequestBuilder.execute().actionGet().getHits().getTotalHits();
|
||||
}
|
||||
|
||||
@ -810,6 +812,10 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate
|
||||
prepareSort(query, requestBuilder, entity);
|
||||
}
|
||||
|
||||
if (query.getIndicesOptions() != null) {
|
||||
requestBuilder.setIndicesOptions(query.getIndicesOptions());
|
||||
}
|
||||
|
||||
if (query instanceof SearchQuery) {
|
||||
SearchQuery searchQuery = (SearchQuery) query;
|
||||
|
||||
|
@ -36,6 +36,7 @@ import org.elasticsearch.action.support.IndicesOptions;
|
||||
import org.elasticsearch.action.support.WriteRequest;
|
||||
import org.elasticsearch.action.support.WriteRequest.RefreshPolicy;
|
||||
import org.elasticsearch.client.Requests;
|
||||
import org.elasticsearch.client.core.CountRequest;
|
||||
import org.elasticsearch.index.get.GetResult;
|
||||
import org.elasticsearch.index.query.QueryBuilder;
|
||||
import org.elasticsearch.index.query.QueryBuilders;
|
||||
@ -247,80 +248,133 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
|
||||
@Nullable String type) {
|
||||
|
||||
return Flux.defer(() -> {
|
||||
SearchRequest request = prepareSearchRequest(buildSearchRequest(query, entity, index, type));
|
||||
|
||||
IndexCoordinates indexCoordinates = operations.determineIndex(entity, index, type);
|
||||
SearchRequest request = new SearchRequest(indices(query, indexCoordinates::getIndexName));
|
||||
request.types(indexTypes(query, indexCoordinates::getTypeName));
|
||||
|
||||
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
|
||||
searchSourceBuilder.query(mappedQuery(query, entity));
|
||||
searchSourceBuilder.version(entity.hasVersionProperty());
|
||||
searchSourceBuilder.trackScores(query.getTrackScores());
|
||||
|
||||
QueryBuilder postFilterQuery = mappedFilterQuery(query, entity);
|
||||
if (postFilterQuery != null) {
|
||||
searchSourceBuilder.postFilter(postFilterQuery);
|
||||
}
|
||||
|
||||
if (query.getSourceFilter() != null) {
|
||||
searchSourceBuilder.fetchSource(query.getSourceFilter().getIncludes(), query.getSourceFilter().getExcludes());
|
||||
}
|
||||
|
||||
if (query instanceof SearchQuery && ((SearchQuery) query).getCollapseBuilder() != null) {
|
||||
searchSourceBuilder.collapse(((SearchQuery) query).getCollapseBuilder());
|
||||
}
|
||||
|
||||
sort(query, entity).forEach(searchSourceBuilder::sort);
|
||||
|
||||
if (query.getMinScore() > 0) {
|
||||
searchSourceBuilder.minScore(query.getMinScore());
|
||||
}
|
||||
|
||||
if (query.getIndicesOptions() != null) {
|
||||
request.indicesOptions(query.getIndicesOptions());
|
||||
}
|
||||
|
||||
if (query.getPreference() != null) {
|
||||
request.preference(query.getPreference());
|
||||
}
|
||||
|
||||
if (query.getSearchType() != null) {
|
||||
request.searchType(query.getSearchType());
|
||||
}
|
||||
|
||||
Pageable pageable = query.getPageable();
|
||||
|
||||
if (pageable.isPaged()) {
|
||||
|
||||
long offset = pageable.getOffset();
|
||||
if (offset > Integer.MAX_VALUE) {
|
||||
throw new IllegalArgumentException(String.format("Offset must not be more than %s", Integer.MAX_VALUE));
|
||||
}
|
||||
|
||||
searchSourceBuilder.from((int) offset);
|
||||
searchSourceBuilder.size(pageable.getPageSize());
|
||||
|
||||
request.source(searchSourceBuilder);
|
||||
return doFind(prepareSearchRequest(request));
|
||||
|
||||
if (query.getPageable().isPaged()) {
|
||||
return doFind(request);
|
||||
} else {
|
||||
|
||||
request.source(searchSourceBuilder);
|
||||
return doScan(prepareSearchRequest(request));
|
||||
return doScroll(request);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Long> count(Query query, Class<?> entityType, String index, String type) {
|
||||
return doCount(query, getPersistentEntity(entityType), index, type);
|
||||
}
|
||||
|
||||
private Mono<Long> doCount(Query query, ElasticsearchPersistentEntity<?> entity, @Nullable String index,
|
||||
@Nullable String type) {
|
||||
return Mono.defer(() -> {
|
||||
|
||||
CountRequest countRequest = buildCountRequest(query, entity, index, type);
|
||||
CountRequest request = prepareCountRequest(countRequest);
|
||||
return doCount(request);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private CountRequest buildCountRequest(Query query, ElasticsearchPersistentEntity<?> entity, @Nullable String index,
|
||||
@Nullable String type) {
|
||||
|
||||
IndexCoordinates indexCoordinates = operations.determineIndex(entity, index, type);
|
||||
CountRequest request = new CountRequest(indices(query, indexCoordinates::getIndexName));
|
||||
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
|
||||
searchSourceBuilder.query(mappedQuery(query, entity));
|
||||
searchSourceBuilder.trackScores(query.getTrackScores());
|
||||
|
||||
QueryBuilder postFilterQuery = mappedFilterQuery(query, entity);
|
||||
if (postFilterQuery != null) {
|
||||
searchSourceBuilder.postFilter(postFilterQuery);
|
||||
}
|
||||
|
||||
if (query.getSourceFilter() != null) {
|
||||
searchSourceBuilder.fetchSource(query.getSourceFilter().getIncludes(), query.getSourceFilter().getExcludes());
|
||||
}
|
||||
|
||||
if (query instanceof SearchQuery && ((SearchQuery) query).getCollapseBuilder() != null) {
|
||||
searchSourceBuilder.collapse(((SearchQuery) query).getCollapseBuilder());
|
||||
}
|
||||
|
||||
sort(query, entity).forEach(searchSourceBuilder::sort);
|
||||
|
||||
if (query.getMinScore() > 0) {
|
||||
searchSourceBuilder.minScore(query.getMinScore());
|
||||
}
|
||||
|
||||
if (query.getIndicesOptions() != null) {
|
||||
request.indicesOptions(query.getIndicesOptions());
|
||||
}
|
||||
|
||||
if (query.getPreference() != null) {
|
||||
request.preference(query.getPreference());
|
||||
}
|
||||
request.source(searchSourceBuilder);
|
||||
return request;
|
||||
}
|
||||
|
||||
private SearchRequest buildSearchRequest(Query query, ElasticsearchPersistentEntity<?> entity, @Nullable String index,
|
||||
@Nullable String type) {
|
||||
IndexCoordinates indexCoordinates = operations.determineIndex(entity, index, type);
|
||||
SearchRequest request = new SearchRequest(indices(query, indexCoordinates::getIndexName));
|
||||
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
|
||||
searchSourceBuilder.query(mappedQuery(query, entity));
|
||||
searchSourceBuilder.version(entity.hasVersionProperty());
|
||||
searchSourceBuilder.trackScores(query.getTrackScores());
|
||||
|
||||
QueryBuilder postFilterQuery = mappedFilterQuery(query, entity);
|
||||
if (postFilterQuery != null) {
|
||||
searchSourceBuilder.postFilter(postFilterQuery);
|
||||
}
|
||||
|
||||
if (query.getSourceFilter() != null) {
|
||||
searchSourceBuilder.fetchSource(query.getSourceFilter().getIncludes(), query.getSourceFilter().getExcludes());
|
||||
}
|
||||
|
||||
if (query instanceof SearchQuery && ((SearchQuery) query).getCollapseBuilder() != null) {
|
||||
searchSourceBuilder.collapse(((SearchQuery) query).getCollapseBuilder());
|
||||
}
|
||||
|
||||
sort(query, entity).forEach(searchSourceBuilder::sort);
|
||||
|
||||
if (query.getMinScore() > 0) {
|
||||
searchSourceBuilder.minScore(query.getMinScore());
|
||||
}
|
||||
|
||||
if (query.getIndicesOptions() != null) {
|
||||
request.indicesOptions(query.getIndicesOptions());
|
||||
}
|
||||
|
||||
if (query.getPreference() != null) {
|
||||
request.preference(query.getPreference());
|
||||
}
|
||||
|
||||
if (query.getSearchType() != null) {
|
||||
request.searchType(query.getSearchType());
|
||||
}
|
||||
|
||||
Pageable pageable = query.getPageable();
|
||||
|
||||
if (pageable.isPaged()) {
|
||||
|
||||
long offset = pageable.getOffset();
|
||||
if (offset > Integer.MAX_VALUE) {
|
||||
throw new IllegalArgumentException(String.format("Offset must not be more than %s", Integer.MAX_VALUE));
|
||||
}
|
||||
|
||||
searchSourceBuilder.from((int) offset);
|
||||
searchSourceBuilder.size(pageable.getPageSize());
|
||||
|
||||
request.source(searchSourceBuilder);
|
||||
} else {
|
||||
request.source(searchSourceBuilder);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations#count(Query, Class, String, String)
|
||||
*/
|
||||
@Override
|
||||
public Mono<Long> count(Query query, Class<?> entityType, String index, String type) {
|
||||
|
||||
// TODO: ES 7.0 has a dedicated CountRequest - use that one once available.
|
||||
return find(query, entityType, index, type).count();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
@ -457,6 +511,22 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
|
||||
return prepareWriteRequest(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Customization hook to modify a generated {@link SearchRequest} prior to its execution. Eg. by setting the
|
||||
* {@link SearchRequest#indicesOptions(IndicesOptions) indices options} if applicable.
|
||||
*
|
||||
* @param request the generated {@link CountRequest}.
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
protected CountRequest prepareCountRequest(CountRequest request) {
|
||||
|
||||
if (indicesOptions == null) {
|
||||
return request;
|
||||
}
|
||||
|
||||
return request.indicesOptions(indicesOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Customization hook to modify a generated {@link SearchRequest} prior to its execution. Eg. by setting the
|
||||
* {@link SearchRequest#indicesOptions(IndicesOptions) indices options} if applicable.
|
||||
@ -557,16 +627,32 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
|
||||
.onErrorResume(NoSuchIndexException.class, it -> Mono.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Customization hook on the actual execution result {@link Publisher}. <br />
|
||||
*
|
||||
* @param request the already prepared {@link CountRequest} ready to be executed.
|
||||
* @return a {@link Mono} emitting the result of the operation.
|
||||
*/
|
||||
protected Mono<Long> doCount(CountRequest request) {
|
||||
|
||||
if (QUERY_LOGGER.isDebugEnabled()) {
|
||||
QUERY_LOGGER.debug("Executing doCount: {}", request);
|
||||
}
|
||||
|
||||
return Mono.from(execute(client -> client.count(request))) //
|
||||
.onErrorResume(NoSuchIndexException.class, it -> Mono.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Customization hook on the actual execution result {@link Publisher}. <br />
|
||||
*
|
||||
* @param request the already prepared {@link SearchRequest} ready to be executed.
|
||||
* @return a {@link Flux} emitting the result of the operation.
|
||||
*/
|
||||
protected Flux<SearchHit> doScan(SearchRequest request) {
|
||||
protected Flux<SearchHit> doScroll(SearchRequest request) {
|
||||
|
||||
if (QUERY_LOGGER.isDebugEnabled()) {
|
||||
QUERY_LOGGER.debug("Executing doScan: {}", request);
|
||||
QUERY_LOGGER.debug("Executing doScroll: {}", request);
|
||||
}
|
||||
|
||||
return Flux.from(execute(client -> client.scroll(request))) //
|
||||
@ -665,9 +751,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
|
||||
elasticsearchQuery = new WrapperQueryBuilder(((StringQuery) query).getSource());
|
||||
} else if (query instanceof NativeSearchQuery) {
|
||||
elasticsearchQuery = ((NativeSearchQuery) query).getQuery();
|
||||
}
|
||||
|
||||
else {
|
||||
} else {
|
||||
throw new IllegalArgumentException(String.format("Unknown query type '%s'.", query.getClass()));
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,45 @@
|
||||
Spring Data Elasticsearch Changelog
|
||||
===================================
|
||||
|
||||
Changes in version 3.2.3.RELEASE (2019-12-04)
|
||||
---------------------------------------------
|
||||
* DATAES-700 - Enable proxy support for RestClient.
|
||||
* DATAES-699 - ElasticsearchRestTemplate.count(..) returns all documents instead of just total hits number.
|
||||
* DATAES-692 - Release 3.2.3 (Moore SR3).
|
||||
|
||||
|
||||
Changes in version 3.1.14.RELEASE (2019-12-04)
|
||||
----------------------------------------------
|
||||
* DATAES-691 - Release 3.1.14 (Lovelace SR14).
|
||||
|
||||
|
||||
Changes in version 3.2.2.RELEASE (2019-11-18)
|
||||
---------------------------------------------
|
||||
* DATAES-685 - Release 3.2.2 (Moore SR2).
|
||||
* DATAES-684 - Implement Bulk Request for Reactive.
|
||||
* DATAES-680 - ReactiveElasticsearchTemplate should use the count API.
|
||||
|
||||
|
||||
Changes in version 3.1.13.RELEASE (2019-11-18)
|
||||
----------------------------------------------
|
||||
* DATAES-683 - Release 3.1.13 (Lovelace SR13).
|
||||
|
||||
|
||||
Changes in version 3.2.1.RELEASE (2019-11-04)
|
||||
---------------------------------------------
|
||||
* DATAES-679 - Upgrade to Elasticsearch 6.8.4.
|
||||
* DATAES-673 - Create a Ssl Rest Client using SslContext and HostnameVerifier.
|
||||
* DATAES-671 - Missing indicesOptions support for scrolling queries.
|
||||
* DATAES-670 - fix version compatibility matrix in documentation.
|
||||
* DATAES-665 - Javadoc not deployed.
|
||||
* DATAES-662 - Release 3.2.1 (Moore SR1).
|
||||
|
||||
|
||||
Changes in version 3.1.12.RELEASE (2019-11-04)
|
||||
----------------------------------------------
|
||||
* DATAES-660 - Release 3.1.12 (Lovelace SR12).
|
||||
|
||||
|
||||
Changes in version 3.2.0.RELEASE (2019-09-30)
|
||||
---------------------------------------------
|
||||
* DATAES-657 - Sort by _score not supported by ElasticsearchRestTemplate.
|
||||
@ -883,3 +922,5 @@ Release Notes - Spring Data Elasticsearch - Version 1.0 M1 (2014-02-07)
|
||||
* #8 - java.lang.NoSuchMethodError: org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty.isVersionProperty()Z
|
||||
* #7 - Missing core types in org.springframework.data.elasticsearch.annotations.FieldType
|
||||
* #6 - spirng-data-elasticsearch with elasticsearch-river-mongodb
|
||||
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
Spring Data Elasticsearch 3.2 GA
|
||||
Spring Data Elasticsearch 3.2.3
|
||||
Copyright (c) [2013-2019] Pivotal Software, Inc.
|
||||
|
||||
This product is licensed to you under the Apache License, Version 2.0 (the "License").
|
||||
@ -8,3 +8,4 @@ This product may include a number of subcomponents with
|
||||
separate copyright notices and license terms. Your use of the source
|
||||
code for the these subcomponents is subject to the terms and
|
||||
conditions of the subcomponent's license, as noted in the LICENSE file.
|
||||
|
||||
|
@ -23,6 +23,7 @@ import java.time.Duration;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.apache.http.conn.ssl.NoopHostnameVerifier;
|
||||
import org.junit.Test;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
@ -31,6 +32,7 @@ import org.springframework.http.HttpHeaders;
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Henrique Amaral
|
||||
*/
|
||||
public class ClientConfigurationUnitTests {
|
||||
|
||||
@ -42,7 +44,7 @@ public class ClientConfigurationUnitTests {
|
||||
assertThat(clientConfiguration.getEndpoints()).containsOnly(InetSocketAddress.createUnresolved("localhost", 9200));
|
||||
}
|
||||
|
||||
@Test // DATAES-488, DATAES-504
|
||||
@Test // DATAES-488, DATAES-504, DATAES-650, DATAES-700
|
||||
public void shouldCreateCustomizedConfiguration() {
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
@ -52,7 +54,8 @@ public class ClientConfigurationUnitTests {
|
||||
.connectedTo("foo", "bar") //
|
||||
.usingSsl() //
|
||||
.withDefaultHeaders(headers) //
|
||||
.withConnectTimeout(Duration.ofDays(1)).withSocketTimeout(Duration.ofDays(2)).build();
|
||||
.withConnectTimeout(Duration.ofDays(1)).withSocketTimeout(Duration.ofDays(2)) //
|
||||
.withProxy("localhost:8080").build();
|
||||
|
||||
assertThat(clientConfiguration.getEndpoints()).containsOnly(InetSocketAddress.createUnresolved("foo", 9200),
|
||||
InetSocketAddress.createUnresolved("bar", 9200));
|
||||
@ -60,6 +63,7 @@ public class ClientConfigurationUnitTests {
|
||||
assertThat(clientConfiguration.getDefaultHeaders().get("foo")).containsOnly("bar");
|
||||
assertThat(clientConfiguration.getConnectTimeout()).isEqualTo(Duration.ofDays(1));
|
||||
assertThat(clientConfiguration.getSocketTimeout()).isEqualTo(Duration.ofDays(2));
|
||||
assertThat(clientConfiguration.getProxy()).contains("localhost:8080");
|
||||
}
|
||||
|
||||
@Test // DATAES-488, DATAES-504
|
||||
@ -117,6 +121,25 @@ public class ClientConfigurationUnitTests {
|
||||
assertThat(defaultHeaders.get(HttpHeaders.AUTHORIZATION)).isNull();
|
||||
}
|
||||
|
||||
@Test // DATAES-673
|
||||
public void shouldCreateSslConfigurationWithHostnameVerifier() {
|
||||
|
||||
SSLContext sslContext = mock(SSLContext.class);
|
||||
|
||||
ClientConfiguration clientConfiguration = ClientConfiguration.builder() //
|
||||
.connectedTo("foo", "bar") //
|
||||
.usingSsl(sslContext, NoopHostnameVerifier.INSTANCE) //
|
||||
.build();
|
||||
|
||||
assertThat(clientConfiguration.getEndpoints()).containsOnly(InetSocketAddress.createUnresolved("foo", 9200),
|
||||
InetSocketAddress.createUnresolved("bar", 9200));
|
||||
assertThat(clientConfiguration.useSsl()).isTrue();
|
||||
assertThat(clientConfiguration.getSslContext()).contains(sslContext);
|
||||
assertThat(clientConfiguration.getConnectTimeout()).isEqualTo(Duration.ofSeconds(10));
|
||||
assertThat(clientConfiguration.getSocketTimeout()).isEqualTo(Duration.ofSeconds(5));
|
||||
assertThat(clientConfiguration.getHostNameVerifier()).contains(NoopHostnameVerifier.INSTANCE);
|
||||
}
|
||||
|
||||
private static String buildBasicAuth(String username, String password) {
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
|
@ -0,0 +1,48 @@
|
||||
package org.springframework.data.elasticsearch.client;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.*;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.elasticsearch.client.RequestOptions;
|
||||
import org.elasticsearch.client.RestHighLevelClient;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.client.WireMock;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* @author Peter-Josef Meisch
|
||||
*/
|
||||
public class RestClientsTest {
|
||||
|
||||
@Test
|
||||
// DATAES-700
|
||||
public void shouldUseConfiguredProxy() throws IOException {
|
||||
|
||||
WireMockServer wireMockServer = new WireMockServer(options() //
|
||||
.dynamicPort() //
|
||||
.usingFilesUnderDirectory("src/test/resources/wiremock-mappings")); // needed, otherwise Wiremock goes to
|
||||
// test/resources/mappings
|
||||
wireMockServer.start();
|
||||
try {
|
||||
WireMock.configureFor(wireMockServer.port());
|
||||
|
||||
ClientConfigurationBuilder configurationBuilder = new ClientConfigurationBuilder();
|
||||
ClientConfiguration clientConfiguration = configurationBuilder //
|
||||
.connectedTo("localhost:9200")//
|
||||
.withProxy("localhost:" + wireMockServer.port()) //
|
||||
.build();
|
||||
|
||||
RestHighLevelClient restClient = RestClients.create(clientConfiguration).rest();
|
||||
restClient.ping(RequestOptions.DEFAULT);
|
||||
|
||||
verify(headRequestedFor(urlEqualTo("/")));
|
||||
|
||||
} finally {
|
||||
wireMockServer.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -22,6 +22,7 @@ import reactor.test.StepVerifier;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
@ -32,6 +33,7 @@ import org.elasticsearch.ElasticsearchStatusException;
|
||||
import org.elasticsearch.Version;
|
||||
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
|
||||
import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
|
||||
import org.elasticsearch.action.bulk.BulkRequest;
|
||||
import org.elasticsearch.action.delete.DeleteRequest;
|
||||
import org.elasticsearch.action.get.GetRequest;
|
||||
import org.elasticsearch.action.get.MultiGetRequest;
|
||||
@ -65,7 +67,7 @@ import org.springframework.test.context.junit4.SpringRunner;
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @currentRead Fool's Fate - Robin Hobb
|
||||
* @author Henrique Amaral
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@ContextConfiguration("classpath:infrastructure.xml")
|
||||
@ -653,6 +655,32 @@ public class ReactiveElasticsearchClientTests {
|
||||
.verifyError(ElasticsearchStatusException.class);
|
||||
}
|
||||
|
||||
@Test // DATAES-684
|
||||
public void bulkShouldUpdateExistingDocument() {
|
||||
String idFirstDoc = addSourceDocument().ofType(TYPE_I).to(INDEX_I);
|
||||
String idSecondDoc = addSourceDocument().ofType(TYPE_I).to(INDEX_I);
|
||||
|
||||
UpdateRequest requestFirstDoc = new UpdateRequest(INDEX_I, TYPE_I, idFirstDoc) //
|
||||
.doc(Collections.singletonMap("dutiful", "farseer"));
|
||||
UpdateRequest requestSecondDoc = new UpdateRequest(INDEX_I, TYPE_I, idSecondDoc) //
|
||||
.doc(Collections.singletonMap("secondDocUpdate", "secondDocUpdatePartTwo"));
|
||||
|
||||
BulkRequest bulkRequest = new BulkRequest();
|
||||
bulkRequest.add(requestFirstDoc);
|
||||
bulkRequest.add(requestSecondDoc);
|
||||
|
||||
client.bulk(bulkRequest).as(StepVerifier::create) //
|
||||
.consumeNextWith(it -> {
|
||||
assertThat(it.status()).isEqualTo(RestStatus.OK);
|
||||
assertThat(it.hasFailures()).isFalse();
|
||||
|
||||
Arrays.stream(it.getItems()).forEach(itemResponse -> {
|
||||
assertThat(itemResponse.status()).isEqualTo(RestStatus.OK);
|
||||
assertThat(itemResponse.getVersion()).isEqualTo(2);
|
||||
});
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
private AddToIndexOfType addSourceDocument() {
|
||||
return add(DOC_SOURCE);
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.data.elasticsearch.client.reactive.ReactiveMockClientTestsUtils.MockWebClientProvider.Receive.*;
|
||||
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
@ -29,6 +30,7 @@ import java.util.Collections;
|
||||
|
||||
import org.elasticsearch.ElasticsearchStatusException;
|
||||
import org.elasticsearch.action.DocWriteResponse.Result;
|
||||
import org.elasticsearch.action.bulk.BulkRequest;
|
||||
import org.elasticsearch.action.delete.DeleteRequest;
|
||||
import org.elasticsearch.action.get.GetRequest;
|
||||
import org.elasticsearch.action.get.MultiGetRequest;
|
||||
@ -51,7 +53,7 @@ import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @currentRead Golden Fool - Robin Hobb
|
||||
* @author Henrique Amaral
|
||||
*/
|
||||
public class ReactiveElasticsearchClientUnitTests {
|
||||
|
||||
@ -623,4 +625,25 @@ public class ReactiveElasticsearchClientUnitTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test // DATAES-684
|
||||
public void bulkShouldEmitResponseCorrectly() {
|
||||
|
||||
hostProvider.when(HOST) //
|
||||
.receiveBulkOk();
|
||||
|
||||
final UpdateRequest updateRequest = new UpdateRequest("twitter", "doc", "1")
|
||||
.doc(Collections.singletonMap("user", "cstrobl"));
|
||||
final BulkRequest bulkRequest = new BulkRequest();
|
||||
bulkRequest.add(updateRequest);
|
||||
|
||||
client.bulk(bulkRequest).as(StepVerifier::create) //
|
||||
.consumeNextWith(bulkResponse -> {
|
||||
|
||||
assertThat(bulkResponse.status()).isEqualTo(RestStatus.OK);
|
||||
assertThat(bulkResponse.hasFailures()).isFalse();
|
||||
|
||||
}) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ import org.springframework.web.util.UriBuilder;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @author Henrique Amaral
|
||||
*/
|
||||
public class ReactiveMockClientTestsUtils {
|
||||
|
||||
@ -361,6 +362,12 @@ public class ReactiveMockClientTestsUtils {
|
||||
});
|
||||
}
|
||||
|
||||
default Receive receiveBulkOk() {
|
||||
|
||||
return receiveJsonFromFile("bulk-ok") //
|
||||
.receive(Receive::ok);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public interface Receive {
|
||||
|
@ -1709,6 +1709,44 @@ public class ElasticsearchTemplateTests {
|
||||
elasticsearchTemplate.clearScroll(scroll.getScrollId());
|
||||
}
|
||||
|
||||
@Test // DATAES-671
|
||||
public void shouldPassIndicesOptionsForGivenSearchScrollQuery() {
|
||||
|
||||
// given
|
||||
long scrollTimeInMillis = 3000;
|
||||
String documentId = randomNumeric(5);
|
||||
SampleEntity sampleEntity = SampleEntity.builder().id(documentId).message("some message")
|
||||
.version(System.currentTimeMillis()).build();
|
||||
|
||||
IndexQuery idxQuery = new IndexQueryBuilder().withIndexName(INDEX_1_NAME).withId(sampleEntity.getId())
|
||||
.withObject(sampleEntity).build();
|
||||
|
||||
elasticsearchTemplate.index(idxQuery);
|
||||
elasticsearchTemplate.refresh(INDEX_1_NAME);
|
||||
|
||||
// when
|
||||
SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery())
|
||||
.withIndices(INDEX_1_NAME, INDEX_2_NAME).withIndicesOptions(IndicesOptions.lenientExpandOpen()).build();
|
||||
|
||||
List<SampleEntity> entities = new ArrayList<>();
|
||||
|
||||
ScrolledPage<SampleEntity> scroll = elasticsearchTemplate.startScroll(scrollTimeInMillis, searchQuery,
|
||||
SampleEntity.class, searchResultMapper);
|
||||
|
||||
entities.addAll(scroll.getContent());
|
||||
|
||||
while (scroll.hasContent()) {
|
||||
scroll = elasticsearchTemplate.continueScroll(scroll.getScrollId(), scrollTimeInMillis, SampleEntity.class,
|
||||
searchResultMapper);
|
||||
|
||||
entities.addAll(scroll.getContent());
|
||||
}
|
||||
|
||||
// then
|
||||
assertThat(entities).isNotNull();
|
||||
assertThat(entities.size()).isGreaterThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
@Test // DATAES-479
|
||||
public void shouldHonorTheHighlightBuilderOptions() {
|
||||
|
||||
|
@ -0,0 +1,23 @@
|
||||
{
|
||||
"took": 30,
|
||||
"errors": false,
|
||||
"items": [
|
||||
{
|
||||
"update": {
|
||||
"_index": "twitter",
|
||||
"_type": "doc",
|
||||
"_id": "1",
|
||||
"_version": 2,
|
||||
"result": "updated",
|
||||
"_shards": {
|
||||
"total": 2,
|
||||
"successful": 1,
|
||||
"failed": 0
|
||||
},
|
||||
"_seq_no": 2,
|
||||
"_primary_term": 4
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
}
|
Binary file not shown.
@ -20,7 +20,7 @@
|
||||
description=Adds "built in" analyzers to Elasticsearch.
|
||||
#
|
||||
# 'version': plugin's version
|
||||
version=6.8.1
|
||||
version=6.8.4
|
||||
#
|
||||
# 'name': the plugin name
|
||||
name=analysis-common
|
||||
@ -35,7 +35,7 @@ classname=org.elasticsearch.analysis.common.CommonAnalysisPlugin
|
||||
java.version=1.8
|
||||
#
|
||||
# 'elasticsearch.version': version of elasticsearch compiled against
|
||||
elasticsearch.version=6.8.1
|
||||
elasticsearch.version=6.8.4
|
||||
### optional elements for plugins:
|
||||
#
|
||||
# 'extended.plugins': other plugins this plugin extends through SPI
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -20,7 +20,7 @@
|
||||
description=Module for ingest processors that do not require additional security permissions or have large dependencies and resources
|
||||
#
|
||||
# 'version': plugin's version
|
||||
version=6.8.1
|
||||
version=6.8.4
|
||||
#
|
||||
# 'name': the plugin name
|
||||
name=ingest-common
|
||||
@ -35,7 +35,7 @@ classname=org.elasticsearch.ingest.common.IngestCommonPlugin
|
||||
java.version=1.8
|
||||
#
|
||||
# 'elasticsearch.version': version of elasticsearch compiled against
|
||||
elasticsearch.version=6.8.1
|
||||
elasticsearch.version=6.8.4
|
||||
### optional elements for plugins:
|
||||
#
|
||||
# 'extended.plugins': other plugins this plugin extends through SPI
|
||||
|
Binary file not shown.
Binary file not shown.
@ -20,7 +20,7 @@
|
||||
description=Lucene expressions integration for Elasticsearch
|
||||
#
|
||||
# 'version': plugin's version
|
||||
version=6.8.1
|
||||
version=6.8.4
|
||||
#
|
||||
# 'name': the plugin name
|
||||
name=lang-expression
|
||||
@ -35,7 +35,7 @@ classname=org.elasticsearch.script.expression.ExpressionPlugin
|
||||
java.version=1.8
|
||||
#
|
||||
# 'elasticsearch.version': version of elasticsearch compiled against
|
||||
elasticsearch.version=6.8.1
|
||||
elasticsearch.version=6.8.4
|
||||
### optional elements for plugins:
|
||||
#
|
||||
# 'extended.plugins': other plugins this plugin extends through SPI
|
||||
|
Binary file not shown.
Binary file not shown.
@ -20,7 +20,7 @@
|
||||
description=An easy, safe and fast scripting language for Elasticsearch
|
||||
#
|
||||
# 'version': plugin's version
|
||||
version=6.8.1
|
||||
version=6.8.4
|
||||
#
|
||||
# 'name': the plugin name
|
||||
name=lang-painless
|
||||
@ -35,7 +35,7 @@ classname=org.elasticsearch.painless.PainlessPlugin
|
||||
java.version=1.8
|
||||
#
|
||||
# 'elasticsearch.version': version of elasticsearch compiled against
|
||||
elasticsearch.version=6.8.1
|
||||
elasticsearch.version=6.8.4
|
||||
### optional elements for plugins:
|
||||
#
|
||||
# 'extended.plugins': other plugins this plugin extends through SPI
|
||||
|
Binary file not shown.
@ -20,7 +20,7 @@
|
||||
description=Adds advanced field mappers
|
||||
#
|
||||
# 'version': plugin's version
|
||||
version=6.8.1
|
||||
version=6.8.4
|
||||
#
|
||||
# 'name': the plugin name
|
||||
name=mapper-extras
|
||||
@ -35,7 +35,7 @@ classname=org.elasticsearch.index.mapper.MapperExtrasPlugin
|
||||
java.version=1.8
|
||||
#
|
||||
# 'elasticsearch.version': version of elasticsearch compiled against
|
||||
elasticsearch.version=6.8.1
|
||||
elasticsearch.version=6.8.4
|
||||
### optional elements for plugins:
|
||||
#
|
||||
# 'extended.plugins': other plugins this plugin extends through SPI
|
||||
|
@ -20,7 +20,7 @@
|
||||
description=The Reindex module adds APIs to reindex from one index to another or update documents in place.
|
||||
#
|
||||
# 'version': plugin's version
|
||||
version=6.8.1
|
||||
version=6.8.4
|
||||
#
|
||||
# 'name': the plugin name
|
||||
name=reindex
|
||||
@ -35,7 +35,7 @@ classname=org.elasticsearch.index.reindex.ReindexPlugin
|
||||
java.version=1.8
|
||||
#
|
||||
# 'elasticsearch.version': version of elasticsearch compiled against
|
||||
elasticsearch.version=6.8.1
|
||||
elasticsearch.version=6.8.4
|
||||
### optional elements for plugins:
|
||||
#
|
||||
# 'extended.plugins': other plugins this plugin extends through SPI
|
||||
|
@ -20,7 +20,7 @@
|
||||
description=Module for URL repository
|
||||
#
|
||||
# 'version': plugin's version
|
||||
version=6.8.1
|
||||
version=6.8.4
|
||||
#
|
||||
# 'name': the plugin name
|
||||
name=repository-url
|
||||
@ -35,7 +35,7 @@ classname=org.elasticsearch.plugin.repository.url.URLRepositoryPlugin
|
||||
java.version=1.8
|
||||
#
|
||||
# 'elasticsearch.version': version of elasticsearch compiled against
|
||||
elasticsearch.version=6.8.1
|
||||
elasticsearch.version=6.8.4
|
||||
### optional elements for plugins:
|
||||
#
|
||||
# 'extended.plugins': other plugins this plugin extends through SPI
|
||||
|
Loading…
x
Reference in New Issue
Block a user