This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 28f5ef2ba89e CAMEL-26233: camel-opensearch - send a built request as
it is, keep the whole JSON search body, take the DeleteIndex name from a String
body, fail a null Index/Update body and read a file as JSON (#29075)
28f5ef2ba89e is described below
commit 28f5ef2ba89e573640903c08a32e563d941bee11
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Nov 3 12:11:49 2026 +0100
CAMEL-26233: camel-opensearch - send a built request as it is, keep the
whole JSON search body, take the DeleteIndex name from a String body, fail a
null Index/Update body and read a file as JSON (#29075)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../camel/catalog/components/opensearch.json | 2 +-
.../camel/catalog/docs/opensearch-component.adoc | 31 +-
.../OpensearchActionRequestConverterLoader.java | 8 +
.../camel/component/opensearch/opensearch.json | 2 +-
.../src/main/docs/opensearch-component.adoc | 31 +-
.../component/opensearch/OpensearchEndpoint.java | 30 +-
.../component/opensearch/OpensearchProducer.java | 24 +-
.../OpensearchActionRequestConverter.java | 135 ++++++--
.../opensearch/OpensearchProducerBodyTest.java | 338 +++++++++++++++++++++
.../OpensearchActionRequestConverterTest.java | 133 ++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 19 ++
11 files changed, 682 insertions(+), 71 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opensearch.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opensearch.json
index aca514e6dbe7..146664596a51 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opensearch.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opensearch.json
@@ -53,7 +53,7 @@
"CamelOpensearchFrom": { "index": 8, "kind": "header", "displayName": "",
"group": "producer", "label": "", "required": false, "javaType": "Integer",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The starting index of the response.", "constantName":
"org.apache.camel.component.opensearch.OpensearchConstants#PARAM_FROM" }
},
"bodyTypes": {
- "producerIn": { "carriers": [ "text", "bytes", "stream", "map", "list",
"pojo" ], "byOption": [ { "option": "operation", "value": "Index", "carriers":
[ "text", "bytes", "stream", "map", "pojo" ] }, { "option": "operation",
"value": "Update", "carriers": [ "text", "bytes", "stream", "map", "pojo" ] },
{ "option": "operation", "value": "Bulk", "carriers": [ "list", "pojo" ] }, {
"option": "operation", "value": "GetById", "carriers": [ "text", "pojo" ] }, {
"option": "operation", "valu [...]
+ "producerIn": { "carriers": [ "text", "bytes", "stream", "file", "map",
"list", "pojo" ], "byOption": [ { "option": "operation", "value": "Index",
"carriers": [ "text", "bytes", "stream", "file", "map", "pojo" ] }, { "option":
"operation", "value": "Update", "carriers": [ "text", "bytes", "stream",
"file", "map", "pojo" ] }, { "option": "operation", "value": "Bulk",
"carriers": [ "list", "pojo" ] }, { "option": "operation", "value": "GetById",
"carriers": [ "text", "pojo" ] }, { "opt [...]
"producerOut": { "carriers": [ "text", "value", "pojo", "list", "iterator"
], "byOption": [ { "option": "operation", "value": "Index", "carriers": [
"text" ], "javaType": "java.lang.String" }, { "option": "operation", "value":
"Update", "carriers": [ "text" ], "javaType": "java.lang.String" }, { "option":
"operation", "value": "Bulk", "carriers": [ "list" ] }, { "option":
"operation", "value": "GetById", "carriers": [ "pojo" ], "javaType":
"org.opensearch.client.opensearch.core.GetRe [...]
},
"properties": {
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opensearch-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opensearch-component.adoc
index 5c4f078dbf16..0059906dcada 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opensearch-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opensearch-component.adoc
@@ -50,37 +50,44 @@ Set an endpoint URI option or exchange header with a key of
"operation"
and a value set to one of the following. Some operations also require
other parameters or the message body to be set.
+A request of the OpenSearch client in the message body, such as an
`IndexRequest`, a `GetRequest` or a `SearchRequest`,
+sets the operation by its type, whatever the option or header say, and is sent
as it is: its index, id and the rest are
+not changed by the headers or options.
+
[width="100%",cols="10%,10%,80%",options="header",]
|===
|operation |message body |description
-|`Index` |`Map`, `String`, `byte[]`, `Reader`, `InputStream` or
`IndexRequest.Builder` content to index |Adds content to an index and returns
the content's indexId in the body.
+|`Index` |`Map`, `String`, `byte[]`, `Reader`, `InputStream`, a file
(`java.io.File`, `Path` or the file of the file component), `IndexRequest` or
`IndexRequest.Builder` content to index |Adds content to an index and returns
the content's indexId in the body.
+A file is read as the JSON of the document, and a null body fails.
You can set the name of the target index by setting the message header with
the key "indexName".
You can set the indexId by setting the message header with
the key "indexId".
-|`GetById` |`String` or `GetRequest.Builder` index id of content to retrieve
|Retrieves the document corresponding to the given index id and returns a
GetResponse object in the body.
+|`GetById` |`String`, `GetRequest` or `GetRequest.Builder` index id of content
to retrieve |Retrieves the document corresponding to the given index id and
returns a GetResponse object in the body.
You can set the name of the target index by setting the message header with
the key "indexName".
You can set the type of document by setting the message header with
the key "documentClass".
-|`Delete` |`String` or `DeleteRequest.Builder` index id of content to delete
|Deletes the specified indexName and returns a Result object in the body.
+|`Delete` |`String`, `DeleteRequest` or `DeleteRequest.Builder` index id of
content to delete |Deletes the specified indexName and returns a Result object
in the body.
You can set the name of the target index by setting the message header with
the key "indexName".
-|`DeleteIndex` |`String` (its text is not used) or
`DeleteIndexRequest.Builder` |Deletes the index named by the indexName header
or option, not the one in a `String` body, and returns a `Boolean` in the body
that is true when the deletion is acknowledged.
+|`DeleteIndex` |`String` name of the index, `DeleteIndexRequest` or
`DeleteIndexRequest.Builder` |Deletes the index named by the indexName header
or option, or else the one named by a `String` body, and returns a `Boolean` in
the body that is true when the deletion is acknowledged.
You can set the name of the target index by setting the message header with
the key "indexName".
-|`Bulk` |`Iterable` or `BulkRequest.Builder` of any type that is already
accepted (DeleteOperation.Builder for delete operation, UpdateOperation.Builder
for update operation, CreateOperation.Builder for create operation, byte[],
InputStream, String, Reader, Map or any document type for index operation) |
Adds/Updates/Deletes content from/to an index and returns a
List<BulkResponseItem> object in the body
+|`Bulk` |`Iterable`, `BulkRequest` or `BulkRequest.Builder` of any type that
is already accepted (DeleteOperation.Builder for delete operation,
UpdateOperation.Builder for update operation, CreateOperation.Builder for
create operation, byte[], InputStream, String, Reader, Map or any document type
for index operation) | Adds/Updates/Deletes content from/to an index and
returns a List<BulkResponseItem> object in the body
You can set the name of the target index by setting the message header with
the key "indexName".
-|`Search` |`Map`, `String` or `SearchRequest.Builder` |Search the content with
the map of query string.
+|`Search` |`Map`, `String`, `SearchRequest` or `SearchRequest.Builder` |Search
the content with the search request as a JSON `String` or a `Map`: its query,
size, from, sort, aggregations, _source and the rest are used.
+A JSON object with a single member other than query, such as `{"match":
{...}}`, is the query alone.
You can set the name of the target index by setting the message header with
the key "indexName".
You can set the number of hits to return by setting the message header with
the key "size".
You can set the starting document offset by setting the message header with
the key "from".
+The size and from headers or options, when set, override those of the body.
-|`MultiSearch` |`MsearchRequest.Builder` |Multiple search in one
+|`MultiSearch` |`MsearchRequest` or `MsearchRequest.Builder` |Multiple search
in one
-|`MultiGet` |`Iterable<String>` or `MgetRequest.Builder` the id of the
document to retrieve |Multiple get in one
+|`MultiGet` |`Iterable<String>`, `MgetRequest` or `MgetRequest.Builder` the id
of the document to retrieve |Multiple get in one
You can set the name of the target index by setting the message header with
the key "indexName".
@@ -88,7 +95,8 @@ You can set the name of the target index by setting the
message header with the
You must set the name of the target index by setting the message header with
the key "indexName".
-|`Update` |`byte[]`, `InputStream`, `String`, `Reader`, `Map` or any document
type content to update |Updates content to an index and returns the content's
indexId in the body.
+|`Update` |`byte[]`, `InputStream`, `String`, `Reader`, `Map`, a file,
`UpdateRequest`, `UpdateRequest.Builder` or any document type content to update
|Updates content to an index and returns the content's indexId in the body.
+A file is read as JSON, and a null body fails.
You can set the name of the target index by setting the message header with
the key "indexName".
You can set the indexId by setting the message header with
the key "indexId".
@@ -187,7 +195,8 @@ String indexId = template.requestBody("direct:index", map,
String.class);
=== Search Example
Searching on specific field(s) and value use the Operation ´Search´.
-Pass in the query JSON String or the Map
+Pass in the search request as a JSON String or a Map: its size, sort,
aggregations and the other members
+are used with its query.
[tabs]
====
@@ -228,7 +237,7 @@ YAML::
._Java-only: Java test API (ProducerTemplate)_
[source,java]
----
-String query = "{\"query\":{\"match\":{\"doc.content\":\"new release of
ApacheCamel\"}}}";
+String query = "{\"query\":{\"match\":{\"doc.content\":\"new release of
ApacheCamel\"}},\"size\":5}";
HitsMetadata<?> response = template.requestBody("direct:search", query,
HitsMetadata.class);
----
diff --git
a/components/camel-opensearch/src/generated/java/org/apache/camel/component/opensearch/converter/OpensearchActionRequestConverterLoader.java
b/components/camel-opensearch/src/generated/java/org/apache/camel/component/opensearch/converter/OpensearchActionRequestConverterLoader.java
index fdb86d331c01..43489a882424 100644
---
a/components/camel-opensearch/src/generated/java/org/apache/camel/component/opensearch/converter/OpensearchActionRequestConverterLoader.java
+++
b/components/camel-opensearch/src/generated/java/org/apache/camel/component/opensearch/converter/OpensearchActionRequestConverterLoader.java
@@ -84,6 +84,14 @@ public final class OpensearchActionRequestConverterLoader
implements TypeConvert
}
return answer;
});
+ addTypeConverter(registry,
org.opensearch.client.opensearch.core.MsearchRequest.Builder.class,
java.lang.Object.class, false,
+ (type, exchange, value) -> {
+ Object answer =
org.apache.camel.component.opensearch.converter.OpensearchActionRequestConverter.toMsearchRequestBuilder(value,
exchange);
+ if (false && answer == null) {
+ answer = Void.class;
+ }
+ return answer;
+ });
addTypeConverter(registry,
org.opensearch.client.opensearch.core.SearchRequest.Builder.class,
java.lang.Object.class, false,
(type, exchange, value) -> {
Object answer =
org.apache.camel.component.opensearch.converter.OpensearchActionRequestConverter.toSearchRequestBuilder(value,
exchange);
diff --git
a/components/camel-opensearch/src/generated/resources/META-INF/org/apache/camel/component/opensearch/opensearch.json
b/components/camel-opensearch/src/generated/resources/META-INF/org/apache/camel/component/opensearch/opensearch.json
index aca514e6dbe7..146664596a51 100644
---
a/components/camel-opensearch/src/generated/resources/META-INF/org/apache/camel/component/opensearch/opensearch.json
+++
b/components/camel-opensearch/src/generated/resources/META-INF/org/apache/camel/component/opensearch/opensearch.json
@@ -53,7 +53,7 @@
"CamelOpensearchFrom": { "index": 8, "kind": "header", "displayName": "",
"group": "producer", "label": "", "required": false, "javaType": "Integer",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The starting index of the response.", "constantName":
"org.apache.camel.component.opensearch.OpensearchConstants#PARAM_FROM" }
},
"bodyTypes": {
- "producerIn": { "carriers": [ "text", "bytes", "stream", "map", "list",
"pojo" ], "byOption": [ { "option": "operation", "value": "Index", "carriers":
[ "text", "bytes", "stream", "map", "pojo" ] }, { "option": "operation",
"value": "Update", "carriers": [ "text", "bytes", "stream", "map", "pojo" ] },
{ "option": "operation", "value": "Bulk", "carriers": [ "list", "pojo" ] }, {
"option": "operation", "value": "GetById", "carriers": [ "text", "pojo" ] }, {
"option": "operation", "valu [...]
+ "producerIn": { "carriers": [ "text", "bytes", "stream", "file", "map",
"list", "pojo" ], "byOption": [ { "option": "operation", "value": "Index",
"carriers": [ "text", "bytes", "stream", "file", "map", "pojo" ] }, { "option":
"operation", "value": "Update", "carriers": [ "text", "bytes", "stream",
"file", "map", "pojo" ] }, { "option": "operation", "value": "Bulk",
"carriers": [ "list", "pojo" ] }, { "option": "operation", "value": "GetById",
"carriers": [ "text", "pojo" ] }, { "opt [...]
"producerOut": { "carriers": [ "text", "value", "pojo", "list", "iterator"
], "byOption": [ { "option": "operation", "value": "Index", "carriers": [
"text" ], "javaType": "java.lang.String" }, { "option": "operation", "value":
"Update", "carriers": [ "text" ], "javaType": "java.lang.String" }, { "option":
"operation", "value": "Bulk", "carriers": [ "list" ] }, { "option":
"operation", "value": "GetById", "carriers": [ "pojo" ], "javaType":
"org.opensearch.client.opensearch.core.GetRe [...]
},
"properties": {
diff --git
a/components/camel-opensearch/src/main/docs/opensearch-component.adoc
b/components/camel-opensearch/src/main/docs/opensearch-component.adoc
index 5c4f078dbf16..0059906dcada 100644
--- a/components/camel-opensearch/src/main/docs/opensearch-component.adoc
+++ b/components/camel-opensearch/src/main/docs/opensearch-component.adoc
@@ -50,37 +50,44 @@ Set an endpoint URI option or exchange header with a key of
"operation"
and a value set to one of the following. Some operations also require
other parameters or the message body to be set.
+A request of the OpenSearch client in the message body, such as an
`IndexRequest`, a `GetRequest` or a `SearchRequest`,
+sets the operation by its type, whatever the option or header say, and is sent
as it is: its index, id and the rest are
+not changed by the headers or options.
+
[width="100%",cols="10%,10%,80%",options="header",]
|===
|operation |message body |description
-|`Index` |`Map`, `String`, `byte[]`, `Reader`, `InputStream` or
`IndexRequest.Builder` content to index |Adds content to an index and returns
the content's indexId in the body.
+|`Index` |`Map`, `String`, `byte[]`, `Reader`, `InputStream`, a file
(`java.io.File`, `Path` or the file of the file component), `IndexRequest` or
`IndexRequest.Builder` content to index |Adds content to an index and returns
the content's indexId in the body.
+A file is read as the JSON of the document, and a null body fails.
You can set the name of the target index by setting the message header with
the key "indexName".
You can set the indexId by setting the message header with
the key "indexId".
-|`GetById` |`String` or `GetRequest.Builder` index id of content to retrieve
|Retrieves the document corresponding to the given index id and returns a
GetResponse object in the body.
+|`GetById` |`String`, `GetRequest` or `GetRequest.Builder` index id of content
to retrieve |Retrieves the document corresponding to the given index id and
returns a GetResponse object in the body.
You can set the name of the target index by setting the message header with
the key "indexName".
You can set the type of document by setting the message header with
the key "documentClass".
-|`Delete` |`String` or `DeleteRequest.Builder` index id of content to delete
|Deletes the specified indexName and returns a Result object in the body.
+|`Delete` |`String`, `DeleteRequest` or `DeleteRequest.Builder` index id of
content to delete |Deletes the specified indexName and returns a Result object
in the body.
You can set the name of the target index by setting the message header with
the key "indexName".
-|`DeleteIndex` |`String` (its text is not used) or
`DeleteIndexRequest.Builder` |Deletes the index named by the indexName header
or option, not the one in a `String` body, and returns a `Boolean` in the body
that is true when the deletion is acknowledged.
+|`DeleteIndex` |`String` name of the index, `DeleteIndexRequest` or
`DeleteIndexRequest.Builder` |Deletes the index named by the indexName header
or option, or else the one named by a `String` body, and returns a `Boolean` in
the body that is true when the deletion is acknowledged.
You can set the name of the target index by setting the message header with
the key "indexName".
-|`Bulk` |`Iterable` or `BulkRequest.Builder` of any type that is already
accepted (DeleteOperation.Builder for delete operation, UpdateOperation.Builder
for update operation, CreateOperation.Builder for create operation, byte[],
InputStream, String, Reader, Map or any document type for index operation) |
Adds/Updates/Deletes content from/to an index and returns a
List<BulkResponseItem> object in the body
+|`Bulk` |`Iterable`, `BulkRequest` or `BulkRequest.Builder` of any type that
is already accepted (DeleteOperation.Builder for delete operation,
UpdateOperation.Builder for update operation, CreateOperation.Builder for
create operation, byte[], InputStream, String, Reader, Map or any document type
for index operation) | Adds/Updates/Deletes content from/to an index and
returns a List<BulkResponseItem> object in the body
You can set the name of the target index by setting the message header with
the key "indexName".
-|`Search` |`Map`, `String` or `SearchRequest.Builder` |Search the content with
the map of query string.
+|`Search` |`Map`, `String`, `SearchRequest` or `SearchRequest.Builder` |Search
the content with the search request as a JSON `String` or a `Map`: its query,
size, from, sort, aggregations, _source and the rest are used.
+A JSON object with a single member other than query, such as `{"match":
{...}}`, is the query alone.
You can set the name of the target index by setting the message header with
the key "indexName".
You can set the number of hits to return by setting the message header with
the key "size".
You can set the starting document offset by setting the message header with
the key "from".
+The size and from headers or options, when set, override those of the body.
-|`MultiSearch` |`MsearchRequest.Builder` |Multiple search in one
+|`MultiSearch` |`MsearchRequest` or `MsearchRequest.Builder` |Multiple search
in one
-|`MultiGet` |`Iterable<String>` or `MgetRequest.Builder` the id of the
document to retrieve |Multiple get in one
+|`MultiGet` |`Iterable<String>`, `MgetRequest` or `MgetRequest.Builder` the id
of the document to retrieve |Multiple get in one
You can set the name of the target index by setting the message header with
the key "indexName".
@@ -88,7 +95,8 @@ You can set the name of the target index by setting the
message header with the
You must set the name of the target index by setting the message header with
the key "indexName".
-|`Update` |`byte[]`, `InputStream`, `String`, `Reader`, `Map` or any document
type content to update |Updates content to an index and returns the content's
indexId in the body.
+|`Update` |`byte[]`, `InputStream`, `String`, `Reader`, `Map`, a file,
`UpdateRequest`, `UpdateRequest.Builder` or any document type content to update
|Updates content to an index and returns the content's indexId in the body.
+A file is read as JSON, and a null body fails.
You can set the name of the target index by setting the message header with
the key "indexName".
You can set the indexId by setting the message header with
the key "indexId".
@@ -187,7 +195,8 @@ String indexId = template.requestBody("direct:index", map,
String.class);
=== Search Example
Searching on specific field(s) and value use the Operation ´Search´.
-Pass in the query JSON String or the Map
+Pass in the search request as a JSON String or a Map: its size, sort,
aggregations and the other members
+are used with its query.
[tabs]
====
@@ -228,7 +237,7 @@ YAML::
._Java-only: Java test API (ProducerTemplate)_
[source,java]
----
-String query = "{\"query\":{\"match\":{\"doc.content\":\"new release of
ApacheCamel\"}}}";
+String query = "{\"query\":{\"match\":{\"doc.content\":\"new release of
ApacheCamel\"}},\"size\":5}";
HitsMetadata<?> response = template.requestBody("direct:search", query,
HitsMetadata.class);
----
diff --git
a/components/camel-opensearch/src/main/java/org/apache/camel/component/opensearch/OpensearchEndpoint.java
b/components/camel-opensearch/src/main/java/org/apache/camel/component/opensearch/OpensearchEndpoint.java
index 8b267d3ae984..568d53dfae55 100644
---
a/components/camel-opensearch/src/main/java/org/apache/camel/component/opensearch/OpensearchEndpoint.java
+++
b/components/camel-opensearch/src/main/java/org/apache/camel/component/opensearch/OpensearchEndpoint.java
@@ -37,16 +37,16 @@ import org.opensearch.client.opensearch.OpenSearchClient;
syntax = "opensearch:clusterName", producerOnly = true,
category = { Category.SEARCH, Category.MONITORING }, headersClass
= OpensearchConstants.class)
@BodyTypes(producerIn = @BodyType(value = {
- Carrier.TEXT, Carrier.BYTES, Carrier.STREAM, Carrier.MAP,
Carrier.LIST, Carrier.POJO },
+ Carrier.TEXT, Carrier.BYTES, Carrier.STREAM, Carrier.FILE,
Carrier.MAP, Carrier.LIST, Carrier.POJO },
byOption = {
@BodyType.When(option = "operation",
value = "Index",
carriers = {
- Carrier.TEXT,
Carrier.BYTES, Carrier.STREAM, Carrier.MAP,
- Carrier.POJO
}),
+ Carrier.TEXT,
Carrier.BYTES, Carrier.STREAM, Carrier.FILE,
+ Carrier.MAP,
Carrier.POJO }),
@BodyType.When(option = "operation",
value = "Update",
carriers = {
- Carrier.TEXT,
Carrier.BYTES, Carrier.STREAM, Carrier.MAP,
- Carrier.POJO
}),
+ Carrier.TEXT,
Carrier.BYTES, Carrier.STREAM, Carrier.FILE,
+ Carrier.MAP,
Carrier.POJO }),
@BodyType.When(option = "operation",
value = "Bulk",
carriers = {
Carrier.LIST, Carrier.POJO }),
@BodyType.When(option = "operation",
value = "GetById",
@@ -63,16 +63,18 @@ import org.opensearch.client.opensearch.OpenSearchClient;
carriers = {
Carrier.TEXT, Carrier.MAP, Carrier.POJO }),
@BodyType.When(option = "operation",
value = "Exists", carriers = Carrier.ANY),
@BodyType.When(option = "operation",
value = "Ping", carriers = Carrier.ANY) },
- description = "By the operation, set by the
operation option or the CamelOpensearchOperation header."
- + " Index and Update take the
document as JSON text, bytes or a stream, a Map"
- + " or a POJO (written as
JSON), or the request builder of the OpenSearch"
- + " client; text that is not
JSON, a List, a java.io.File or no body fail."
+ description = "By the operation, set by the
operation option or the CamelOpensearchOperation header,"
+ + " or by the request of the
OpenSearch client in the body. Index and Update"
+ + " take the document as JSON
text, bytes, a stream or a file of it, or a Map"
+ + " or a POJO (written as
JSON); text that is not JSON, a List or no body fail."
+ " Update takes the partial
document, or the Update API form {\"doc\": {...}}. GetById and Delete take the
id as text, Bulk and MultiGet a List"
- + " of documents or ids,
Search a query as JSON text or a Map (only its query"
- + " member when it has one),
and MultiSearch only an MsearchRequest.Builder;"
- + " each also takes its
request builder. DeleteIndex needs a text body but"
- + " deletes the index of the
CamelOpensearchIndexName header or indexName, not the one"
- + " in the body. Exists and
Ping do not use the body"),
+ + " of documents or ids,
Search the search request as JSON text or a Map, with"
+ + " its query, size, sort,
aggregations and the rest (an object with a single"
+ + " member other than query is
the query alone), and DeleteIndex the name of"
+ + " the index as text, used
when neither the CamelOpensearchIndexName header"
+ + " nor indexName is set. Each
also takes its request of the OpenSearch"
+ + " client, or the builder of
it, which is sent as it is; MultiSearch takes"
+ + " only those. Exists and
Ping do not use the body"),
producerOut = @BodyType(value = { Carrier.TEXT, Carrier.VALUE,
Carrier.POJO, Carrier.LIST, Carrier.ITERATOR },
byOption = {
@BodyType.When(option =
"operation", value = "Index", carriers = Carrier.TEXT,
diff --git
a/components/camel-opensearch/src/main/java/org/apache/camel/component/opensearch/OpensearchProducer.java
b/components/camel-opensearch/src/main/java/org/apache/camel/component/opensearch/OpensearchProducer.java
index 16f8a2d00b47..872a9055e2e2 100644
---
a/components/camel-opensearch/src/main/java/org/apache/camel/component/opensearch/OpensearchProducer.java
+++
b/components/camel-opensearch/src/main/java/org/apache/camel/component/opensearch/OpensearchProducer.java
@@ -237,7 +237,7 @@ class OpensearchProducer extends DefaultAsyncProducer {
SearchRequest.Builder searchRequestBuilder =
message.getBody(SearchRequest.Builder.class);
if (searchRequestBuilder == null) {
throw new IllegalArgumentException(
- "Wrong body type. Only Map, String or
SearchRequest.Builder is allowed as a type");
+ "Wrong body type. Only Map, String,
SearchRequest or SearchRequest.Builder is allowed as a type");
}
// is it a scroll request ?
boolean useScroll = message.getHeader(PARAM_SCROLL,
configuration.isUseScroll(), Boolean.class);
@@ -291,7 +291,8 @@ class OpensearchProducer extends DefaultAsyncProducer {
private void processMultiGetAsync(ActionContext ctx, Class<?>
documentClass) throws IOException {
MgetRequest.Builder mgetRequestBuilder =
ctx.getMessage().getBody(MgetRequest.Builder.class);
if (mgetRequestBuilder == null) {
- throw new IllegalArgumentException("Wrong body type. Only
MgetRequest.Builder is allowed as a type");
+ throw new IllegalArgumentException(
+ "Wrong body type. Only Iterable, MgetRequest or
MgetRequest.Builder is allowed as a type");
}
onComplete(
ctx.getClient().mget(mgetRequestBuilder.build(), documentClass)
@@ -305,7 +306,8 @@ class OpensearchProducer extends DefaultAsyncProducer {
private void processMultiSearchAsync(ActionContext ctx, Class<?>
documentClass) throws IOException {
MsearchRequest.Builder msearchRequestBuilder =
ctx.getMessage().getBody(MsearchRequest.Builder.class);
if (msearchRequestBuilder == null) {
- throw new IllegalArgumentException("Wrong body type. Only
MsearchRequest.Builder is allowed as a type");
+ throw new IllegalArgumentException(
+ "Wrong body type. Only MsearchRequest or
MsearchRequest.Builder is allowed as a type");
}
onComplete(
ctx.getClient().msearch(msearchRequestBuilder.build(),
documentClass)
@@ -332,7 +334,7 @@ class OpensearchProducer extends DefaultAsyncProducer {
DeleteIndexRequest.Builder deleteIndexRequestBuilder =
ctx.getMessage().getBody(DeleteIndexRequest.Builder.class);
if (deleteIndexRequestBuilder == null) {
throw new IllegalArgumentException(
- "Wrong body type. Only String or
DeleteIndexRequest.Builder is allowed as a type");
+ "Wrong body type. Only String, DeleteIndexRequest or
DeleteIndexRequest.Builder is allowed as a type");
}
onComplete(
ctx.getClient().indices().delete(deleteIndexRequestBuilder.build())
@@ -347,7 +349,7 @@ class OpensearchProducer extends DefaultAsyncProducer {
DeleteRequest.Builder deleteRequestBuilder =
ctx.getMessage().getBody(DeleteRequest.Builder.class);
if (deleteRequestBuilder == null) {
throw new IllegalArgumentException(
- "Wrong body type. Only String or DeleteRequest.Builder is
allowed as a type");
+ "Wrong body type. Only String, DeleteRequest or
DeleteRequest.Builder is allowed as a type");
}
onComplete(
ctx.getClient().delete(deleteRequestBuilder.build())
@@ -362,7 +364,7 @@ class OpensearchProducer extends DefaultAsyncProducer {
BulkRequest.Builder bulkRequestBuilder =
ctx.getMessage().getBody(BulkRequest.Builder.class);
if (bulkRequestBuilder == null) {
throw new IllegalArgumentException(
- "Wrong body type. Only Iterable or BulkRequest.Builder is
allowed as a type");
+ "Wrong body type. Only Iterable, BulkRequest or
BulkRequest.Builder is allowed as a type");
}
onComplete(
ctx.getClient().bulk(bulkRequestBuilder.build())
@@ -377,7 +379,7 @@ class OpensearchProducer extends DefaultAsyncProducer {
GetRequest.Builder getRequestBuilder =
ctx.getMessage().getBody(GetRequest.Builder.class);
if (getRequestBuilder == null) {
throw new IllegalArgumentException(
- "Wrong body type. Only String or GetRequest.Builder is
allowed as a type");
+ "Wrong body type. Only String, GetRequest or
GetRequest.Builder is allowed as a type");
}
onComplete(
ctx.getClient().get(getRequestBuilder.build(), documentClass),
@@ -388,6 +390,10 @@ class OpensearchProducer extends DefaultAsyncProducer {
* Updates asynchronously a document.
*/
private void processUpdateAsync(ActionContext ctx, Class<?> documentClass)
throws IOException {
+ if (ctx.getMessage().getBody() == null) {
+ throw new IllegalArgumentException(
+ "The body of the Update operation cannot be null: it is
the update of the document, or its request");
+ }
var updateRequestBuilder =
ctx.getMessage().getBody(UpdateRequest.Builder.class);
onComplete(
ctx.getClient().update(updateRequestBuilder.build(),
documentClass)
@@ -399,6 +405,10 @@ class OpensearchProducer extends DefaultAsyncProducer {
* Indexes asynchronously a document.
*/
private void processIndexAsync(ActionContext ctx) throws IOException {
+ if (ctx.getMessage().getBody() == null) {
+ throw new IllegalArgumentException(
+ "The body of the Index operation cannot be null: it is the
document to index, or its request");
+ }
IndexRequest.Builder<?> indexRequestBuilder =
ctx.getMessage().getBody(IndexRequest.Builder.class);
onComplete(
ctx.getClient().index(indexRequestBuilder.build())
diff --git
a/components/camel-opensearch/src/main/java/org/apache/camel/component/opensearch/converter/OpensearchActionRequestConverter.java
b/components/camel-opensearch/src/main/java/org/apache/camel/component/opensearch/converter/OpensearchActionRequestConverter.java
index bdb6c711ec9c..7f25a453cb75 100644
---
a/components/camel-opensearch/src/main/java/org/apache/camel/component/opensearch/converter/OpensearchActionRequestConverter.java
+++
b/components/camel-opensearch/src/main/java/org/apache/camel/component/opensearch/converter/OpensearchActionRequestConverter.java
@@ -16,10 +16,12 @@
*/
package org.apache.camel.component.opensearch.converter;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
+import java.nio.file.Path;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude;
@@ -28,6 +30,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.apache.camel.Converter;
import org.apache.camel.Exchange;
+import org.apache.camel.WrappedFile;
import org.apache.camel.component.opensearch.OpensearchConstants;
import org.apache.camel.util.ObjectHelper;
import org.opensearch.client.json.JsonData;
@@ -39,6 +42,7 @@ import org.opensearch.client.opensearch.core.DeleteRequest;
import org.opensearch.client.opensearch.core.GetRequest;
import org.opensearch.client.opensearch.core.IndexRequest;
import org.opensearch.client.opensearch.core.MgetRequest;
+import org.opensearch.client.opensearch.core.MsearchRequest;
import org.opensearch.client.opensearch.core.SearchRequest;
import org.opensearch.client.opensearch.core.UpdateRequest;
import org.opensearch.client.opensearch.core.bulk.BulkOperation;
@@ -95,6 +99,11 @@ public final class OpensearchActionRequestConverter {
}
return builder;
}
+ if (document instanceof IndexRequest<?> indexRequest) {
+ // a built request is sent as it is, as a builder body is: the
producer gets a builder body without
+ // converting it, so the header rules of the builder above do not
apply to it either
+ return indexRequest.toBuilder();
+ }
JacksonJsonpMapper mapper = createMapper();
IndexRequest.Builder<Object> builder = new IndexRequest.Builder<>();
if (document instanceof byte[] byteArray) {
@@ -105,6 +114,10 @@ public final class OpensearchActionRequestConverter {
builder.document(JsonData.of(mapper.objectMapper().reader().readTree(new
StringReader(string)), mapper).toJson());
} else if (document instanceof Reader reader) {
builder.document(JsonData.of(mapper.objectMapper().reader().readTree(reader),
mapper).toJson());
+ } else if (isFile(document)) {
+ try (InputStream inputStream = readFile(document, exchange)) {
+
builder.document(JsonData.of(mapper.objectMapper().reader().readTree(inputStream),
mapper).toJson());
+ }
} else {
builder.document(document);
}
@@ -128,6 +141,10 @@ public final class OpensearchActionRequestConverter {
}
return builder;
}
+ if (document instanceof UpdateRequest<?, ?> updateRequest) {
+ // a built request is sent as it is, as a builder body is
+ return updateRequest.toBuilder();
+ }
JacksonJsonpMapper mapper = createMapper();
UpdateRequest.Builder<?, Object> builder = new
UpdateRequest.Builder<>();
if (document instanceof byte[] byteArray) {
@@ -141,6 +158,10 @@ public final class OpensearchActionRequestConverter {
document =
JsonData.of(mapper.objectMapper().reader().readTree(reader),
mapper).to(JsonNode.class);
} else if (document instanceof Map<?, ?> map) {
document = mapper.objectMapper().convertValue(map, JsonNode.class);
+ } else if (isFile(document)) {
+ try (InputStream inputStream = readFile(document, exchange)) {
+ document =
JsonData.of(mapper.objectMapper().reader().readTree(inputStream),
mapper).to(JsonNode.class);
+ }
}
if (document instanceof JsonNode jsonNode) {
@@ -167,6 +188,10 @@ public final class OpensearchActionRequestConverter {
if (document instanceof GetRequest.Builder getReqBuilder) {
return getReqBuilder;
}
+ if (document instanceof GetRequest getRequest) {
+ // a built request is sent as it is, as a builder body is
+ return getRequest.toBuilder();
+ }
if (document instanceof String string) {
return new GetRequest.Builder()
.index(exchange.getIn().getHeader(OpensearchConstants.PARAM_INDEX_NAME,
String.class))
@@ -180,6 +205,10 @@ public final class OpensearchActionRequestConverter {
if (document instanceof DeleteRequest.Builder deleteReqBuilder) {
return deleteReqBuilder;
}
+ if (document instanceof DeleteRequest deleteRequest) {
+ // a built request is sent as it is, as a builder body is
+ return deleteRequest.toBuilder();
+ }
if (document instanceof String string) {
return new DeleteRequest.Builder()
.index(exchange.getIn().getHeader(OpensearchConstants.PARAM_INDEX_NAME,
String.class))
@@ -193,9 +222,22 @@ public final class OpensearchActionRequestConverter {
if (document instanceof DeleteIndexRequest.Builder
deleteIdxReqBuilder) {
return deleteIdxReqBuilder;
}
- if (document instanceof String) {
- return new DeleteIndexRequest.Builder()
-
.index(exchange.getIn().getHeader(OpensearchConstants.PARAM_INDEX_NAME,
String.class));
+ if (document instanceof DeleteIndexRequest deleteIndexRequest) {
+ // a built request is sent as it is, as a builder body is
+ return deleteIndexRequest.toBuilder();
+ }
+ if (document instanceof String string) {
+ // the index of the header or of the indexName option, else the
one named by the body
+ String indexName =
exchange.getIn().getHeader(OpensearchConstants.PARAM_INDEX_NAME, String.class);
+ if (ObjectHelper.isEmpty(indexName)) {
+ indexName = string;
+ }
+ DeleteIndexRequest.Builder builder = new
DeleteIndexRequest.Builder();
+ if (ObjectHelper.isNotEmpty(indexName)) {
+ builder.index(indexName);
+ }
+ // with no name at all, the request fails when it is built, as the
index is required
+ return builder;
}
return null;
}
@@ -205,6 +247,10 @@ public final class OpensearchActionRequestConverter {
if (documents instanceof MgetRequest.Builder mgetReqBuilder) {
return mgetReqBuilder;
}
+ if (documents instanceof MgetRequest mgetRequest) {
+ // a built request is sent as it is, as a builder body is
+ return mgetRequest.toBuilder();
+ }
if (documents instanceof Iterable<?> documentIterable) {
MgetRequest.Builder builder = new MgetRequest.Builder();
builder.index(exchange.getIn().getHeader(OpensearchConstants.PARAM_INDEX_NAME,
String.class));
@@ -233,33 +279,20 @@ public final class OpensearchActionRequestConverter {
}
return searchReqBuilder;
}
- SearchRequest.Builder builder = new SearchRequest.Builder();
-
- // Only set up the indexName if the message header has the
- // setting
+ if (queryObject instanceof SearchRequest searchRequest) {
+ // a built request is sent as it is, as a builder body is
+ return searchRequest.toBuilder();
+ }
Integer size =
exchange.getIn().getHeader(OpensearchConstants.PARAM_SIZE, Integer.class);
Integer from =
exchange.getIn().getHeader(OpensearchConstants.PARAM_FROM, Integer.class);
- if (ObjectHelper.isNotEmpty(indexName)) {
- builder.index(indexName);
- }
+ JacksonJsonpMapper mapper = createMapper();
+ JsonNode json;
if (queryObject instanceof Map<?, ?> mapQuery) {
- // Remove 'query' prefix from the query object for backward
- // compatibility with Elasticsearch
- if (mapQuery.containsKey(OPENSEARCH_QUERY_DSL_PREFIX)) {
- mapQuery = (Map<?, ?>)
mapQuery.get(OPENSEARCH_QUERY_DSL_PREFIX);
- }
- queryObject = mapQuery;
+ json = mapper.objectMapper().valueToTree(mapQuery);
} else if (queryObject instanceof String queryString) {
- JacksonJsonpMapper mapper = createMapper();
- JsonNode jsonTextObject =
mapper.objectMapper().readValue(queryString, JsonNode.class);
- JsonNode parentJsonNode =
jsonTextObject.get(OPENSEARCH_QUERY_DSL_PREFIX);
- if (parentJsonNode != null) {
- queryString = parentJsonNode.toString();
- }
- mapper.objectMapper().reader().readTree(new
StringReader(queryString));
- queryObject =
JsonData.of(mapper.objectMapper().reader().readTree(new
StringReader(queryString)), mapper).toJson();
+ json = mapper.objectMapper().readValue(queryString,
JsonNode.class);
} else {
// Cannot convert the queryObject into SearchRequest
LOG.warn(
@@ -267,6 +300,22 @@ public final class OpensearchActionRequestConverter {
queryObject == null ? "null" :
queryObject.getClass().getName());
return null;
}
+
+ SearchRequest.Builder builder;
+ if (json != null && json.isObject() && json.size() == 1 &&
!json.has(OPENSEARCH_QUERY_DSL_PREFIX)) {
+ // a query alone, such as {"match": {...}}: an object with a
single member, other than query
+ builder = new SearchRequest.Builder().query(JsonData.of(json,
mapper).to(Query.class));
+ } else {
+ // the body of a search request: its query, size, from, sort,
aggregations, _source and the rest
+ builder = JsonData.of(json,
mapper).to(SearchRequest.class).toBuilder();
+ }
+
+ // Only set up the indexName if the message header has the
+ // setting
+ if (ObjectHelper.isNotEmpty(indexName)) {
+ builder.index(indexName);
+ }
+ // the size and from of the headers or of the options, when set,
override those of the body
if (size != null) {
builder.size(size);
}
@@ -274,16 +323,30 @@ public final class OpensearchActionRequestConverter {
builder.from(from);
}
- builder.query(JsonData.of(queryObject,
createMapper()).to(Query.class));
-
return builder;
}
+ @Converter
+ public static MsearchRequest.Builder toMsearchRequestBuilder(Object
queryObject, Exchange exchange) {
+ if (queryObject instanceof MsearchRequest.Builder msearchReqBuilder) {
+ return msearchReqBuilder;
+ }
+ if (queryObject instanceof MsearchRequest msearchRequest) {
+ // a built request is sent as it is, as a builder body is
+ return msearchRequest.toBuilder();
+ }
+ return null;
+ }
+
@Converter
public static BulkRequest.Builder toBulkRequestBuilder(Object documents,
Exchange exchange) throws IOException {
if (documents instanceof BulkRequest.Builder bulkReqBuilder) {
return bulkReqBuilder;
}
+ if (documents instanceof BulkRequest bulkRequest) {
+ // a built request is sent as it is, as a builder body is
+ return bulkRequest.toBuilder();
+ }
if (documents instanceof Iterable<?> documentIterable) {
BulkRequest.Builder builder = new BulkRequest.Builder();
builder.index(exchange.getIn().getHeader(OpensearchConstants.PARAM_INDEX_NAME,
String.class));
@@ -311,6 +374,26 @@ public final class OpensearchActionRequestConverter {
return null;
}
+ /**
+ * Whether the document is a file, a java.io.File, a Path or the file of
the file component, whose content is the
+ * JSON of the document.
+ */
+ private static boolean isFile(Object document) {
+ return document instanceof File || document instanceof Path ||
document instanceof WrappedFile;
+ }
+
+ /**
+ * Opens the content of the file, to read as JSON.
+ */
+ private static InputStream readFile(Object file, Exchange exchange) {
+ InputStream inputStream =
exchange.getContext().getTypeConverter().convertTo(InputStream.class, exchange,
file);
+ if (inputStream == null) {
+ throw new IllegalArgumentException(
+ "Cannot read the file of type " +
file.getClass().getName() + " as the JSON of the document");
+ }
+ return inputStream;
+ }
+
private static JacksonJsonpMapper createMapper() {
ObjectMapper objectMapper = new ObjectMapper()
.configure(SerializationFeature.INDENT_OUTPUT, false)
diff --git
a/components/camel-opensearch/src/test/java/org/apache/camel/component/opensearch/OpensearchProducerBodyTest.java
b/components/camel-opensearch/src/test/java/org/apache/camel/component/opensearch/OpensearchProducerBodyTest.java
new file mode 100644
index 000000000000..899533d77b01
--- /dev/null
+++
b/components/camel-opensearch/src/test/java/org/apache/camel/component/opensearch/OpensearchProducerBodyTest.java
@@ -0,0 +1,338 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.camel.component.opensearch;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.component.file.FileConsumer;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.opensearch.client.opensearch._types.Result;
+import org.opensearch.client.opensearch.core.BulkRequest;
+import org.opensearch.client.opensearch.core.DeleteRequest;
+import org.opensearch.client.opensearch.core.GetRequest;
+import org.opensearch.client.opensearch.core.GetResponse;
+import org.opensearch.client.opensearch.core.IndexRequest;
+import org.opensearch.client.opensearch.core.MgetRequest;
+import org.opensearch.client.opensearch.core.MsearchRequest;
+import org.opensearch.client.opensearch.core.SearchRequest;
+import org.opensearch.client.opensearch.core.UpdateRequest;
+import org.opensearch.client.opensearch.core.search.HitsMetadata;
+import org.opensearch.client.opensearch.indices.DeleteIndexRequest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The request the producer sends for the bodies it takes, checked against a
node that records the requests and answers
+ * them as OpenSearch does.
+ */
+class OpensearchProducerBodyTest {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private static final String SHARDS =
"\"_shards\":{\"total\":1,\"successful\":1,\"failed\":0}";
+ private static final String WRITTEN =
"{\"_index\":\"idx\",\"_id\":\"1\",\"_version\":1,\"result\":\"%s\"," + SHARDS
+ +
",\"_seq_no\":0,\"_primary_term\":1}";
+ private static final String FOUND
+ =
"{\"_index\":\"idx\",\"_id\":\"1\",\"_version\":1,\"_seq_no\":0,\"_primary_term\":1,\"found\":true,"
+ + "\"_source\":{\"name\":\"camel\"}}";
+ private static final String HITS = "{\"took\":1,\"timed_out\":false," +
SHARDS
+ +
",\"hits\":{\"total\":{\"value\":0,\"relation\":\"eq\"},\"hits\":[]}}";
+
+ private final List<Recorded> requests = new CopyOnWriteArrayList<>();
+ private HttpServer server;
+ private CamelContext context;
+ private ProducerTemplate template;
+ private String uri;
+
+ @TempDir
+ Path dir;
+
+ @BeforeEach
+ void start() throws Exception {
+ server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
+ server.createContext("/", this::answer);
+ server.start();
+ uri = "opensearch:test-cluster?hostAddresses=127.0.0.1:" +
server.getAddress().getPort();
+ context = new DefaultCamelContext();
+ context.start();
+ template = context.createProducerTemplate();
+ }
+
+ @AfterEach
+ void stop() {
+ context.stop();
+ server.stop(0);
+ }
+
+ @Test
+ void aBuiltIndexRequestIsSentAsItIs() throws Exception {
+ IndexRequest<Object> request = IndexRequest.of(b ->
b.index("idx").id("1").document(Map.of("name", "camel")));
+
+ assertEquals("1", template.requestBody(uri + "&indexName=other",
request));
+
+ Recorded sent = single();
+ assertEquals("PUT", sent.method());
+ assertEquals("/idx/_doc/1", sent.path());
+ assertEquals(json("{\"name\":\"camel\"}"), json(sent.body()));
+ }
+
+ @Test
+ void aBuiltUpdateRequestIsSentAsItIs() throws Exception {
+ UpdateRequest<Object, Object> request
+ = UpdateRequest.of(b ->
b.index("idx").id("1").doc(Map.of("name", "camel")));
+
+ assertEquals("1", template.requestBody(uri + "&indexName=other",
request));
+
+ Recorded sent = single();
+ assertEquals("/idx/_update/1", sent.path());
+ assertEquals(json("{\"doc\":{\"name\":\"camel\"}}"),
json(sent.body()));
+ }
+
+ @Test
+ void aBuiltGetRequestIsSentAsItIs() {
+ GetResponse<?> response
+ = template.requestBody(uri, GetRequest.of(b ->
b.index("idx").id("1")), GetResponse.class);
+
+ assertTrue(response.found());
+ assertEquals("GET", single().method());
+ assertEquals("/idx/_doc/1", single().path());
+ }
+
+ @Test
+ void aBuiltDeleteRequestIsSentAsItIs() {
+ Object result = template.requestBody(uri, DeleteRequest.of(b ->
b.index("idx").id("1")));
+
+ assertEquals(Result.Deleted, result);
+ assertEquals("DELETE", single().method());
+ assertEquals("/idx/_doc/1", single().path());
+ }
+
+ @Test
+ void aBuiltSearchRequestIsSentAsItIs() throws Exception {
+ SearchRequest request = SearchRequest.of(b ->
b.index("idx").size(3).query(q -> q.matchAll(m -> m)));
+
+ assertInstanceOf(HitsMetadata.class, template.requestBody(uri +
"&indexName=other", request));
+
+ Recorded sent = single();
+ assertEquals("/idx/_search", sent.path());
+ assertEquals(json("{\"query\":{\"match_all\":{}},\"size\":3}"),
json(sent.body()));
+ }
+
+ @Test
+ void aBuiltMultiSearchRequestIsSentAsItIs() {
+ MsearchRequest request = MsearchRequest.of(b -> b.index("idx")
+ .searches(s -> s.header(h -> h).body(q -> q.query(m ->
m.matchAll(a -> a)))));
+
+ List<?> responses = template.requestBody(uri, request, List.class);
+
+ assertEquals(1, responses.size());
+ assertEquals("/idx/_msearch", single().path());
+ assertTrue(single().body().contains("match_all"), single().body());
+ }
+
+ @Test
+ void aBuiltMultiGetRequestIsSentAsItIs() throws Exception {
+ List<?> docs = template.requestBody(uri, MgetRequest.of(b ->
b.index("idx").ids("1")), List.class);
+
+ assertEquals(1, docs.size());
+ assertEquals("/idx/_mget", single().path());
+ assertEquals(json("{\"ids\":[\"1\"]}"), json(single().body()));
+ }
+
+ @Test
+ void aBuiltBulkRequestIsSentAsItIs() throws Exception {
+ BulkRequest request = BulkRequest.of(b -> b.operations(
+ o -> o.index(i ->
i.index("idx").id("1").document(Map.of("name", "camel")))));
+
+ List<?> items = template.requestBody(uri, request, List.class);
+
+ assertEquals(1, items.size());
+ assertEquals("/_bulk", single().path());
+ String[] lines = single().body().split("\n");
+ assertEquals(json("{\"index\":{\"_id\":\"1\",\"_index\":\"idx\"}}"),
json(lines[0]));
+ assertEquals(json("{\"name\":\"camel\"}"), json(lines[1]));
+ }
+
+ @Test
+ void aBuiltDeleteIndexRequestIsSentAsItIs() {
+ Object acknowledged = template.requestBody(uri,
DeleteIndexRequest.of(b -> b.index("idx")));
+
+ assertEquals(Boolean.TRUE, acknowledged);
+ assertEquals("DELETE", single().method());
+ assertEquals("/idx", single().path());
+ }
+
+ @Test
+ void deleteIndexDeletesTheIndexNamedByTheBodyWithoutHeaderOrOption() {
+ Object acknowledged = template.requestBody(uri +
"&operation=DeleteIndex", "idx");
+
+ assertEquals(Boolean.TRUE, acknowledged);
+ assertEquals("/idx", single().path());
+ }
+
+ @Test
+ void deleteIndexPrefersTheIndexNameOption() {
+ Object acknowledged = template.requestBody(uri +
"&operation=DeleteIndex&indexName=idx", "other");
+
+ assertEquals(Boolean.TRUE, acknowledged);
+ assertEquals("/idx", single().path());
+ }
+
+ @Test
+ void indexFailsClearlyWithoutBody() {
+ Exchange exchange = template.request(uri +
"&operation=Index&indexName=idx", e -> e.getMessage().setBody(null));
+
+ IllegalArgumentException e =
assertInstanceOf(IllegalArgumentException.class, exchange.getException());
+ assertEquals("The body of the Index operation cannot be null: it is
the document to index, or its request",
+ e.getMessage());
+ assertTrue(requests.isEmpty(), "a request was sent");
+ }
+
+ @Test
+ void updateFailsClearlyWithoutBody() {
+ Exchange exchange = template.request(uri +
"&operation=Update&indexName=idx", e -> {
+ e.getMessage().setHeader(OpensearchConstants.PARAM_INDEX_ID, "1");
+ e.getMessage().setBody(null);
+ });
+
+ IllegalArgumentException e =
assertInstanceOf(IllegalArgumentException.class, exchange.getException());
+ assertEquals("The body of the Update operation cannot be null: it is
the update of the document, or its request",
+ e.getMessage());
+ assertTrue(requests.isEmpty(), "a request was sent");
+ }
+
+ @Test
+ void indexReadsAFileAsTheJsonOfTheDocument() throws Exception {
+ File file = write("doc.json", "{\"name\":\"camel\"}").toFile();
+
+ assertEquals("1", template.requestBody(uri +
"&operation=Index&indexName=idx", file));
+
+ assertEquals(json("{\"name\":\"camel\"}"), json(single().body()));
+ }
+
+ @Test
+ void indexReadsAPathAsTheJsonOfTheDocument() throws Exception {
+ Path path = write("doc.json", "{\"name\":\"camel\"}");
+
+ assertEquals("1", template.requestBody(uri +
"&operation=Index&indexName=idx", path));
+
+ assertEquals(json("{\"name\":\"camel\"}"), json(single().body()));
+ }
+
+ @Test
+ void indexReadsTheFileOfTheFileComponentAsTheJsonOfTheDocument() throws
Exception {
+ File file = write("doc.json", "{\"name\":\"camel\"}").toFile();
+ Object genericFile = FileConsumer.asGenericFile(dir.toString(), file,
null, false);
+
+ assertEquals("1", template.requestBody(uri +
"&operation=Index&indexName=idx", genericFile));
+
+ assertEquals(json("{\"name\":\"camel\"}"), json(single().body()));
+ }
+
+ @Test
+ void updateReadsAFileAsJson() throws Exception {
+ File file = write("update.json",
"{\"doc\":{\"name\":\"camel\"}}").toFile();
+
+ assertEquals("1", template.requestBodyAndHeader(uri +
"&operation=Update&indexName=idx", file,
+ OpensearchConstants.PARAM_INDEX_ID, "1"));
+
+ assertEquals("/idx/_update/1", single().path());
+ assertEquals(json("{\"doc\":{\"name\":\"camel\"}}"),
json(single().body()));
+ }
+
+ @Test
+ void getByIdStillTakesTheIdAsText() {
+ GetResponse<?> response = template.requestBody(uri +
"&operation=GetById&indexName=idx", "1", GetResponse.class);
+
+ assertTrue(response.found());
+ assertEquals("/idx/_doc/1", single().path());
+ }
+
+ private Path write(String name, String content) throws IOException {
+ return Files.writeString(dir.resolve(name), content);
+ }
+
+ private Recorded single() {
+ assertEquals(1, requests.size(), "requests sent: " + requests);
+ return requests.get(0);
+ }
+
+ private static JsonNode json(String text) throws IOException {
+ return MAPPER.readTree(text);
+ }
+
+ private void answer(HttpExchange exchange) throws IOException {
+ String body = new String(exchange.getRequestBody().readAllBytes(),
StandardCharsets.UTF_8);
+ String method = exchange.getRequestMethod();
+ String path = exchange.getRequestURI().getPath();
+ requests.add(new Recorded(method, path, body));
+ byte[] response = response(method,
path).getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().add("Content-Type", "application/json");
+ exchange.sendResponseHeaders(200, response.length);
+ try (OutputStream out = exchange.getResponseBody()) {
+ out.write(response);
+ }
+ }
+
+ private static String response(String method, String path) {
+ if (path.endsWith("/_search")) {
+ return HITS;
+ } else if (path.endsWith("/_msearch")) {
+ return "{\"took\":1,\"responses\":[" + HITS.replaceFirst("\\}$",
",\"status\":200}") + "]}";
+ } else if (path.endsWith("/_mget")) {
+ return "{\"docs\":[" + FOUND + "]}";
+ } else if (path.endsWith("/_bulk")) {
+ return "{\"took\":1,\"errors\":false,\"items\":[{\"index\":"
+ + String.format(WRITTEN, "created").replaceFirst("\\}$",
",\"status\":201}") + "}]}";
+ } else if (path.contains("/_update/")) {
+ return String.format(WRITTEN, "updated");
+ } else if (path.contains("/_doc")) {
+ return switch (method) {
+ case "GET" -> FOUND;
+ case "DELETE" -> String.format(WRITTEN, "deleted");
+ default -> String.format(WRITTEN, "created");
+ };
+ }
+ // the deletion of an index
+ return "{\"acknowledged\":true}";
+ }
+
+ private record Recorded(String method, String path, String body) {
+ }
+}
diff --git
a/components/camel-opensearch/src/test/java/org/apache/camel/component/opensearch/converter/OpensearchActionRequestConverterTest.java
b/components/camel-opensearch/src/test/java/org/apache/camel/component/opensearch/converter/OpensearchActionRequestConverterTest.java
index 87dede478c7d..223cc6085d8d 100644
---
a/components/camel-opensearch/src/test/java/org/apache/camel/component/opensearch/converter/OpensearchActionRequestConverterTest.java
+++
b/components/camel-opensearch/src/test/java/org/apache/camel/component/opensearch/converter/OpensearchActionRequestConverterTest.java
@@ -16,8 +16,11 @@
*/
package org.apache.camel.component.opensearch.converter;
+import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
+import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.component.opensearch.OpensearchConstants;
@@ -26,10 +29,17 @@ import org.apache.camel.support.DefaultExchange;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.opensearch.client.opensearch._types.query_dsl.Query;
import org.opensearch.client.opensearch.core.IndexRequest;
+import org.opensearch.client.opensearch.core.SearchRequest;
import org.opensearch.client.opensearch.core.UpdateRequest;
+import org.opensearch.client.opensearch.indices.DeleteIndexRequest;
+import org.opensearch.client.util.MissingRequiredPropertyException;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
public class OpensearchActionRequestConverterTest {
@@ -83,4 +93,127 @@ public class OpensearchActionRequestConverterTest {
// no CamelOpensearchIndexId header -> the caller's id must be
preserved, not overwritten with null
assertEquals("original", result.build().id());
}
+
+ @Test
+ void aJsonSearchBodyKeepsItsSizeSortAggregationsAndSource() throws
Exception {
+ Exchange exchange = new DefaultExchange(context);
+ String body = """
+ {"query": {"match": {"name": "camel"}}, "size": 5, "from": 2,
+ "sort": [{"name": "asc"}], "aggs": {"names": {"terms":
{"field": "name"}}},
+ "_source": ["name"], "track_total_hits": true}
+ """;
+
+ SearchRequest request =
OpensearchActionRequestConverter.toSearchRequestBuilder(body, exchange).build();
+
+ assertEquals(Query.Kind.Match, request.query()._kind());
+ assertEquals(5, request.size());
+ assertEquals(2, request.from());
+ assertEquals(1, request.sort().size());
+ assertEquals("name", request.sort().get(0).field().field());
+ assertTrue(request.aggregations().containsKey("names"));
+ assertNotNull(request.source());
+ assertNotNull(request.trackTotalHits());
+ }
+
+ @Test
+ void aJsonSearchBodyWithoutQueryIsASearchRequest() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+
+ SearchRequest request =
OpensearchActionRequestConverter.toSearchRequestBuilder(
+ "{\"size\": 0, \"aggs\": {\"names\": {\"terms\": {\"field\":
\"name\"}}}}", exchange).build();
+
+ assertEquals(0, request.size());
+ assertTrue(request.aggregations().containsKey("names"));
+ }
+
+ @Test
+ void aJsonQueryAloneIsStillTheQuery() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getIn().setHeader(OpensearchConstants.PARAM_INDEX_NAME,
"idx");
+
+ SearchRequest request = OpensearchActionRequestConverter
+ .toSearchRequestBuilder("{\"match\": {\"name\": \"camel\"}}",
exchange).build();
+
+ assertEquals(Query.Kind.Match, request.query()._kind());
+ assertEquals("name", request.query().match().field());
+ assertEquals(List.of("idx"), request.index());
+ }
+
+ @Test
+ void aMapSearchBodyKeepsItsSizeAndSort() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+ Map<String, Object> body = new LinkedHashMap<>();
+ body.put("query", Map.of("match", Map.of("name", "camel")));
+ body.put("size", 5);
+ body.put("sort", List.of(Map.of("name", "asc")));
+
+ SearchRequest request =
OpensearchActionRequestConverter.toSearchRequestBuilder(body, exchange).build();
+
+ assertEquals(Query.Kind.Match, request.query()._kind());
+ assertEquals(5, request.size());
+ assertEquals(1, request.sort().size());
+ }
+
+ @Test
+ void aMapQueryAloneIsStillTheQuery() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+
+ SearchRequest request = OpensearchActionRequestConverter
+ .toSearchRequestBuilder(Map.of("match", Map.of("name",
"camel")), exchange).build();
+
+ assertEquals(Query.Kind.Match, request.query()._kind());
+ }
+
+ @Test
+ void aMapSearchBodyWhoseQueryIsNotAMap() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+ Map<String, Object> body = Map.of("query", new
ObjectMapper().readTree("{\"match_all\": {}}"));
+
+ SearchRequest request =
OpensearchActionRequestConverter.toSearchRequestBuilder(body, exchange).build();
+
+ assertEquals(Query.Kind.MatchAll, request.query()._kind());
+ }
+
+ @Test
+ void theSizeAndFromHeadersOverrideThoseOfTheSearchBody() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getIn().setHeader(OpensearchConstants.PARAM_SIZE, 10);
+ exchange.getIn().setHeader(OpensearchConstants.PARAM_FROM, 20);
+
+ SearchRequest request =
OpensearchActionRequestConverter.toSearchRequestBuilder(
+ "{\"query\": {\"match_all\": {}}, \"size\": 5, \"from\": 2}",
exchange).build();
+
+ assertEquals(10, request.size());
+ assertEquals(20, request.from());
+ }
+
+ @Test
+ void deleteIndexTakesTheIndexNameOfTheBodyWithoutHeader() {
+ Exchange exchange = new DefaultExchange(context);
+
+ DeleteIndexRequest request
+ =
OpensearchActionRequestConverter.toDeleteIndexRequestBuilder("twitter",
exchange).build();
+
+ assertEquals(List.of("twitter"), request.index());
+ }
+
+ @Test
+ void deleteIndexPrefersTheIndexNameHeader() {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getIn().setHeader(OpensearchConstants.PARAM_INDEX_NAME,
"logs");
+
+ DeleteIndexRequest request
+ =
OpensearchActionRequestConverter.toDeleteIndexRequestBuilder("twitter",
exchange).build();
+
+ assertEquals(List.of("logs"), request.index());
+ }
+
+ @Test
+ void deleteIndexWithoutAnyIndexNameFailsBeforeSending() {
+ Exchange exchange = new DefaultExchange(context);
+
+ DeleteIndexRequest.Builder builder =
OpensearchActionRequestConverter.toDeleteIndexRequestBuilder(" ", exchange);
+
+ assertThrows(MissingRequiredPropertyException.class, builder::build);
+ }
}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 95a84ce3a204..488977773fba 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -4829,6 +4829,25 @@ success and stored nothing.
The catalog now says that `deleteByQuery` defaults to `false`, as it always
did at runtime. A `null` value in a `Map`
body or in a `CamelSolrField.*` header is left out of the document, instead of
failing with a `NullPointerException`.
+=== camel-opensearch - built requests, search bodies, DeleteIndex and file
bodies
+
+A request of the OpenSearch client in the message body (an `IndexRequest`,
`UpdateRequest`, `GetRequest`,
+`DeleteRequest`, `SearchRequest`, `MsearchRequest`, `MgetRequest`,
`BulkRequest` or `DeleteIndexRequest`) is now sent
+as it is, like its builder. Before, a built `IndexRequest` was written as the
document itself, with a generated id, a
+built `UpdateRequest` failed without the id header, and the other built
requests failed with "Wrong body type".
+
+The `Search` operation now uses the whole search request of a JSON text or
`Map` body: its `size`, `from`, `sort`,
+aggregations, `_source` and the other members are no longer dropped, so the
hits returned can change. A JSON object
+with a single member other than `query`, such as `{"match": {...}}`, is still
taken as the query alone. The `size` and
+`from` headers or options, when set, still override those of the body.
+
+The `DeleteIndex` operation now deletes the index named by a `String` body
when neither the
+`CamelOpensearchIndexName` header nor the `indexName` option is set. Before,
the call failed.
+
+The `Index` and `Update` operations now fail with an
`IllegalArgumentException` when the body is null, instead of
+failing with a Jackson error. A `java.io.File`, a `java.nio.file.Path` or the
file of the file
+component is now read as the JSON of the document, instead of being written as
a JSON string of its path.
+
=== camel-ssh - the shell channel and sleepForShellPrompt
An `ssh` endpoint with `channelType=shell` and no `shellPrompt` now fails when
it starts. Previously each command