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 d61386813417 CAMEL-25361: camel-couchbase - use the view value as the
body and document the fullDocument default (#27588)
d61386813417 is described below
commit d61386813417f5917ea4bae1b3282fe583273a1f
Author: allthingssecurity <[email protected]>
AuthorDate: Sat Oct 10 01:20:50 2026 +0530
CAMEL-25361: camel-couchbase - use the view value as the body and document
the fullDocument default (#27588)
With useView=true and fullDocument=false the consumer body was the Optional
returned by the Couchbase SDK 3 instead of the view value. The body is now
the
value, or null when the view emitted none. The fullDocument option is now
documented with its actual default (true). The SQL++ path is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../apache/camel/catalog/components/couchbase.json | 2 +-
.../camel/component/couchbase/couchbase.json | 2 +-
.../component/couchbase/CouchbaseConsumer.java | 3 +-
.../component/couchbase/CouchbaseEndpoint.java | 2 +-
.../couchbase/CouchbaseConsumerBodyTest.java | 174 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 13 ++
.../dsl/CouchbaseEndpointBuilderFactory.java | 4 +-
7 files changed, 194 insertions(+), 6 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/couchbase.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/couchbase.json
index 3f962e5a755d..e3153aa368a1 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/couchbase.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/couchbase.json
@@ -50,7 +50,7 @@
"consumerRetryPause": { "index": 8, "kind": "parameter", "displayName":
"Consumer Retry Pause", "group": "consumer", "label": "consumer", "required":
false, "type": "integer", "javaType": "int", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": 5000, "description": "Define the
consumer retry pause between different attempts" },
"descending": { "index": 9, "kind": "parameter", "displayName":
"Descending", "group": "consumer", "label": "consumer", "required": false,
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": false, "description": "Define if this
operation is descending or not" },
"designDocumentName": { "index": 10, "kind": "parameter", "displayName":
"Design Document Name", "group": "consumer", "label": "consumer", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": true,
"autowired": false, "secret": false, "defaultValue": "beer", "description":
"The design document name to use. Deprecated: use the statement option with SQL
queries instead." },
- "fullDocument": { "index": 11, "kind": "parameter", "displayName": "Full
Document", "group": "consumer", "label": "consumer", "required": false, "type":
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": false, "description": "If true consumer will
return complete document instead data defined in view" },
+ "fullDocument": { "index": 11, "kind": "parameter", "displayName": "Full
Document", "group": "consumer", "label": "consumer", "required": false, "type":
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": true, "description": "If true consumer will
return complete document instead data defined in view" },
"limit": { "index": 12, "kind": "parameter", "displayName": "Limit",
"group": "consumer", "label": "consumer", "required": false, "type": "integer",
"javaType": "int", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": -1, "description": "The output limit to use" },
"rangeEndKey": { "index": 13, "kind": "parameter", "displayName": "Range
End Key", "group": "consumer", "label": "consumer", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": false, "description": "Define a range for the end key" },
"rangeStartKey": { "index": 14, "kind": "parameter", "displayName": "Range
Start Key", "group": "consumer", "label": "consumer", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "description": "Define a range for the
start key" },
diff --git
a/components/camel-couchbase/src/generated/resources/META-INF/org/apache/camel/component/couchbase/couchbase.json
b/components/camel-couchbase/src/generated/resources/META-INF/org/apache/camel/component/couchbase/couchbase.json
index 3f962e5a755d..e3153aa368a1 100644
---
a/components/camel-couchbase/src/generated/resources/META-INF/org/apache/camel/component/couchbase/couchbase.json
+++
b/components/camel-couchbase/src/generated/resources/META-INF/org/apache/camel/component/couchbase/couchbase.json
@@ -50,7 +50,7 @@
"consumerRetryPause": { "index": 8, "kind": "parameter", "displayName":
"Consumer Retry Pause", "group": "consumer", "label": "consumer", "required":
false, "type": "integer", "javaType": "int", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": 5000, "description": "Define the
consumer retry pause between different attempts" },
"descending": { "index": 9, "kind": "parameter", "displayName":
"Descending", "group": "consumer", "label": "consumer", "required": false,
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": false, "description": "Define if this
operation is descending or not" },
"designDocumentName": { "index": 10, "kind": "parameter", "displayName":
"Design Document Name", "group": "consumer", "label": "consumer", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": true,
"autowired": false, "secret": false, "defaultValue": "beer", "description":
"The design document name to use. Deprecated: use the statement option with SQL
queries instead." },
- "fullDocument": { "index": 11, "kind": "parameter", "displayName": "Full
Document", "group": "consumer", "label": "consumer", "required": false, "type":
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": false, "description": "If true consumer will
return complete document instead data defined in view" },
+ "fullDocument": { "index": 11, "kind": "parameter", "displayName": "Full
Document", "group": "consumer", "label": "consumer", "required": false, "type":
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": true, "description": "If true consumer will
return complete document instead data defined in view" },
"limit": { "index": 12, "kind": "parameter", "displayName": "Limit",
"group": "consumer", "label": "consumer", "required": false, "type": "integer",
"javaType": "int", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": -1, "description": "The output limit to use" },
"rangeEndKey": { "index": 13, "kind": "parameter", "displayName": "Range
End Key", "group": "consumer", "label": "consumer", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": false, "description": "Define a range for the end key" },
"rangeStartKey": { "index": 14, "kind": "parameter", "displayName": "Range
Start Key", "group": "consumer", "label": "consumer", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "description": "Define a range for the
start key" },
diff --git
a/components/camel-couchbase/src/main/java/org/apache/camel/component/couchbase/CouchbaseConsumer.java
b/components/camel-couchbase/src/main/java/org/apache/camel/component/couchbase/CouchbaseConsumer.java
index 897268a3b57f..09fba1970d87 100644
---
a/components/camel-couchbase/src/main/java/org/apache/camel/component/couchbase/CouchbaseConsumer.java
+++
b/components/camel-couchbase/src/main/java/org/apache/camel/component/couchbase/CouchbaseConsumer.java
@@ -253,7 +253,8 @@ public class CouchbaseConsumer extends
ScheduledBatchPollingConsumer implements
doc = CouchbaseCollectionOperation.getDocument(collection, id,
endpoint.getQueryTimeout(),
endpoint.getConsumerRetryPause());
} else {
- doc = row.valueAs(Object.class);
+ // the SDK returns the value as an Optional, which is empty
when the view emitted no value (null)
+ doc = row.valueAs(Object.class).orElse(null);
}
// Use String.class instead of the shaded JsonNode class to avoid
conflicts
diff --git
a/components/camel-couchbase/src/main/java/org/apache/camel/component/couchbase/CouchbaseEndpoint.java
b/components/camel-couchbase/src/main/java/org/apache/camel/component/couchbase/CouchbaseEndpoint.java
index 5ca1a7f9b9e3..d83117e80425 100644
---
a/components/camel-couchbase/src/main/java/org/apache/camel/component/couchbase/CouchbaseEndpoint.java
+++
b/components/camel-couchbase/src/main/java/org/apache/camel/component/couchbase/CouchbaseEndpoint.java
@@ -149,7 +149,7 @@ public class CouchbaseEndpoint extends
ScheduledPollEndpoint implements Endpoint
private String rangeStartKey;
@UriParam(label = "consumer")
private String rangeEndKey = "";
- @UriParam(label = "consumer", defaultValue = "false")
+ @UriParam(label = "consumer", defaultValue = "true")
private boolean fullDocument = true;
@UriParam(label = "consumer", defaultValue = "5000")
private int consumerRetryPause = DEFAULT_PAUSE_BETWEEN_RETRIES;
diff --git
a/components/camel-couchbase/src/test/java/org/apache/camel/component/couchbase/CouchbaseConsumerBodyTest.java
b/components/camel-couchbase/src/test/java/org/apache/camel/component/couchbase/CouchbaseConsumerBodyTest.java
new file mode 100644
index 000000000000..b92371521b61
--- /dev/null
+++
b/components/camel-couchbase/src/test/java/org/apache/camel/component/couchbase/CouchbaseConsumerBodyTest.java
@@ -0,0 +1,174 @@
+/*
+ * 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.couchbase;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import com.couchbase.client.java.Bucket;
+import com.couchbase.client.java.Cluster;
+import com.couchbase.client.java.ClusterOptions;
+import com.couchbase.client.java.Collection;
+import com.couchbase.client.java.Scope;
+import com.couchbase.client.java.json.JsonObject;
+import com.couchbase.client.java.kv.GetOptions;
+import com.couchbase.client.java.kv.GetResult;
+import com.couchbase.client.java.query.QueryOptions;
+import com.couchbase.client.java.query.QueryResult;
+import com.couchbase.client.java.view.ViewOptions;
+import com.couchbase.client.java.view.ViewResult;
+import com.couchbase.client.java.view.ViewRow;
+import org.apache.camel.builder.RouteBuilder;
+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.mockito.MockedStatic;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+/**
+ * The message body the consumer creates for a row, with and without {@code
fullDocument}.
+ * <p/>
+ * The scheduler of the consumer is not started; the test calls {@code poll()}
itself against a mocked cluster.
+ */
+class CouchbaseConsumerBodyTest {
+
+ private static final String URI =
"couchbase:http://localhost:8091?bucket=bucket&username=user&password=secret"
+ + "&startScheduler=false";
+ private static final String VIEW =
"&useView=true&designDocumentName=dd&viewName=view";
+
+ private DefaultCamelContext context;
+ private MockedStatic<Cluster> clusters;
+ private Bucket bucket;
+ private Scope scope;
+ private Collection collection;
+ private final List<Object> bodies = new ArrayList<>();
+
+ @BeforeEach
+ void setUp() {
+ bucket = mock(Bucket.class);
+ scope = mock(Scope.class);
+ collection = mock(Collection.class);
+ when(bucket.defaultScope()).thenReturn(scope);
+ when(bucket.defaultCollection()).thenReturn(collection);
+ Cluster cluster = mock(Cluster.class);
+ when(cluster.bucket(anyString())).thenReturn(bucket);
+ clusters = mockStatic(Cluster.class);
+ clusters.when(() -> Cluster.connect(anyString(),
any(ClusterOptions.class))).thenReturn(cluster);
+ context = new DefaultCamelContext();
+ }
+
+ @AfterEach
+ void tearDown() {
+ context.stop();
+ clusters.close();
+ }
+
+ private CouchbaseConsumer startRoute(String uri) throws Exception {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from(uri).routeId("couchbase").process(exchange ->
bodies.add(exchange.getMessage().getBody()));
+ }
+ });
+ context.start();
+ return (CouchbaseConsumer) context.getRoute("couchbase").getConsumer();
+ }
+
+ @SuppressWarnings("deprecation")
+ private void viewReturns(ViewRow... rows) {
+ ViewResult result = mock(ViewResult.class);
+ when(result.rows()).thenReturn(List.of(rows));
+ when(bucket.viewQuery(anyString(), anyString(),
any(ViewOptions.class))).thenReturn(result);
+ }
+
+ private void queryReturns(JsonObject... rows) {
+ QueryResult result = mock(QueryResult.class);
+ when(result.rowsAsObject()).thenReturn(List.of(rows));
+ when(scope.query(anyString(),
any(QueryOptions.class))).thenReturn(result);
+ }
+
+ /** A view row as the SDK returns it: the value is an {@code Optional},
empty when the view emitted null. */
+ private static ViewRow viewRow(String id, Object value) {
+ ViewRow row = mock(ViewRow.class);
+ when(row.id()).thenReturn(Optional.of(id));
+ // pins the valueAs(Class) and keyAs(Class) overloads that
CouchbaseConsumer calls: if the consumer moves to
+ // another overload such as valueAs(TypeRef), stub that one here too,
as an unstubbed call returns null and the
+ // tests would fail with a NullPointerException instead of a clear
assertion
+ when(row.valueAs(Object.class)).thenReturn(Optional.ofNullable(value));
+ when(row.keyAs(String.class)).thenReturn(Optional.of(id));
+ return row;
+ }
+
+ @Test
+ void theValueEmittedByTheViewIsTheBodyWithoutFullDocument() throws
Exception {
+ Map<String, Object> value = Map.of("name", "Alice");
+ viewReturns(viewRow("doc-1", value), viewRow("doc-2", "Bob"));
+ CouchbaseConsumer consumer = startRoute(URI + VIEW +
"&fullDocument=false");
+
+ assertEquals(2, consumer.poll());
+
+ assertEquals(List.of(value, "Bob"), bodies, "the body must be the
value of the view row, not an Optional");
+ }
+
+ @Test
+ void theBodyIsNullWhenTheViewEmitsNoValue() throws Exception {
+ viewReturns(viewRow("doc-1", null));
+ CouchbaseConsumer consumer = startRoute(URI + VIEW +
"&fullDocument=false");
+
+ assertEquals(1, consumer.poll());
+
+ assertEquals(1, bodies.size());
+ assertNull(bodies.get(0), "the body must be null when the view emitted
null, not an empty Optional");
+ }
+
+ @Test
+ void theQueryRowIsTheBodyWithoutFullDocument() throws Exception {
+ JsonObject row =
JsonObject.create().put(CouchbaseConsumer.SQL_DOCUMENT_ID_ALIAS,
"doc-1").put("name", "Alice");
+ queryReturns(row);
+ CouchbaseConsumer consumer = startRoute(URI + "&fullDocument=false");
+
+ assertEquals(1, consumer.poll());
+
+ assertEquals(List.of(row.toString()), bodies, "the body must be the
SQL++ row as a JSON string");
+ }
+
+ @Test
+ void theFullDocumentIsTheBodyByDefault() throws Exception {
+
queryReturns(JsonObject.create().put(CouchbaseConsumer.SQL_DOCUMENT_ID_ALIAS,
"doc-1").put("name", "Alice"));
+ GetResult document = mock(GetResult.class);
+ when(collection.get(eq("doc-1"),
any(GetOptions.class))).thenReturn(document);
+ // fullDocument is not set: it defaults to true, so the consumer
fetches the document by its ID
+ CouchbaseConsumer consumer = startRoute(URI);
+
+ assertEquals(1, consumer.poll());
+
+ assertEquals(1, bodies.size());
+ assertSame(document, bodies.get(0), "the body must be the document
fetched by the ID of the row");
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index adfdf36b2044..6cb1f2aad4a6 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -679,6 +679,12 @@ route's error handler, which logs the exhausted failure,
and `BridgeExceptionHan
falls back to its logging handler for such an exchange rather than bridging it
a second time. What changes is that
the consumer itself no longer stays silent.
+==== useView=true with fullDocument=false
+
+With `useView=true` and `fullDocument=false` the message body is now the value
the view emitted for the row, or `null`
+when the view emitted no value. It used to be a `java.util.Optional` holding
that value. A route that unwrapped the
+`Optional` itself must now use the body directly.
+
==== consumerProcessedStrategy=delete
With `consumerProcessedStrategy=delete` the consumer now removes a document
once its exchange has completed
@@ -700,6 +706,13 @@ count towards `maxMessagesPerPoll`. This only applies to
the same consumer: anot
for example on another node, can still read the document before it is removed.
With the other strategies the document
stays in the bucket, so later polls read it again as before.
+A view or SQL++ query that returns several rows for one document (for example
a view that emits one row per element of
+an array in the document) now delivers only the first of these rows with
`consumerProcessedStrategy=delete`: the
+document is in progress from its first row on, so its other rows are skipped,
and once that exchange has completed the
+document is removed. Previously the first row removed the document while the
poll was built, and the next row of the
+same document failed to read or remove it, as it no longer existed, so the
whole poll failed. With the other
+strategies every row is delivered as before.
+
=== camel-smpp - received exchanges now come from the consumer
Exchanges for received `deliver_sm` and `data_sm` messages are now created
through
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/CouchbaseEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/CouchbaseEndpointBuilderFactory.java
index 1daf1548b64a..9b5601c73bee 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/CouchbaseEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/CouchbaseEndpointBuilderFactory.java
@@ -199,7 +199,7 @@ public interface CouchbaseEndpointBuilderFactory {
*
* The option is a: <code>boolean</code> type.
*
- * Default: false
+ * Default: true
* Group: consumer
*
* @param fullDocument the value to set
@@ -215,7 +215,7 @@ public interface CouchbaseEndpointBuilderFactory {
*
* The option will be converted to a <code>boolean</code> type.
*
- * Default: false
+ * Default: true
* Group: consumer
*
* @param fullDocument the value to set