This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/camel-4.18.x by this push:
     new aeb7c2151add camel-google fixes backport to camel-4.18.x (CAMEL-24344, 
CAMEL-24345)
aeb7c2151add is described below

commit aeb7c2151add8ff35e81998882ee881a9edcd7ca
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Aug 27 16:46:19 2026 +0200

    camel-google fixes backport to camel-4.18.x (CAMEL-24344, CAMEL-24345)
    
    Backport to camel-4.18.x of two camel-google audit fixes that never
    reached this branch (both already on main and camel-4.22.x, predating
    the 4.22.0 cut):
    
    - CAMEL-24344: camel-google-mail's stream consumer always requested the
      FULL Gmail message format, so raw=true produced a null body, and the
      non-raw path only read the first MIME part, losing non-multipart and
      nested-multipart bodies. Cherry-pick of e8dc6b8 (#25357).
    - CAMEL-24345: camel-google-vertexai's jsonMode option was declared but
      never applied to the request MIME type, and buildRawPredictRequestBody
      threw an NPE with no body set. Hand-ported (partial) from a2ab8280
      (#25362) — the streamOutputMode half is left out since
      generateChatStreaming is not implemented on this branch.
    
    Closes #25814
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 components/camel-google/camel-google-mail/pom.xml  |   5 +
 .../mail/stream/GoogleMailStreamConsumer.java      |  57 +++++++--
 .../stream/GoogleMailStreamConsumerBodyTest.java   | 128 +++++++++++++++++++++
 .../camel-google/camel-google-vertexai/pom.xml     |   5 +
 .../google/vertexai/GoogleVertexAIOperations.java  |   6 +-
 .../google/vertexai/GoogleVertexAIProducer.java    |  10 +-
 .../GoogleVertexAIProducerOptionsTest.java         |  66 +++++++++++
 7 files changed, 265 insertions(+), 12 deletions(-)

diff --git a/components/camel-google/camel-google-mail/pom.xml 
b/components/camel-google/camel-google-mail/pom.xml
index 01357022ac68..125e5ec8533f 100644
--- a/components/camel-google/camel-google-mail/pom.xml
+++ b/components/camel-google/camel-google-mail/pom.xml
@@ -150,6 +150,11 @@
             <artifactId>commons-codec</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
 
     </dependencies>
 
diff --git 
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamConsumer.java
 
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamConsumer.java
index aba62302a233..1708e1042cd5 100644
--- 
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamConsumer.java
+++ 
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamConsumer.java
@@ -92,7 +92,8 @@ public class GoogleMailStreamConsumer extends 
ScheduledBatchPollingConsumer {
 
         if (c.getMessages() != null) {
             for (Message message : c.getMessages()) {
-                Message mess = getClient().users().messages().get("me", 
message.getId()).setFormat("FULL").execute();
+                Message mess
+                        = getClient().users().messages().get("me", 
message.getId()).setFormat(messageFormat()).execute();
                 Exchange exchange = 
createExchange(getEndpoint().getExchangePattern(), mess);
                 answer.add(exchange);
             }
@@ -167,8 +168,12 @@ public class GoogleMailStreamConsumer extends 
ScheduledBatchPollingConsumer {
      * Strategy when processing the exchange failed.
      */
     protected void processRollback(Exchange exchange, String unreadLabelId) {
+        if (!getConfiguration().isMarkAsRead()) {
+            // the mail was never marked as read, so there is nothing to roll 
back
+            return;
+        }
         try {
-            LOG.warn("Exchange failed, so rolling back mail {} to un {}", 
exchange, unreadLabelId);
+            LOG.warn("Exchange failed, so marking mail {} as unread again", 
exchange);
 
             List<String> add = new ArrayList<>();
             add.add(unreadLabelId);
@@ -176,7 +181,8 @@ public class GoogleMailStreamConsumer extends 
ScheduledBatchPollingConsumer {
             getClient().users().messages()
                     .modify("me", 
exchange.getIn().getHeader(GoogleMailStreamConstants.MAIL_ID, String.class), 
mods).execute();
         } catch (Exception e) {
-            getExceptionHandler().handleException("Error occurred mark as read 
mail. This exception is ignored.", exchange, e);
+            getExceptionHandler().handleException("Error occurred marking the 
mail as unread. This exception is ignored.",
+                    exchange, e);
         }
     }
 
@@ -188,17 +194,52 @@ public class GoogleMailStreamConsumer extends 
ScheduledBatchPollingConsumer {
         if (getConfiguration().isRaw()) {
             message.setBody(mail.getRaw());
         } else {
-            List<MessagePart> parts = mail.getPayload().getParts();
-            if (parts != null && parts.get(0).getBody().getData() != null) {
-                byte[] bodyBytes = 
Base64.decodeBase64(parts.get(0).getBody().getData().trim());
-                String body = new String(bodyBytes, StandardCharsets.UTF_8);
+            String body = extractBody(mail.getPayload());
+            if (body != null) {
                 message.setBody(body);
             }
         }
-        configureHeaders(message, mail.getPayload().getHeaders());
+        if (mail.getPayload() != null && mail.getPayload().getHeaders() != 
null) {
+            configureHeaders(message, mail.getPayload().getHeaders());
+        }
         return exchange;
     }
 
+    /**
+     * The message format the consumer has to ask for. The raw field of a 
message is only populated when the RAW format
+     * is requested, the payload only when the FULL format is.
+     */
+    String messageFormat() {
+        return getConfiguration().isRaw() ? "RAW" : "FULL";
+    }
+
+    /**
+     * Returns the decoded content of the first part carrying data, walking 
into nested multiparts. A message that is
+     * not multipart at all keeps its content directly on the payload.
+     */
+    private String extractBody(MessagePart part) {
+        if (part == null) {
+            return null;
+        }
+
+        if (part.getBody() != null && part.getBody().getData() != null) {
+            byte[] bodyBytes = 
Base64.decodeBase64(part.getBody().getData().trim());
+            return new String(bodyBytes, StandardCharsets.UTF_8);
+        }
+
+        List<MessagePart> parts = part.getParts();
+        if (parts != null) {
+            for (MessagePart child : parts) {
+                String body = extractBody(child);
+                if (body != null) {
+                    return body;
+                }
+            }
+        }
+
+        return null;
+    }
+
     private void configureHeaders(org.apache.camel.Message message, 
List<MessagePartHeader> headers) {
         for (MessagePartHeader header : headers) {
             String headerName = header.getName();
diff --git 
a/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamConsumerBodyTest.java
 
b/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamConsumerBodyTest.java
new file mode 100644
index 000000000000..7b960e0752f3
--- /dev/null
+++ 
b/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamConsumerBodyTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.google.mail.stream;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import com.google.api.client.util.Base64;
+import com.google.api.services.gmail.model.Message;
+import com.google.api.services.gmail.model.MessagePart;
+import com.google.api.services.gmail.model.MessagePartBody;
+import com.google.api.services.gmail.model.MessagePartHeader;
+import org.apache.camel.Exchange;
+import org.apache.camel.ExchangePattern;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+/**
+ * Verifies how the stream consumer turns a Gmail message into an exchange: 
which format it asks the API for, and where
+ * it picks the body up from.
+ */
+class GoogleMailStreamConsumerBodyTest {
+
+    private DefaultCamelContext context;
+
+    @AfterEach
+    void tearDown() {
+        if (context != null) {
+            context.stop();
+        }
+    }
+
+    private GoogleMailStreamConsumer consumer(boolean raw) throws Exception {
+        if (context != null) {
+            context.stop();
+        }
+        context = new DefaultCamelContext();
+        context.start();
+        GoogleMailStreamEndpoint endpoint = context.getEndpoint(
+                
"google-mail-stream://index?clientId=id&clientSecret=secret&raw=" + raw,
+                GoogleMailStreamEndpoint.class);
+        return new GoogleMailStreamConsumer(endpoint, exchange -> {
+        }, "UNREAD", List.of());
+    }
+
+    private static MessagePartBody body(String content) {
+        return new 
MessagePartBody().setData(Base64.encodeBase64URLSafeString(content.getBytes(StandardCharsets.UTF_8)));
+    }
+
+    @Test
+    void theRawOptionAsksForTheRawFormat() throws Exception {
+        // the raw field of a message is only returned for the RAW format, 
asking for FULL always left it null
+        assertThat(consumer(true).messageFormat()).isEqualTo("RAW");
+        assertThat(consumer(false).messageFormat()).isEqualTo("FULL");
+    }
+
+    @Test
+    void aNonMultipartMessageKeepsItsBody() throws Exception {
+        Message mail = new Message().setId("1").setThreadId("t1")
+                .setPayload(new 
MessagePart().setMimeType("text/plain").setBody(body("plain content")));
+
+        Exchange exchange = 
consumer(false).createExchange(ExchangePattern.InOnly, mail);
+
+        assertThat(exchange.getIn().getBody()).isEqualTo("plain content");
+    }
+
+    @Test
+    void aMultipartMessageUsesTheFirstPartCarryingData() throws Exception {
+        Message mail = new Message().setId("2").setPayload(new 
MessagePart().setMimeType("multipart/alternative")
+                .setParts(List.of(
+                        new MessagePart().setMimeType("multipart/mixed")
+                                .setParts(List.of(new 
MessagePart().setMimeType("text/plain").setBody(body("nested")))),
+                        new 
MessagePart().setMimeType("text/html").setBody(body("<p>html</p>")))));
+
+        Exchange exchange = 
consumer(false).createExchange(ExchangePattern.InOnly, mail);
+
+        assertThat(exchange.getIn().getBody()).isEqualTo("nested");
+    }
+
+    @Test
+    void aMessageWithoutPayloadIsNotAFailure() throws Exception {
+        Message mail = new Message().setId("3");
+
+        Exchange exchange = 
consumer(false).createExchange(ExchangePattern.InOnly, mail);
+
+        assertThat(exchange.getIn().getBody()).isNull();
+        
assertThat(exchange.getIn().getHeader(GoogleMailStreamConstants.MAIL_ID)).isEqualTo("3");
+    }
+
+    @Test
+    void headersAreMappedWhenPresent() throws Exception {
+        Message mail = new Message().setId("4").setPayload(new MessagePart()
+                .setBody(body("content"))
+                .setHeaders(List.of(
+                        new MessagePartHeader().setName("Subject").setValue("a 
subject"),
+                        new 
MessagePartHeader().setName("From").setValue("[email protected]"))));
+
+        Exchange exchange = 
consumer(false).createExchange(ExchangePattern.InOnly, mail);
+
+        
assertThat(exchange.getIn().getHeader(GoogleMailStreamConstants.MAIL_SUBJECT)).isEqualTo("a
 subject");
+        
assertThat(exchange.getIn().getHeader(GoogleMailStreamConstants.MAIL_FROM)).isEqualTo("[email protected]");
+    }
+
+    @Test
+    void aPayloadWithoutHeadersIsNotAFailure() throws Exception {
+        Message mail = new Message().setId("5").setPayload(new 
MessagePart().setBody(body("content")));
+
+        assertThatCode(() -> 
consumer(false).createExchange(ExchangePattern.InOnly, 
mail)).doesNotThrowAnyException();
+    }
+}
diff --git a/components/camel-google/camel-google-vertexai/pom.xml 
b/components/camel-google/camel-google-vertexai/pom.xml
index f8379e5eadca..444a07421533 100644
--- a/components/camel-google/camel-google-vertexai/pom.xml
+++ b/components/camel-google/camel-google-vertexai/pom.xml
@@ -99,5 +99,10 @@
             <artifactId>camel-test-junit5</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git 
a/components/camel-google/camel-google-vertexai/src/main/java/org/apache/camel/component/google/vertexai/GoogleVertexAIOperations.java
 
b/components/camel-google/camel-google-vertexai/src/main/java/org/apache/camel/component/google/vertexai/GoogleVertexAIOperations.java
index 3644c852e615..429d3d791d20 100644
--- 
a/components/camel-google/camel-google-vertexai/src/main/java/org/apache/camel/component/google/vertexai/GoogleVertexAIOperations.java
+++ 
b/components/camel-google/camel-google-vertexai/src/main/java/org/apache/camel/component/google/vertexai/GoogleVertexAIOperations.java
@@ -30,7 +30,8 @@ public enum GoogleVertexAIOperations {
     generateText,
 
     /**
-     * Generate chat response using Gemini models with conversation history.
+     * Generate a chat response using Gemini models. Alias of {@link 
#generateText}: the request is built from the same
+     * prompt and configuration, the operation name only documents the intent 
of the route.
      */
     generateChat,
 
@@ -50,7 +51,8 @@ public enum GoogleVertexAIOperations {
     generateEmbeddings,
 
     /**
-     * Generate code using Gemini or code-specialized models.
+     * Generate code using Gemini or code-specialized models. Alias of {@link 
#generateText}: the model is selected with
+     * the modelId option, the operation name only documents the intent of the 
route.
      */
     generateCode,
 
diff --git 
a/components/camel-google/camel-google-vertexai/src/main/java/org/apache/camel/component/google/vertexai/GoogleVertexAIProducer.java
 
b/components/camel-google/camel-google-vertexai/src/main/java/org/apache/camel/component/google/vertexai/GoogleVertexAIProducer.java
index 3bfb59ae94bc..9c60012d6adf 100644
--- 
a/components/camel-google/camel-google-vertexai/src/main/java/org/apache/camel/component/google/vertexai/GoogleVertexAIProducer.java
+++ 
b/components/camel-google/camel-google-vertexai/src/main/java/org/apache/camel/component/google/vertexai/GoogleVertexAIProducer.java
@@ -38,6 +38,7 @@ public class GoogleVertexAIProducer extends DefaultProducer {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(GoogleVertexAIProducer.class);
     private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+    private static final String JSON_MIME_TYPE = "application/json";
 
     private final GoogleVertexAIEndpoint endpoint;
 
@@ -260,7 +261,8 @@ public class GoogleVertexAIProducer extends DefaultProducer 
{
         }
 
         throw new IllegalArgumentException(
-                "Request body must be a JSON String, Map, or plain text 
prompt. Got: " + body.getClass().getName());
+                "Request body must be a JSON String, Map, or plain text 
prompt. Got: "
+                                           + (body == null ? "no body" : 
body.getClass().getName()));
     }
 
     /**
@@ -355,7 +357,7 @@ public class GoogleVertexAIProducer extends DefaultProducer 
{
         return prompt;
     }
 
-    private GenerateContentConfig buildConfig(Exchange exchange) {
+    GenerateContentConfig buildConfig(Exchange exchange) {
         GoogleVertexAIConfiguration config = endpoint.getConfiguration();
 
         GenerateContentConfig.Builder configBuilder = 
GenerateContentConfig.builder();
@@ -401,6 +403,10 @@ public class GoogleVertexAIProducer extends 
DefaultProducer {
             configBuilder.candidateCount(candidateCount);
         }
 
+        if (config.isJsonMode()) {
+            configBuilder.responseMimeType(JSON_MIME_TYPE);
+        }
+
         return configBuilder.build();
     }
 
diff --git 
a/components/camel-google/camel-google-vertexai/src/test/java/org/apache/camel/component/google/vertexai/GoogleVertexAIProducerOptionsTest.java
 
b/components/camel-google/camel-google-vertexai/src/test/java/org/apache/camel/component/google/vertexai/GoogleVertexAIProducerOptionsTest.java
new file mode 100644
index 000000000000..536728db7b8b
--- /dev/null
+++ 
b/components/camel-google/camel-google-vertexai/src/test/java/org/apache/camel/component/google/vertexai/GoogleVertexAIProducerOptionsTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.google.vertexai;
+
+import com.google.genai.types.GenerateContentConfig;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Verifies that the producer applies the options that describe the shape of 
the request and of the response. Uses
+ * direct object construction to avoid starting the endpoint (which requires 
Google Cloud credentials).
+ */
+class GoogleVertexAIProducerOptionsTest {
+
+    private DefaultCamelContext context;
+
+    @AfterEach
+    void tearDown() {
+        if (context != null) {
+            context.stop();
+        }
+    }
+
+    private GoogleVertexAIProducer producer(GoogleVertexAIConfiguration 
config) {
+        context = new DefaultCamelContext();
+        GoogleVertexAIComponent component = new 
GoogleVertexAIComponent(context);
+        GoogleVertexAIEndpoint endpoint
+                = new 
GoogleVertexAIEndpoint("google-vertexai:my-project:us-central1:gemini-2.0-flash",
 component, config);
+        return new GoogleVertexAIProducer(endpoint);
+    }
+
+    @Test
+    void jsonModeAsksTheModelForJson() {
+        GoogleVertexAIConfiguration config = new GoogleVertexAIConfiguration();
+        config.setJsonMode(true);
+        GenerateContentConfig result = producer(config).buildConfig(new 
DefaultExchange(context));
+
+        assertThat(result.responseMimeType()).contains("application/json");
+    }
+
+    @Test
+    void jsonModeIsOffByDefault() {
+        GoogleVertexAIConfiguration config = new GoogleVertexAIConfiguration();
+        GenerateContentConfig result = producer(config).buildConfig(new 
DefaultExchange(context));
+
+        assertThat(result.responseMimeType()).isEmpty();
+    }
+}

Reply via email to