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 81d06027f606 CAMEL-26249: camel-google-text-to-speech - 
pojoRequest=true runs the operation, and listVoices lists all voices unless 
languageCode is set (#28961)
81d06027f606 is described below

commit 81d06027f6069d9706afd6679e1b3b561474c5d1
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Nov 2 22:56:13 2026 +0100

    CAMEL-26249: camel-google-text-to-speech - pojoRequest=true runs the 
operation, and listVoices lists all voices unless languageCode is set (#28961)
    
    * CAMEL-26249: camel-google-text-to-speech - pojoRequest=true runs the 
operation, and listVoices lists all voices unless languageCode is set
    
    * CAMEL-26249: camel-google-text-to-speech - upgrade guide: with 
pojoRequest=true the endpoint path must name the operation
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../catalog/components/google-text-to-speech.json  |   6 +-
 .../docs/google-text-to-speech-component.adoc      |  32 +++-
 .../google/texttospeech/google-text-to-speech.json |   6 +-
 .../main/docs/google-text-to-speech-component.adoc |  32 +++-
 .../GoogleCloudTextToSpeechConfiguration.java      |  13 +-
 .../GoogleCloudTextToSpeechEndpoint.java           |  16 +-
 .../GoogleCloudTextToSpeechProducer.java           |  48 +++--
 .../unit/GoogleCloudTextToSpeechProducerTest.java  | 210 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  16 ++
 ...gleCloudTextToSpeechEndpointBuilderFactory.java |   4 +-
 10 files changed, 344 insertions(+), 39 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/google-text-to-speech.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/google-text-to-speech.json
index 6b7c7c2d758d..77722aa67476 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/google-text-to-speech.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/google-text-to-speech.json
@@ -32,14 +32,14 @@
     "CamelGoogleCloudTextToSpeechResponseObject": { "index": 1, "kind": 
"header", "displayName": "", "group": "producer", "label": "", "required": 
false, "javaType": "com.google.cloud.texttospeech.v1.SynthesizeSpeechResponse", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The response object resulting from the Google Cloud 
Text-to-Speech API invocation", "constantName": 
"org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpe [...]
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "any" ], "byOption": [ { "option": 
"operation", "value": "synthesize", "carriers": [ "text", "bytes", "stream", 
"file" ] }, { "option": "operation", "value": "listVoices", "carriers": [ "any" 
] }, { "option": "pojoRequest", "value": "true", "carriers": [ "pojo" ], 
"javaType": "com.google.cloud.texttospeech.v1.SynthesizeSpeechRequest" } ], 
"description": "The text to speak for synthesize: bytes, a stream or a file are 
read as text, a Map or a POJO is spok [...]
-    "producerOut": { "carriers": [ "bytes", "list" ], "byOption": [ { 
"option": "operation", "value": "synthesize", "carriers": [ "bytes" ], 
"javaType": "byte[]" }, { "option": "operation", "value": "listVoices", 
"carriers": [ "list" ], "javaType": 
"java.util.List<com.google.cloud.texttospeech.v1.Voice>" }, { "option": 
"pojoRequest", "value": "true", "carriers": [ "bytes" ], "javaType": "byte[]" } 
], "description": "The audio of synthesize as bytes, in the audioEncoding 
option (MP3 by de [...]
+    "producerIn": { "carriers": [ "any" ], "byOption": [ { "option": 
"operation", "value": "synthesize", "carriers": [ "text", "bytes", "stream", 
"file" ] }, { "option": "operation", "value": "listVoices", "carriers": [ "any" 
] }, { "option": "pojoRequest", "value": "true", "carriers": [ "pojo" ] } ], 
"description": "The text to speak for synthesize: bytes, a stream or a file are 
read as text, a Map or a POJO is spoken as its toString(), so take the text out 
of it first, and no body fail [...]
+    "producerOut": { "carriers": [ "bytes", "list" ], "byOption": [ { 
"option": "operation", "value": "synthesize", "carriers": [ "bytes" ], 
"javaType": "byte[]" }, { "option": "operation", "value": "listVoices", 
"carriers": [ "list" ], "javaType": 
"java.util.List<com.google.cloud.texttospeech.v1.Voice>" } ], "description": 
"The audio of synthesize as bytes, in the audioEncoding option (MP3 by 
default), with the SynthesizeSpeechResponse in the 
CamelGoogleCloudTextToSpeechResponseObject h [...]
   },
   "properties": {
     "operation": { "index": 0, "kind": "path", "displayName": "Operation", 
"group": "common", "label": "common", "required": true, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpeechConfiguration",
 "configurationField": "configuration", "description": "The operation name" },
     "serviceAccountKey": { "index": 1, "kind": "parameter", "displayName": 
"Service Account Key", "group": "common", "label": "common", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpeechConfiguration",
 "configurationField": "configuration", "description": "Service account key to 
authenticate an application as a service a [...]
     "audioEncoding": { "index": 2, "kind": "parameter", "displayName": "Audio 
Encoding", "group": "producer", "label": "producer", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": "MP3", "configurationClass": 
"org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpeechConfiguration",
 "configurationField": "configuration", "description": "The audio encoding of 
the output. Supported val [...]
-    "languageCode": { "index": 3, "kind": "parameter", "displayName": 
"Language Code", "group": "producer", "label": "producer", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": "en-US", 
"configurationClass": 
"org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpeechConfiguration",
 "configurationField": "configuration", "description": "The language code for 
the voice (e.g., en-US, f [...]
+    "languageCode": { "index": 3, "kind": "parameter", "displayName": 
"Language Code", "group": "producer", "label": "producer", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": "en-US", 
"configurationClass": 
"org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpeechConfiguration",
 "configurationField": "configuration", "description": "The language code for 
the voice (e.g., en-US, f [...]
     "pitch": { "index": 4, "kind": "parameter", "displayName": "Pitch", 
"group": "producer", "label": "producer", "required": false, "type": "number", 
"javaType": "java.lang.Double", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpeechConfiguration",
 "configurationField": "configuration", "description": "The pitch, from -20.0 
to 20.0 semitones. Default is 0.0" },
     "pojoRequest": { "index": 5, "kind": "parameter", "displayName": "Pojo 
Request", "group": "producer", "label": "", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpeechConfiguration",
 "configurationField": "configuration", "description": "Specifies if the 
request is a pojo request" },
     "speakingRate": { "index": 6, "kind": "parameter", "displayName": 
"Speaking Rate", "group": "producer", "label": "producer", "required": false, 
"type": "number", "javaType": "java.lang.Double", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpeechConfiguration",
 "configurationField": "configuration", "description": "The speaking rate, from 
0.25 to 4.0. Default is 1.0" },
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-text-to-speech-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-text-to-speech-component.adoc
index f7bbfc8957c6..d66caa0ebd86 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-text-to-speech-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-text-to-speech-component.adoc
@@ -116,7 +116,9 @@ For the `synthesize` operation, the message body should 
contain the text to synt
 
 For the `listVoices` operation, the message body is ignored.
 
-When `pojoRequest=true`, the body should be a 
`com.google.cloud.texttospeech.v1.SynthesizeSpeechRequest` instance instead.
+When `pojoRequest=true`, the body should be the request of the Google Cloud 
SDK for the operation instead: a
+`com.google.cloud.texttospeech.v1.SynthesizeSpeechRequest` for `synthesize`, 
and a
+`com.google.cloud.texttospeech.v1.ListVoicesRequest` for `listVoices`. Another 
body fails with an `InvalidPayloadException`.
 
 === Google Cloud Text-to-Speech Producer operations
 
@@ -126,7 +128,8 @@ Google Cloud Text-to-Speech component provides the 
following operations on the p
 - listVoices
 
 The operation is specified as part of the endpoint URI (e.g., 
`google-text-to-speech://synthesize`).
-You can override the operation at runtime by setting the 
`GoogleCloudTextToSpeechOperation` message header.
+You can override the operation at runtime by setting the 
`CamelGoogleCloudTextToSpeechOperation` message header,
+also when `pojoRequest=true`.
 
 === Advanced component configuration
 
@@ -328,7 +331,9 @@ YAML::
 ----
 ====
 
-This operation will return a `List<Voice>` with the available voices for the 
specified language.
+This operation will return a `List<Voice>` with the available voices for the 
language in the `languageCode` option.
+Without the `languageCode` option, it returns the voices of all languages: the 
default value of the option (`en-US`)
+applies to `synthesize` only.
 
 - Using POJO request for full control:
 
@@ -362,6 +367,27 @@ from("direct:start")
 
 When using `pojoRequest=true`, the body should be a `SynthesizeSpeechRequest` 
and the audio bytes are returned as body.
 
+- Using POJO request to list voices:
+
+._Java-only: Google Cloud SDK ListVoicesRequest builder_
+
+[source,java]
+----
+from("direct:start")
+    .process(exchange -> {
+        ListVoicesRequest request = ListVoicesRequest.newBuilder()
+            .setLanguageCode("fr-FR")
+            .build();
+        exchange.getIn().setBody(request);
+    })
+    
.to("google-text-to-speech://listVoices?serviceAccountKey=/home/user/Downloads/my-key.json&pojoRequest=true")
+    .log("body:${body}")
+    .to("mock:result");
+----
+
+When using `pojoRequest=true` with `listVoices`, the body should be a 
`ListVoicesRequest` and the `List<Voice>` is returned as body.
+The `languageCode` option is not used then: the request says which voices to 
list.
+
 - Overriding the operation at runtime via header:
 
 [tabs]
diff --git 
a/components/camel-google/camel-google-text-to-speech/src/generated/resources/META-INF/org/apache/camel/component/google/texttospeech/google-text-to-speech.json
 
b/components/camel-google/camel-google-text-to-speech/src/generated/resources/META-INF/org/apache/camel/component/google/texttospeech/google-text-to-speech.json
index 6b7c7c2d758d..77722aa67476 100644
--- 
a/components/camel-google/camel-google-text-to-speech/src/generated/resources/META-INF/org/apache/camel/component/google/texttospeech/google-text-to-speech.json
+++ 
b/components/camel-google/camel-google-text-to-speech/src/generated/resources/META-INF/org/apache/camel/component/google/texttospeech/google-text-to-speech.json
@@ -32,14 +32,14 @@
     "CamelGoogleCloudTextToSpeechResponseObject": { "index": 1, "kind": 
"header", "displayName": "", "group": "producer", "label": "", "required": 
false, "javaType": "com.google.cloud.texttospeech.v1.SynthesizeSpeechResponse", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The response object resulting from the Google Cloud 
Text-to-Speech API invocation", "constantName": 
"org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpe [...]
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "any" ], "byOption": [ { "option": 
"operation", "value": "synthesize", "carriers": [ "text", "bytes", "stream", 
"file" ] }, { "option": "operation", "value": "listVoices", "carriers": [ "any" 
] }, { "option": "pojoRequest", "value": "true", "carriers": [ "pojo" ], 
"javaType": "com.google.cloud.texttospeech.v1.SynthesizeSpeechRequest" } ], 
"description": "The text to speak for synthesize: bytes, a stream or a file are 
read as text, a Map or a POJO is spok [...]
-    "producerOut": { "carriers": [ "bytes", "list" ], "byOption": [ { 
"option": "operation", "value": "synthesize", "carriers": [ "bytes" ], 
"javaType": "byte[]" }, { "option": "operation", "value": "listVoices", 
"carriers": [ "list" ], "javaType": 
"java.util.List<com.google.cloud.texttospeech.v1.Voice>" }, { "option": 
"pojoRequest", "value": "true", "carriers": [ "bytes" ], "javaType": "byte[]" } 
], "description": "The audio of synthesize as bytes, in the audioEncoding 
option (MP3 by de [...]
+    "producerIn": { "carriers": [ "any" ], "byOption": [ { "option": 
"operation", "value": "synthesize", "carriers": [ "text", "bytes", "stream", 
"file" ] }, { "option": "operation", "value": "listVoices", "carriers": [ "any" 
] }, { "option": "pojoRequest", "value": "true", "carriers": [ "pojo" ] } ], 
"description": "The text to speak for synthesize: bytes, a stream or a file are 
read as text, a Map or a POJO is spoken as its toString(), so take the text out 
of it first, and no body fail [...]
+    "producerOut": { "carriers": [ "bytes", "list" ], "byOption": [ { 
"option": "operation", "value": "synthesize", "carriers": [ "bytes" ], 
"javaType": "byte[]" }, { "option": "operation", "value": "listVoices", 
"carriers": [ "list" ], "javaType": 
"java.util.List<com.google.cloud.texttospeech.v1.Voice>" } ], "description": 
"The audio of synthesize as bytes, in the audioEncoding option (MP3 by 
default), with the SynthesizeSpeechResponse in the 
CamelGoogleCloudTextToSpeechResponseObject h [...]
   },
   "properties": {
     "operation": { "index": 0, "kind": "path", "displayName": "Operation", 
"group": "common", "label": "common", "required": true, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpeechConfiguration",
 "configurationField": "configuration", "description": "The operation name" },
     "serviceAccountKey": { "index": 1, "kind": "parameter", "displayName": 
"Service Account Key", "group": "common", "label": "common", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpeechConfiguration",
 "configurationField": "configuration", "description": "Service account key to 
authenticate an application as a service a [...]
     "audioEncoding": { "index": 2, "kind": "parameter", "displayName": "Audio 
Encoding", "group": "producer", "label": "producer", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": "MP3", "configurationClass": 
"org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpeechConfiguration",
 "configurationField": "configuration", "description": "The audio encoding of 
the output. Supported val [...]
-    "languageCode": { "index": 3, "kind": "parameter", "displayName": 
"Language Code", "group": "producer", "label": "producer", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": "en-US", 
"configurationClass": 
"org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpeechConfiguration",
 "configurationField": "configuration", "description": "The language code for 
the voice (e.g., en-US, f [...]
+    "languageCode": { "index": 3, "kind": "parameter", "displayName": 
"Language Code", "group": "producer", "label": "producer", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": "en-US", 
"configurationClass": 
"org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpeechConfiguration",
 "configurationField": "configuration", "description": "The language code for 
the voice (e.g., en-US, f [...]
     "pitch": { "index": 4, "kind": "parameter", "displayName": "Pitch", 
"group": "producer", "label": "producer", "required": false, "type": "number", 
"javaType": "java.lang.Double", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpeechConfiguration",
 "configurationField": "configuration", "description": "The pitch, from -20.0 
to 20.0 semitones. Default is 0.0" },
     "pojoRequest": { "index": 5, "kind": "parameter", "displayName": "Pojo 
Request", "group": "producer", "label": "", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpeechConfiguration",
 "configurationField": "configuration", "description": "Specifies if the 
request is a pojo request" },
     "speakingRate": { "index": 6, "kind": "parameter", "displayName": 
"Speaking Rate", "group": "producer", "label": "producer", "required": false, 
"type": "number", "javaType": "java.lang.Double", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpeechConfiguration",
 "configurationField": "configuration", "description": "The speaking rate, from 
0.25 to 4.0. Default is 1.0" },
diff --git 
a/components/camel-google/camel-google-text-to-speech/src/main/docs/google-text-to-speech-component.adoc
 
b/components/camel-google/camel-google-text-to-speech/src/main/docs/google-text-to-speech-component.adoc
index f7bbfc8957c6..d66caa0ebd86 100644
--- 
a/components/camel-google/camel-google-text-to-speech/src/main/docs/google-text-to-speech-component.adoc
+++ 
b/components/camel-google/camel-google-text-to-speech/src/main/docs/google-text-to-speech-component.adoc
@@ -116,7 +116,9 @@ For the `synthesize` operation, the message body should 
contain the text to synt
 
 For the `listVoices` operation, the message body is ignored.
 
-When `pojoRequest=true`, the body should be a 
`com.google.cloud.texttospeech.v1.SynthesizeSpeechRequest` instance instead.
+When `pojoRequest=true`, the body should be the request of the Google Cloud 
SDK for the operation instead: a
+`com.google.cloud.texttospeech.v1.SynthesizeSpeechRequest` for `synthesize`, 
and a
+`com.google.cloud.texttospeech.v1.ListVoicesRequest` for `listVoices`. Another 
body fails with an `InvalidPayloadException`.
 
 === Google Cloud Text-to-Speech Producer operations
 
@@ -126,7 +128,8 @@ Google Cloud Text-to-Speech component provides the 
following operations on the p
 - listVoices
 
 The operation is specified as part of the endpoint URI (e.g., 
`google-text-to-speech://synthesize`).
-You can override the operation at runtime by setting the 
`GoogleCloudTextToSpeechOperation` message header.
+You can override the operation at runtime by setting the 
`CamelGoogleCloudTextToSpeechOperation` message header,
+also when `pojoRequest=true`.
 
 === Advanced component configuration
 
@@ -328,7 +331,9 @@ YAML::
 ----
 ====
 
-This operation will return a `List<Voice>` with the available voices for the 
specified language.
+This operation will return a `List<Voice>` with the available voices for the 
language in the `languageCode` option.
+Without the `languageCode` option, it returns the voices of all languages: the 
default value of the option (`en-US`)
+applies to `synthesize` only.
 
 - Using POJO request for full control:
 
@@ -362,6 +367,27 @@ from("direct:start")
 
 When using `pojoRequest=true`, the body should be a `SynthesizeSpeechRequest` 
and the audio bytes are returned as body.
 
+- Using POJO request to list voices:
+
+._Java-only: Google Cloud SDK ListVoicesRequest builder_
+
+[source,java]
+----
+from("direct:start")
+    .process(exchange -> {
+        ListVoicesRequest request = ListVoicesRequest.newBuilder()
+            .setLanguageCode("fr-FR")
+            .build();
+        exchange.getIn().setBody(request);
+    })
+    
.to("google-text-to-speech://listVoices?serviceAccountKey=/home/user/Downloads/my-key.json&pojoRequest=true")
+    .log("body:${body}")
+    .to("mock:result");
+----
+
+When using `pojoRequest=true` with `listVoices`, the body should be a 
`ListVoicesRequest` and the `List<Voice>` is returned as body.
+The `languageCode` option is not used then: the request says which voices to 
list.
+
 - Overriding the operation at runtime via header:
 
 [tabs]
diff --git 
a/components/camel-google/camel-google-text-to-speech/src/main/java/org/apache/camel/component/google/texttospeech/GoogleCloudTextToSpeechConfiguration.java
 
b/components/camel-google/camel-google-text-to-speech/src/main/java/org/apache/camel/component/google/texttospeech/GoogleCloudTextToSpeechConfiguration.java
index c7da0713cdb0..b29c6a9ff906 100644
--- 
a/components/camel-google/camel-google-text-to-speech/src/main/java/org/apache/camel/component/google/texttospeech/GoogleCloudTextToSpeechConfiguration.java
+++ 
b/components/camel-google/camel-google-text-to-speech/src/main/java/org/apache/camel/component/google/texttospeech/GoogleCloudTextToSpeechConfiguration.java
@@ -35,8 +35,11 @@ public class GoogleCloudTextToSpeechConfiguration implements 
Cloneable, GoogleCo
     private String serviceAccountKey;
 
     @UriParam(label = "producer", defaultValue = "en-US",
-              description = "The language code for the voice (e.g., en-US, 
fr-FR)")
+              description = "The language code for the voice (e.g., en-US, 
fr-FR). The default applies to synthesize"
+                            + " only: listVoices lists the voices of all 
languages unless this option is set")
     private String languageCode = "en-US";
+    // whether languageCode was set: listVoices filters by the language only 
then, not by the default
+    private boolean languageCodeSet;
 
     @UriParam(label = "producer",
               description = "The name of the voice. If not set, the service 
selects a default voice for the specified language")
@@ -99,6 +102,14 @@ public class GoogleCloudTextToSpeechConfiguration 
implements Cloneable, GoogleCo
      */
     public void setLanguageCode(String languageCode) {
         this.languageCode = languageCode;
+        this.languageCodeSet = true;
+    }
+
+    /**
+     * Whether the language code was set, as opposed to holding the default 
value.
+     */
+    boolean isLanguageCodeSet() {
+        return languageCodeSet;
     }
 
     public String getVoiceName() {
diff --git 
a/components/camel-google/camel-google-text-to-speech/src/main/java/org/apache/camel/component/google/texttospeech/GoogleCloudTextToSpeechEndpoint.java
 
b/components/camel-google/camel-google-text-to-speech/src/main/java/org/apache/camel/component/google/texttospeech/GoogleCloudTextToSpeechEndpoint.java
index d97c942a3b12..baf2fc249b16 100644
--- 
a/components/camel-google/camel-google-text-to-speech/src/main/java/org/apache/camel/component/google/texttospeech/GoogleCloudTextToSpeechEndpoint.java
+++ 
b/components/camel-google/camel-google-text-to-speech/src/main/java/org/apache/camel/component/google/texttospeech/GoogleCloudTextToSpeechEndpoint.java
@@ -43,25 +43,25 @@ import org.apache.camel.util.ObjectHelper;
                                                          carriers = {
                                                                  Carrier.TEXT, 
Carrier.BYTES, Carrier.STREAM, Carrier.FILE }),
                                           @BodyType.When(option = "operation", 
value = "listVoices", carriers = Carrier.ANY),
-                                          @BodyType.When(option = 
"pojoRequest", value = "true", carriers = Carrier.POJO,
-                                                         javaType = 
"com.google.cloud.texttospeech.v1.SynthesizeSpeechRequest") },
+                                          @BodyType.When(option = 
"pojoRequest", value = "true", carriers = Carrier.POJO) },
                                   description = "The text to speak for 
synthesize: bytes, a stream or a file are read as"
                                                 + " text, a Map or a POJO is 
spoken as its toString(), so take the text out of"
                                                 + " it first, and no body 
fails. listVoices does not use the body. With"
-                                                + " pojoRequest=true a 
SynthesizeSpeechRequest of the Google Cloud SDK, whatever"
-                                                + " the operation: another 
body fails"),
+                                                + " pojoRequest=true the 
request of the Google Cloud SDK for the operation: a"
+                                                + " SynthesizeSpeechRequest 
for synthesize, a ListVoicesRequest for listVoices;"
+                                                + " another body fails"),
            producerOut = @BodyType(value = { Carrier.BYTES, Carrier.LIST },
                                    byOption = {
                                            @BodyType.When(option = 
"operation", value = "synthesize", carriers = Carrier.BYTES,
                                                           javaType = "byte[]"),
                                            @BodyType.When(option = 
"operation", value = "listVoices", carriers = Carrier.LIST,
-                                                          javaType = 
"java.util.List<com.google.cloud.texttospeech.v1.Voice>"),
-                                           @BodyType.When(option = 
"pojoRequest", value = "true", carriers = Carrier.BYTES,
-                                                          javaType = "byte[]") 
},
+                                                          javaType = 
"java.util.List<com.google.cloud.texttospeech.v1.Voice>") },
                                    description = "The audio of synthesize as 
bytes, in the audioEncoding option (MP3 by"
                                                  + " default), with the 
SynthesizeSpeechResponse in the"
                                                  + " 
CamelGoogleCloudTextToSpeechResponseObject header. The List of the voices"
-                                                 + " for listVoices, of the 
languageCode option (en-US by default)"))
+                                                 + " for listVoices: of the 
language in the languageCode option when it is"
+                                                 + " set, of all languages 
when it is not, and with pojoRequest=true of the"
+                                                 + " ListVoicesRequest"))
 public class GoogleCloudTextToSpeechEndpoint extends DefaultEndpoint 
implements EndpointServiceLocation {
 
     @UriParam
diff --git 
a/components/camel-google/camel-google-text-to-speech/src/main/java/org/apache/camel/component/google/texttospeech/GoogleCloudTextToSpeechProducer.java
 
b/components/camel-google/camel-google-text-to-speech/src/main/java/org/apache/camel/component/google/texttospeech/GoogleCloudTextToSpeechProducer.java
index 3ccb7322786b..fa5d747cea06 100644
--- 
a/components/camel-google/camel-google-text-to-speech/src/main/java/org/apache/camel/component/google/texttospeech/GoogleCloudTextToSpeechProducer.java
+++ 
b/components/camel-google/camel-google-text-to-speech/src/main/java/org/apache/camel/component/google/texttospeech/GoogleCloudTextToSpeechProducer.java
@@ -29,6 +29,7 @@ import org.apache.camel.Exchange;
 import org.apache.camel.InvalidPayloadException;
 import org.apache.camel.Message;
 import org.apache.camel.support.DefaultProducer;
+import org.apache.camel.util.ObjectHelper;
 
 public class GoogleCloudTextToSpeechProducer extends DefaultProducer {
 
@@ -41,24 +42,28 @@ public class GoogleCloudTextToSpeechProducer extends 
DefaultProducer {
 
     @Override
     public void process(final Exchange exchange) throws Exception {
-        if (getConfiguration().isPojoRequest()) {
-            processPojo(exchange);
-        } else {
-            GoogleCloudTextToSpeechOperations operation = 
determineOperation(exchange);
-            switch (operation) {
-                case synthesize:
+        GoogleCloudTextToSpeechOperations operation = 
determineOperation(exchange);
+        switch (operation) {
+            case synthesize:
+                if (getConfiguration().isPojoRequest()) {
+                    processSynthesizePojo(exchange);
+                } else {
                     processSynthesize(exchange);
-                    break;
-                case listVoices:
+                }
+                break;
+            case listVoices:
+                if (getConfiguration().isPojoRequest()) {
+                    processListVoicesPojo(exchange);
+                } else {
                     processListVoices(exchange);
-                    break;
-                default:
-                    throw new IllegalArgumentException("Unsupported operation: 
" + operation);
-            }
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("Unsupported operation: " + 
operation);
         }
     }
 
-    private void processPojo(Exchange exchange) throws InvalidPayloadException 
{
+    private void processSynthesizePojo(Exchange exchange) throws 
InvalidPayloadException {
         TextToSpeechClient client = endpoint.getClient();
         SynthesizeSpeechRequest request = 
exchange.getIn().getMandatoryBody(SynthesizeSpeechRequest.class);
 
@@ -103,15 +108,24 @@ public class GoogleCloudTextToSpeechProducer extends 
DefaultProducer {
     }
 
     private void processListVoices(Exchange exchange) {
-        TextToSpeechClient client = endpoint.getClient();
-
         ListVoicesRequest.Builder requestBuilder = 
ListVoicesRequest.newBuilder();
 
-        if (getConfiguration().getLanguageCode() != null) {
+        // only a language code that was set filters the voices: the default 
(en-US) is for synthesize
+        if (getConfiguration().isLanguageCodeSet() && 
ObjectHelper.isNotEmpty(getConfiguration().getLanguageCode())) {
             
requestBuilder.setLanguageCode(getConfiguration().getLanguageCode());
         }
 
-        ListVoicesResponse response = 
client.listVoices(requestBuilder.build());
+        listVoices(exchange, requestBuilder.build());
+    }
+
+    private void processListVoicesPojo(Exchange exchange) throws 
InvalidPayloadException {
+        listVoices(exchange, 
exchange.getIn().getMandatoryBody(ListVoicesRequest.class));
+    }
+
+    private void listVoices(Exchange exchange, ListVoicesRequest request) {
+        TextToSpeechClient client = endpoint.getClient();
+
+        ListVoicesResponse response = client.listVoices(request);
 
         Message message = exchange.getMessage();
         message.setBody(response.getVoicesList());
diff --git 
a/components/camel-google/camel-google-text-to-speech/src/test/java/org/apache/camel/component/google/texttospeech/unit/GoogleCloudTextToSpeechProducerTest.java
 
b/components/camel-google/camel-google-text-to-speech/src/test/java/org/apache/camel/component/google/texttospeech/unit/GoogleCloudTextToSpeechProducerTest.java
new file mode 100644
index 000000000000..2cbdf846475c
--- /dev/null
+++ 
b/components/camel-google/camel-google-text-to-speech/src/test/java/org/apache/camel/component/google/texttospeech/unit/GoogleCloudTextToSpeechProducerTest.java
@@ -0,0 +1,210 @@
+/*
+ * 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.texttospeech.unit;
+
+import java.util.List;
+
+import com.google.cloud.texttospeech.v1.AudioConfig;
+import com.google.cloud.texttospeech.v1.ListVoicesRequest;
+import com.google.cloud.texttospeech.v1.ListVoicesResponse;
+import com.google.cloud.texttospeech.v1.SynthesisInput;
+import com.google.cloud.texttospeech.v1.SynthesizeSpeechRequest;
+import com.google.cloud.texttospeech.v1.SynthesizeSpeechResponse;
+import com.google.cloud.texttospeech.v1.TextToSpeechClient;
+import com.google.cloud.texttospeech.v1.Voice;
+import com.google.cloud.texttospeech.v1.VoiceSelectionParams;
+import com.google.protobuf.ByteString;
+import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
+import 
org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpeechComponent;
+import 
org.apache.camel.component.google.texttospeech.GoogleCloudTextToSpeechConstants;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Verifies the requests the producer sends to a mocked {@link 
TextToSpeechClient}.
+ */
+class GoogleCloudTextToSpeechProducerTest extends CamelTestSupport {
+
+    private static final Voice VOICE = 
Voice.newBuilder().setName("fr-FR-Wavenet-A").addLanguageCodes("fr-FR").build();
+    private static final byte[] AUDIO = { 1, 2, (byte) 0xff };
+
+    private final TextToSpeechClient client = mock(TextToSpeechClient.class);
+
+    @Override
+    protected void bindToRegistry(Registry registry) {
+        registry.bind("ttsClient", client);
+    }
+
+    @BeforeEach
+    void stubClient() {
+        SynthesizeSpeechResponse response
+                = 
SynthesizeSpeechResponse.newBuilder().setAudioContent(ByteString.copyFrom(AUDIO)).build();
+        when(client.listVoices(any(ListVoicesRequest.class)))
+                
.thenReturn(ListVoicesResponse.newBuilder().addVoices(VOICE).build());
+        
when(client.synthesizeSpeech(any(SynthesizeSpeechRequest.class))).thenReturn(response);
+        when(client.synthesizeSpeech(any(SynthesisInput.class), 
any(VoiceSelectionParams.class), any(AudioConfig.class)))
+                .thenReturn(response);
+    }
+
+    @Test
+    void listVoicesWithPojoRequestSendsTheListVoicesRequestOfTheBody() {
+        ListVoicesRequest request = 
ListVoicesRequest.newBuilder().setLanguageCode("fr-FR").build();
+
+        Exchange exchange = 
template.request("google-text-to-speech:listVoices?client=#ttsClient&pojoRequest=true",
+                e -> e.getIn().setBody(request));
+
+        assertNull(exchange.getException());
+        assertEquals(List.of(VOICE), 
exchange.getMessage().getBody(List.class));
+        verify(client).listVoices(request);
+        verify(client, 
never()).synthesizeSpeech(any(SynthesizeSpeechRequest.class));
+    }
+
+    @Test
+    void listVoicesWithPojoRequestDoesNotSynthesizeASynthesizeSpeechRequest() {
+        SynthesizeSpeechRequest request = SynthesizeSpeechRequest.newBuilder()
+                
.setInput(SynthesisInput.newBuilder().setText("Hello")).build();
+
+        Exchange exchange = 
template.request("google-text-to-speech:listVoices?client=#ttsClient&pojoRequest=true",
+                e -> e.getIn().setBody(request));
+
+        assertInstanceOf(InvalidPayloadException.class, 
exchange.getException());
+        verify(client, 
never()).synthesizeSpeech(any(SynthesizeSpeechRequest.class));
+        verify(client, never()).listVoices(any(ListVoicesRequest.class));
+    }
+
+    @Test
+    void listVoicesWithPojoRequestAndNoBodyFails() {
+        Exchange exchange = 
template.request("google-text-to-speech:listVoices?client=#ttsClient&pojoRequest=true",
+                e -> e.getIn().setBody(null));
+
+        assertInstanceOf(InvalidPayloadException.class, 
exchange.getException());
+        verify(client, never()).listVoices(any(ListVoicesRequest.class));
+    }
+
+    @Test
+    void theOperationHeaderSelectsListVoicesWithPojoRequest() {
+        ListVoicesRequest request = 
ListVoicesRequest.newBuilder().setLanguageCode("de-DE").build();
+
+        Exchange exchange = 
template.request("google-text-to-speech:synthesize?client=#ttsClient&pojoRequest=true",
 e -> {
+            e.getIn().setHeader(GoogleCloudTextToSpeechConstants.OPERATION, 
"listVoices");
+            e.getIn().setBody(request);
+        });
+
+        assertNull(exchange.getException());
+        assertEquals(List.of(VOICE), 
exchange.getMessage().getBody(List.class));
+        verify(client).listVoices(request);
+    }
+
+    @Test
+    void aPathThatIsNoOperationFailsWithPojoRequest() {
+        SynthesizeSpeechRequest request = SynthesizeSpeechRequest.newBuilder()
+                
.setInput(SynthesisInput.newBuilder().setText("Hello")).build();
+
+        Exchange exchange = 
template.request("google-text-to-speech:speak?client=#ttsClient&pojoRequest=true",
+                e -> e.getIn().setBody(request));
+
+        assertInstanceOf(IllegalArgumentException.class, 
exchange.getException());
+        verify(client, 
never()).synthesizeSpeech(any(SynthesizeSpeechRequest.class));
+    }
+
+    @Test
+    void synthesizeWithPojoRequestSendsTheSynthesizeSpeechRequestOfTheBody() {
+        SynthesizeSpeechRequest request = SynthesizeSpeechRequest.newBuilder()
+                .setInput(SynthesisInput.newBuilder().setText("Hello"))
+                
.setVoice(VoiceSelectionParams.newBuilder().setLanguageCode("it-IT"))
+                .build();
+
+        Exchange exchange = 
template.request("google-text-to-speech:synthesize?client=#ttsClient&pojoRequest=true",
+                e -> e.getIn().setBody(request));
+
+        assertNull(exchange.getException());
+        assertArrayEquals(AUDIO, exchange.getMessage().getBody(byte[].class));
+        verify(client).synthesizeSpeech(request);
+    }
+
+    @Test
+    void listVoicesWithoutTheLanguageCodeOptionListsTheVoicesOfAllLanguages() {
+        Exchange exchange = 
template.request("google-text-to-speech:listVoices?client=#ttsClient",
+                e -> e.getIn().setBody(null));
+
+        assertNull(exchange.getException());
+        assertEquals("", sentListVoicesRequest().getLanguageCode());
+        assertEquals(List.of(VOICE), 
exchange.getMessage().getBody(List.class));
+    }
+
+    @Test
+    void listVoicesWithTheLanguageCodeOptionListsTheVoicesOfThatLanguage() {
+        Exchange exchange = 
template.request("google-text-to-speech:listVoices?client=#ttsClient&languageCode=fr-FR",
+                e -> e.getIn().setBody(null));
+
+        assertNull(exchange.getException());
+        assertEquals("fr-FR", sentListVoicesRequest().getLanguageCode());
+    }
+
+    @Test
+    void listVoicesWithTheDefaultLanguageCodeSetOnTheEndpointFiltersByIt() {
+        Exchange exchange = 
template.request("google-text-to-speech:listVoices?client=#ttsClient&languageCode=en-US",
+                e -> e.getIn().setBody(null));
+
+        assertNull(exchange.getException());
+        assertEquals("en-US", sentListVoicesRequest().getLanguageCode());
+    }
+
+    @Test
+    void listVoicesWithTheLanguageCodeSetOnTheComponentFiltersByIt() {
+        context.getComponent("google-text-to-speech", 
GoogleCloudTextToSpeechComponent.class).getConfiguration()
+                .setLanguageCode("de-DE");
+
+        Exchange exchange = 
template.request("google-text-to-speech:listVoices?client=#ttsClient",
+                e -> e.getIn().setBody(null));
+
+        assertNull(exchange.getException());
+        assertEquals("de-DE", sentListVoicesRequest().getLanguageCode());
+    }
+
+    @Test
+    void synthesizeStillUsesTheDefaultLanguageCode() {
+        Exchange exchange = 
template.request("google-text-to-speech:synthesize?client=#ttsClient",
+                e -> e.getIn().setBody("Hello"));
+
+        assertNull(exchange.getException());
+        ArgumentCaptor<VoiceSelectionParams> voice = 
ArgumentCaptor.forClass(VoiceSelectionParams.class);
+        verify(client).synthesizeSpeech(any(SynthesisInput.class), 
voice.capture(), any(AudioConfig.class));
+        assertEquals("en-US", voice.getValue().getLanguageCode());
+        assertArrayEquals(AUDIO, exchange.getMessage().getBody(byte[].class));
+    }
+
+    private ListVoicesRequest sentListVoicesRequest() {
+        ArgumentCaptor<ListVoicesRequest> request = 
ArgumentCaptor.forClass(ListVoicesRequest.class);
+        verify(client).listVoices(request.capture());
+        return request.getValue();
+    }
+}
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 21b2df6d6dcf..49fb1457cf9c 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
@@ -2489,6 +2489,22 @@ new `compile` option of the endpoint (default `false`). 
The module is now loaded
 every producer, so stopping and starting a route does not read the module 
again: remove the route and add it
 again, or restart the application, to load a module that has changed.
 
+=== camel-google-text-to-speech - listVoices lists the voices of all languages 
unless languageCode is set
+
+The `listVoices` operation of `google-text-to-speech` now lists the voices of 
all languages when the `languageCode`
+option is not set. Before, it always listed only the voices of `en-US`, the 
default value of the option, which is meant
+for `synthesize`. To keep listing the voices of one language only, set the 
option on the endpoint, for example
+`google-text-to-speech:listVoices?languageCode=en-US`.
+
+With `pojoRequest=true`, the producer now runs the operation of the endpoint, 
or of the
+`CamelGoogleCloudTextToSpeechOperation` header: `listVoices` takes a 
`ListVoicesRequest` as body and gives the `List` of
+the voices. Before, it always called `synthesize` with a 
`SynthesizeSpeechRequest` body, whatever the operation. A
+`listVoices` endpoint with `pojoRequest=true` now fails with an 
`InvalidPayloadException` for a body that is not a
+`ListVoicesRequest`. The path of the endpoint must therefore name the 
operation, `synthesize` or `listVoices`, unless
+the header does: an endpoint such as 
`google-text-to-speech:speak?pojoRequest=true`, which synthesized before, now 
fails
+with an `IllegalArgumentException`. A `CamelGoogleCloudTextToSpeechOperation` 
header left on the message by an earlier
+step now applies with `pojoRequest=true` too.
+
 === camel-web3j - the consumer subscribes again after an error
 
 When the subscription of a consumer ends with an error (for example a request 
to the node that fails inside
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GoogleCloudTextToSpeechEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GoogleCloudTextToSpeechEndpointBuilderFactory.java
index b5d02ac6a8a0..fa58e21c4202 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GoogleCloudTextToSpeechEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GoogleCloudTextToSpeechEndpointBuilderFactory.java
@@ -76,7 +76,9 @@ public interface 
GoogleCloudTextToSpeechEndpointBuilderFactory {
             return this;
         }
         /**
-         * The language code for the voice (e.g., en-US, fr-FR).
+         * The language code for the voice (e.g., en-US, fr-FR). The default
+         * applies to synthesize only: listVoices lists the voices of all
+         * languages unless this option is set.
          * 
          * The option is a: <code>java.lang.String</code> type.
          * 

Reply via email to