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 720358425bc7 CAMEL-25889: camel-google-mail/drive/calendar/sheets -
create the API client per endpoint credentials instead of one per component
(#28217)
720358425bc7 is described below
commit 720358425bc7dd31b7dff533f49a0b7914e1556f
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Oct 22 22:45:27 2026 +0200
CAMEL-25889: camel-google-mail/drive/calendar/sheets - create the API
client per endpoint credentials instead of one per component (#28217)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
---
.../camel/catalog/docs/google-mail-component.adoc | 2 +
.../google/calendar/GoogleCalendarComponent.java | 33 +++---
.../google/calendar/GoogleCalendarEndpoint.java | 22 +++-
.../stream/GoogleCalendarStreamComponent.java | 31 +++--
.../stream/GoogleCalendarStreamEndpoint.java | 22 +++-
...GoogleCalendarClientPerEndpointOptionsTest.java | 104 +++++++++++++++++
...CalendarStreamClientPerEndpointOptionsTest.java | 109 ++++++++++++++++++
.../google/drive/GoogleDriveComponent.java | 42 ++++---
.../google/drive/GoogleDriveEndpoint.java | 22 +++-
.../GoogleDriveClientPerEndpointOptionsTest.java | 107 +++++++++++++++++
.../src/main/docs/google-mail-component.adoc | 2 +
.../component/google/mail/GoogleMailComponent.java | 63 +++++++---
.../component/google/mail/GoogleMailEndpoint.java | 22 +++-
.../mail/stream/GoogleMailStreamComponent.java | 32 +++---
.../mail/stream/GoogleMailStreamEndpoint.java | 22 +++-
...MailUpdateMessageLabelsDataTypeTransformer.java | 2 +-
.../GoogleMailClientPerEndpointOptionsTest.java | 127 +++++++++++++++++++++
...ogleMailStreamClientPerEndpointOptionsTest.java | 124 ++++++++++++++++++++
.../google/sheets/GoogleSheetsComponent.java | 26 ++---
.../google/sheets/GoogleSheetsEndpoint.java | 22 +++-
.../sheets/stream/GoogleSheetsStreamComponent.java | 24 ++--
.../sheets/stream/GoogleSheetsStreamEndpoint.java | 23 +++-
.../GoogleSheetsClientPerEndpointOptionsTest.java | 103 +++++++++++++++++
...leSheetsStreamClientPerEndpointOptionsTest.java | 109 ++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 15 +++
25 files changed, 1095 insertions(+), 115 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-mail-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-mail-component.adoc
index eeb80e952985..52dd7ab52523 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-mail-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-mail-component.adoc
@@ -77,6 +77,8 @@ The `google-mail:update-message-labels` data type transformer
builds a `ModifyMe
This is useful for modifying Gmail message labels (categorizing, archiving,
starring, moving, etc.) without manually calling the labels/list API.
+The transformer calls the Gmail API with the credentials configured on the
`google-mail` component (for example `camel.component.google-mail.clientId`,
`clientSecret` and `refreshToken`, or `serviceAccountKey` and `delegate`).
Credentials set only on an endpoint URI do not apply to the transformer.
+
=== Exchange Variables
[cols="1,1,3"]
diff --git
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/GoogleCalendarComponent.java
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/GoogleCalendarComponent.java
index 902a6fc68534..960bbdfc263f 100644
---
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/GoogleCalendarComponent.java
+++
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/GoogleCalendarComponent.java
@@ -32,8 +32,6 @@ public class GoogleCalendarComponent
@Metadata
GoogleCalendarConfiguration configuration;
@Metadata(label = "advanced")
- private Calendar client;
- @Metadata(label = "advanced")
private GoogleCalendarClientFactory clientFactory;
public GoogleCalendarComponent() {
@@ -49,23 +47,24 @@ public class GoogleCalendarComponent
return
getCamelContext().getTypeConverter().convertTo(GoogleCalendarApiName.class,
apiNameStr);
}
+ /**
+ * Creates a client for the given configuration with the client factory of
the component. Each endpoint creates its
+ * own client from its own options and keeps it for its lifetime.
+ */
public Calendar getClient(GoogleCalendarConfiguration config) {
- if (client == null) {
- if (config.getClientId() != null && !config.getClientId().isBlank()
- && config.getClientSecret() != null &&
!config.getClientSecret().isBlank()) {
- client = getClientFactory().makeClient(config.getClientId(),
config.getClientSecret(), config.getScopesAsList(),
- config.getApplicationName(), config.getRefreshToken(),
- config.getAccessToken(), config.getEmailAddress(),
config.getP12FileName(), config.getUser());
- } else if (config.getServiceAccountKey() != null &&
!config.getServiceAccountKey().isBlank()) {
- client = getClientFactory().makeClient(getCamelContext(),
config.getServiceAccountKey(),
- config.getScopesAsList(),
- config.getApplicationName(), config.getDelegate());
- } else {
- throw new IllegalArgumentException(
- "(clientId and clientSecret) or serviceAccountKey are
required to create Google Calendar client");
- }
+ if (config.getClientId() != null && !config.getClientId().isBlank()
+ && config.getClientSecret() != null &&
!config.getClientSecret().isBlank()) {
+ return getClientFactory().makeClient(config.getClientId(),
config.getClientSecret(), config.getScopesAsList(),
+ config.getApplicationName(), config.getRefreshToken(),
+ config.getAccessToken(), config.getEmailAddress(),
config.getP12FileName(), config.getUser());
+ } else if (config.getServiceAccountKey() != null &&
!config.getServiceAccountKey().isBlank()) {
+ return getClientFactory().makeClient(getCamelContext(),
config.getServiceAccountKey(),
+ config.getScopesAsList(),
+ config.getApplicationName(), config.getDelegate());
+ } else {
+ throw new IllegalArgumentException(
+ "(clientId and clientSecret) or serviceAccountKey are
required to create Google Calendar client");
}
- return client;
}
public GoogleCalendarClientFactory getClientFactory() {
diff --git
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/GoogleCalendarEndpoint.java
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/GoogleCalendarEndpoint.java
index ce1874bdf544..18bb41040a40 100644
---
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/GoogleCalendarEndpoint.java
+++
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/GoogleCalendarEndpoint.java
@@ -17,6 +17,8 @@
package org.apache.camel.component.google.calendar;
import java.util.Map;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
import com.google.api.services.calendar.Calendar;
import org.apache.camel.Category;
@@ -46,6 +48,8 @@ public class GoogleCalendarEndpoint extends
AbstractApiEndpoint<GoogleCalendarAp
@UriParam
private GoogleCalendarConfiguration configuration;
+ private final Lock clientLock = new ReentrantLock();
+ private volatile Calendar client;
private Object apiProxy;
@@ -116,8 +120,24 @@ public class GoogleCalendarEndpoint extends
AbstractApiEndpoint<GoogleCalendarAp
}
}
+ /**
+ * The client of this endpoint, created on first use from the options of
this endpoint and kept for the lifetime of
+ * the endpoint.
+ */
public Calendar getClient() {
- return ((GoogleCalendarComponent)
getComponent()).getClient(configuration);
+ Calendar answer = client;
+ if (answer == null) {
+ clientLock.lock();
+ try {
+ if (client == null) {
+ client = ((GoogleCalendarComponent)
getComponent()).getClient(configuration);
+ }
+ answer = client;
+ } finally {
+ clientLock.unlock();
+ }
+ }
+ return answer;
}
@Override
diff --git
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamComponent.java
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamComponent.java
index e02ccb129489..2091af64b47f 100644
---
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamComponent.java
+++
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamComponent.java
@@ -30,8 +30,6 @@ import org.apache.camel.support.HealthCheckComponent;
@Component("google-calendar-stream")
public class GoogleCalendarStreamComponent extends HealthCheckComponent {
- @Metadata(label = "advanced")
- private Calendar client;
@Metadata(label = "advanced")
private GoogleCalendarClientFactory clientFactory;
@Metadata
@@ -47,22 +45,23 @@ public class GoogleCalendarStreamComponent extends
HealthCheckComponent {
this.configuration = new GoogleCalendarStreamConfiguration();
}
+ /**
+ * Creates a client for the given configuration with the client factory of
the component. Each endpoint creates its
+ * own client from its own options and keeps it for its lifetime.
+ */
public Calendar getClient(GoogleCalendarStreamConfiguration config) {
- if (client == null) {
- if (config.getClientId() != null && config.getClientSecret() !=
null) {
- client = getClientFactory().makeClient(config.getClientId(),
config.getClientSecret(), config.getScopesAsList(),
- config.getApplicationName(), config.getRefreshToken(),
- config.getAccessToken(), config.getEmailAddress(),
config.getP12FileName(), config.getUser());
- } else if (config.getServiceAccountKey() != null) {
- client = getClientFactory().makeClient(getCamelContext(),
config.getServiceAccountKey(),
- config.getScopesAsList(),
- config.getApplicationName(), config.getDelegate());
- } else {
- throw new IllegalArgumentException(
- "(clientId and clientSecret) or serviceAccountKey are
required to create Gmail client");
- }
+ if (config.getClientId() != null && config.getClientSecret() != null) {
+ return getClientFactory().makeClient(config.getClientId(),
config.getClientSecret(), config.getScopesAsList(),
+ config.getApplicationName(), config.getRefreshToken(),
+ config.getAccessToken(), config.getEmailAddress(),
config.getP12FileName(), config.getUser());
+ } else if (config.getServiceAccountKey() != null) {
+ return getClientFactory().makeClient(getCamelContext(),
config.getServiceAccountKey(),
+ config.getScopesAsList(),
+ config.getApplicationName(), config.getDelegate());
+ } else {
+ throw new IllegalArgumentException(
+ "(clientId and clientSecret) or serviceAccountKey are
required to create Google Calendar client");
}
- return client;
}
/**
diff --git
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamEndpoint.java
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamEndpoint.java
index c14d6783bde5..5dbd2b3e9cba 100644
---
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamEndpoint.java
+++
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamEndpoint.java
@@ -17,6 +17,8 @@
package org.apache.camel.component.google.calendar.stream;
import java.util.Map;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
import com.google.api.services.calendar.Calendar;
import com.google.api.services.calendar.model.Event;
@@ -46,6 +48,8 @@ public class GoogleCalendarStreamEndpoint extends
ScheduledPollEndpoint implemen
@UriParam
private GoogleCalendarStreamConfiguration configuration;
+ private final Lock clientLock = new ReentrantLock();
+ private volatile Calendar client;
public GoogleCalendarStreamEndpoint(String uri,
GoogleCalendarStreamComponent component,
GoogleCalendarStreamConfiguration
endpointConfiguration) {
@@ -75,8 +79,24 @@ public class GoogleCalendarStreamEndpoint extends
ScheduledPollEndpoint implemen
return consumer;
}
+ /**
+ * The client of this endpoint, created on first use from the options of
this endpoint and kept for the lifetime of
+ * the endpoint.
+ */
public Calendar getClient() {
- return ((GoogleCalendarStreamComponent)
getComponent()).getClient(configuration);
+ Calendar answer = client;
+ if (answer == null) {
+ clientLock.lock();
+ try {
+ if (client == null) {
+ client = ((GoogleCalendarStreamComponent)
getComponent()).getClient(configuration);
+ }
+ answer = client;
+ } finally {
+ clientLock.unlock();
+ }
+ }
+ return answer;
}
public GoogleCalendarStreamConfiguration getConfiguration() {
diff --git
a/components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/GoogleCalendarClientPerEndpointOptionsTest.java
b/components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/GoogleCalendarClientPerEndpointOptionsTest.java
new file mode 100644
index 000000000000..39563caaed2a
--- /dev/null
+++
b/components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/GoogleCalendarClientPerEndpointOptionsTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.calendar;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.services.calendar.Calendar;
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+class GoogleCalendarClientPerEndpointOptionsTest {
+
+ private static final String OAUTH =
"?clientId=id&clientSecret=secret&refreshToken=";
+ private static final String SERVICE_ACCOUNT =
"?serviceAccountKey=classpath:service-account.json&delegate=";
+
+ @Test
+ void shouldCreateOneClientPerEndpoint() throws Exception {
+ RecordingClientFactory factory = new RecordingClientFactory();
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.getComponent("google-calendar",
GoogleCalendarComponent.class).setClientFactory(factory);
+ context.start();
+
+ GoogleCalendarEndpoint first =
context.getEndpoint("google-calendar://calendars/get" + OAUTH + "token-a",
+ GoogleCalendarEndpoint.class);
+ GoogleCalendarEndpoint second =
context.getEndpoint("google-calendar://calendars/get" + OAUTH + "token-b",
+ GoogleCalendarEndpoint.class);
+ GoogleCalendarEndpoint sameAsFirst = context.getEndpoint(
+ "google-calendar://calendars/insert" + OAUTH + "token-a",
GoogleCalendarEndpoint.class);
+
+ assertEquals(Arrays.asList("token-a", "token-b", "token-a"),
factory.created);
+ assertNotSame(first.getClient(), second.getClient());
+ assertNotSame(first.getClient(), sameAsFirst.getClient());
+ assertSame(first.getClient(), first.getClient());
+ assertEquals(3, factory.created.size());
+ }
+ }
+
+ @Test
+ void shouldKeepOneClientPerDelegate() throws Exception {
+ RecordingClientFactory factory = new RecordingClientFactory();
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.getComponent("google-calendar",
GoogleCalendarComponent.class).setClientFactory(factory);
+ context.start();
+
+ GoogleCalendarEndpoint first = context.getEndpoint(
+ "google-calendar://calendars/get" + SERVICE_ACCOUNT +
"user-a", GoogleCalendarEndpoint.class);
+ GoogleCalendarEndpoint second = context.getEndpoint(
+ "google-calendar://calendars/get" + SERVICE_ACCOUNT +
"user-b", GoogleCalendarEndpoint.class);
+
+ assertEquals(Arrays.asList("user-a", "user-b"), factory.created);
+ assertNotSame(first.getClient(), second.getClient());
+ }
+ }
+
+ private static final class RecordingClientFactory implements
GoogleCalendarClientFactory {
+
+ private final List<String> created = new ArrayList<>();
+
+ @Override
+ public Calendar makeClient(
+ String clientId, String clientSecret, Collection<String>
scopes, String applicationName,
+ String refreshToken, String accessToken, String emailAddress,
String p12FileName, String user) {
+ created.add(refreshToken);
+ return newClient();
+ }
+
+ @Override
+ public Calendar makeClient(
+ CamelContext camelContext, String serviceAccountKey,
Collection<String> scopes, String applicationName,
+ String delegate) {
+ created.add(delegate);
+ return newClient();
+ }
+
+ private static Calendar newClient() {
+ return new Calendar.Builder(new NetHttpTransport(), new
JacksonFactory(), null).setApplicationName("test")
+ .build();
+ }
+ }
+}
diff --git
a/components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamClientPerEndpointOptionsTest.java
b/components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamClientPerEndpointOptionsTest.java
new file mode 100644
index 000000000000..220e5f52ba52
--- /dev/null
+++
b/components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamClientPerEndpointOptionsTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.calendar.stream;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.services.calendar.Calendar;
+import org.apache.camel.CamelContext;
+import org.apache.camel.component.google.calendar.GoogleCalendarClientFactory;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+class GoogleCalendarStreamClientPerEndpointOptionsTest {
+
+ private static final String OAUTH =
"?clientId=id&clientSecret=secret&refreshToken=";
+ private static final String SERVICE_ACCOUNT =
"?serviceAccountKey=classpath:service-account.json&delegate=";
+
+ @Test
+ void shouldCreateOneClientPerEndpoint() throws Exception {
+ RecordingClientFactory factory = new RecordingClientFactory();
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.getComponent("google-calendar-stream",
GoogleCalendarStreamComponent.class).setClientFactory(factory);
+ context.start();
+
+ GoogleCalendarStreamEndpoint first = context.getEndpoint(
+ "google-calendar-stream://cal-a" + OAUTH + "token-a",
GoogleCalendarStreamEndpoint.class);
+ GoogleCalendarStreamEndpoint second = context.getEndpoint(
+ "google-calendar-stream://cal-b" + OAUTH + "token-b",
GoogleCalendarStreamEndpoint.class);
+ GoogleCalendarStreamEndpoint sameAsFirst = context.getEndpoint(
+ "google-calendar-stream://cal-c" + OAUTH + "token-a",
GoogleCalendarStreamEndpoint.class);
+
+ Calendar firstClient = first.getClient();
+ Calendar secondClient = second.getClient();
+
+ assertNotSame(firstClient, secondClient);
+ assertNotSame(firstClient, sameAsFirst.getClient());
+ assertSame(firstClient, first.getClient());
+ assertEquals(Arrays.asList("token-a", "token-b", "token-a"),
factory.created);
+ }
+ }
+
+ @Test
+ void shouldKeepOneClientPerDelegate() throws Exception {
+ RecordingClientFactory factory = new RecordingClientFactory();
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.getComponent("google-calendar-stream",
GoogleCalendarStreamComponent.class).setClientFactory(factory);
+ context.start();
+
+ GoogleCalendarStreamEndpoint first
+ = context.getEndpoint("google-calendar-stream://cal-a" +
SERVICE_ACCOUNT + "user-a",
+ GoogleCalendarStreamEndpoint.class);
+ GoogleCalendarStreamEndpoint second
+ = context.getEndpoint("google-calendar-stream://cal-b" +
SERVICE_ACCOUNT + "user-b",
+ GoogleCalendarStreamEndpoint.class);
+
+ assertNotSame(first.getClient(), second.getClient());
+ assertEquals(Arrays.asList("user-a", "user-b"), factory.created);
+ }
+ }
+
+ private static final class RecordingClientFactory implements
GoogleCalendarClientFactory {
+
+ private final List<String> created = new ArrayList<>();
+
+ @Override
+ public Calendar makeClient(
+ String clientId, String clientSecret, Collection<String>
scopes, String applicationName,
+ String refreshToken, String accessToken, String emailAddress,
String p12FileName, String user) {
+ created.add(refreshToken);
+ return newClient();
+ }
+
+ @Override
+ public Calendar makeClient(
+ CamelContext camelContext, String serviceAccountKey,
Collection<String> scopes, String applicationName,
+ String delegate) {
+ created.add(delegate);
+ return newClient();
+ }
+
+ private static Calendar newClient() {
+ return new Calendar.Builder(new NetHttpTransport(), new
JacksonFactory(), null).setApplicationName("test")
+ .build();
+ }
+ }
+}
diff --git
a/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveComponent.java
b/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveComponent.java
index 74d14d010355..83a3b16feb44 100644
---
a/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveComponent.java
+++
b/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveComponent.java
@@ -37,8 +37,6 @@ public class GoogleDriveComponent
@Metadata
GoogleDriveConfiguration configuration;
@Metadata(label = "advanced")
- private Drive client;
- @Metadata(label = "advanced")
private GoogleDriveClientFactory clientFactory;
@Metadata(label = "proxy", description = "Proxy server host")
private String proxyHost;
@@ -58,22 +56,34 @@ public class GoogleDriveComponent
return
getCamelContext().getTypeConverter().convertTo(GoogleDriveApiName.class,
apiNameStr);
}
+ /**
+ * Creates a client for the given configuration with the client factory of
the component.
+ *
+ * @see #getClient(GoogleDriveConfiguration, GoogleDriveClientFactory)
+ */
public Drive getClient(GoogleDriveConfiguration config) {
- if (client == null) {
- if (config.getClientId() != null && !config.getClientId().isBlank()
- && config.getClientSecret() != null &&
!config.getClientSecret().isBlank()) {
- client = getClientFactory().makeClient(config.getClientId(),
- config.getClientSecret(), config.getScopesAsList(),
- config.getApplicationName(), config.getRefreshToken(),
config.getAccessToken());
- } else if (config.getServiceAccountKey() != null &&
!config.getServiceAccountKey().isBlank()) {
- client = getClientFactory().makeClient(getCamelContext(),
config.getServiceAccountKey(),
- config.getScopesAsList(), config.getApplicationName(),
config.getDelegate());
- } else {
- throw new IllegalArgumentException(
- "(clientId and clientSecret) or serviceAccountKey are
required to create Google Drive client");
- }
+ return getClient(config, null);
+ }
+
+ /**
+ * Creates a client for the given configuration with the given client
factory or, when that is null, with the client
+ * factory of the component. Each endpoint creates its own client from its
own options and keeps it for its
+ * lifetime.
+ */
+ public Drive getClient(GoogleDriveConfiguration config,
GoogleDriveClientFactory factory) {
+ GoogleDriveClientFactory factoryToUse = factory != null ? factory :
getClientFactory();
+ if (config.getClientId() != null && !config.getClientId().isBlank()
+ && config.getClientSecret() != null &&
!config.getClientSecret().isBlank()) {
+ return factoryToUse.makeClient(config.getClientId(),
+ config.getClientSecret(), config.getScopesAsList(),
+ config.getApplicationName(), config.getRefreshToken(),
config.getAccessToken());
+ } else if (config.getServiceAccountKey() != null &&
!config.getServiceAccountKey().isBlank()) {
+ return factoryToUse.makeClient(getCamelContext(),
config.getServiceAccountKey(),
+ config.getScopesAsList(), config.getApplicationName(),
config.getDelegate());
+ } else {
+ throw new IllegalArgumentException(
+ "(clientId and clientSecret) or serviceAccountKey are
required to create Google Drive client");
}
- return client;
}
public GoogleDriveClientFactory getClientFactory() {
diff --git
a/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveEndpoint.java
b/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveEndpoint.java
index 9a428f77400b..def2ac80d3c5 100644
---
a/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveEndpoint.java
+++
b/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveEndpoint.java
@@ -17,6 +17,8 @@
package org.apache.camel.component.google.drive;
import java.util.Map;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
import com.google.api.services.drive.Drive;
import org.apache.camel.*;
@@ -44,6 +46,8 @@ public class GoogleDriveEndpoint extends
AbstractApiEndpoint<GoogleDriveApiName,
@UriParam
private GoogleDriveConfiguration configuration;
+ private final Lock clientLock = new ReentrantLock();
+ private volatile Drive client;
@UriParam
private GoogleDriveClientFactory clientFactory;
@@ -120,8 +124,24 @@ public class GoogleDriveEndpoint extends
AbstractApiEndpoint<GoogleDriveApiName,
}
}
+ /**
+ * The client of this endpoint, created on first use from the options of
this endpoint and kept for the lifetime of
+ * the endpoint.
+ */
public Drive getClient() {
- return ((GoogleDriveComponent)
getComponent()).getClient(configuration);
+ Drive answer = client;
+ if (answer == null) {
+ clientLock.lock();
+ try {
+ if (client == null) {
+ client = ((GoogleDriveComponent)
getComponent()).getClient(configuration, clientFactory);
+ }
+ answer = client;
+ } finally {
+ clientLock.unlock();
+ }
+ }
+ return answer;
}
@Override
diff --git
a/components/camel-google/camel-google-drive/src/test/java/org/apache/camel/component/google/drive/GoogleDriveClientPerEndpointOptionsTest.java
b/components/camel-google/camel-google-drive/src/test/java/org/apache/camel/component/google/drive/GoogleDriveClientPerEndpointOptionsTest.java
new file mode 100644
index 000000000000..ea99736905fb
--- /dev/null
+++
b/components/camel-google/camel-google-drive/src/test/java/org/apache/camel/component/google/drive/GoogleDriveClientPerEndpointOptionsTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.drive;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.services.drive.Drive;
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+class GoogleDriveClientPerEndpointOptionsTest {
+
+ private static final String OAUTH =
"?clientId=id&clientSecret=secret&refreshToken=";
+
+ @Test
+ void shouldCreateOneClientPerEndpoint() throws Exception {
+ RecordingClientFactory factory = new RecordingClientFactory();
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.getComponent("google-drive",
GoogleDriveComponent.class).setClientFactory(factory);
+ context.start();
+
+ GoogleDriveEndpoint first
+ = context.getEndpoint("google-drive://drive-files/list" +
OAUTH + "token-a", GoogleDriveEndpoint.class);
+ GoogleDriveEndpoint second
+ = context.getEndpoint("google-drive://drive-files/list" +
OAUTH + "token-b", GoogleDriveEndpoint.class);
+ GoogleDriveEndpoint sameAsFirst
+ = context.getEndpoint("google-drive://drive-files/get" +
OAUTH + "token-a", GoogleDriveEndpoint.class);
+
+ assertEquals(Arrays.asList("token-a", "token-b", "token-a"),
factory.created);
+ assertNotSame(first.getClient(), second.getClient());
+ assertNotSame(first.getClient(), sameAsFirst.getClient());
+ assertSame(first.getClient(), first.getClient());
+ assertEquals(3, factory.created.size());
+ }
+ }
+
+ @Test
+ void shouldUseTheClientFactoryOfTheEndpoint() throws Exception {
+ RecordingClientFactory componentFactory = new RecordingClientFactory();
+ RecordingClientFactory endpointFactory = new RecordingClientFactory();
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.getRegistry().bind("endpointFactory", endpointFactory);
+ context.getComponent("google-drive",
GoogleDriveComponent.class).setClientFactory(componentFactory);
+ context.start();
+
+ GoogleDriveEndpoint withOwnFactory = context.getEndpoint(
+ "google-drive://drive-files/list" + OAUTH +
"token-a&clientFactory=#endpointFactory",
+ GoogleDriveEndpoint.class);
+ GoogleDriveEndpoint withComponentFactory
+ = context.getEndpoint("google-drive://drive-files/get" +
OAUTH + "token-a", GoogleDriveEndpoint.class);
+
+ assertEquals(List.of("token-a"), endpointFactory.created);
+ assertEquals(List.of("token-a"), componentFactory.created);
+ assertNotSame(withOwnFactory.getClient(),
withComponentFactory.getClient());
+ }
+ }
+
+ private static final class RecordingClientFactory implements
GoogleDriveClientFactory {
+
+ private final List<String> created = new ArrayList<>();
+
+ @Override
+ public Drive makeClient(
+ String clientId, String clientSecret, Collection<String>
scopes, String applicationName,
+ String refreshToken, String accessToken) {
+ created.add(refreshToken);
+ return newClient();
+ }
+
+ @Override
+ public Drive makeClient(
+ CamelContext camelContext, String serviceAccountKey,
Collection<String> scopes, String applicationName,
+ String delegate) {
+ created.add(delegate);
+ return newClient();
+ }
+
+ private static Drive newClient() {
+ return new Drive.Builder(new NetHttpTransport(), new
JacksonFactory(), null).setApplicationName("test")
+ .build();
+ }
+ }
+}
diff --git
a/components/camel-google/camel-google-mail/src/main/docs/google-mail-component.adoc
b/components/camel-google/camel-google-mail/src/main/docs/google-mail-component.adoc
index eeb80e952985..52dd7ab52523 100644
---
a/components/camel-google/camel-google-mail/src/main/docs/google-mail-component.adoc
+++
b/components/camel-google/camel-google-mail/src/main/docs/google-mail-component.adoc
@@ -77,6 +77,8 @@ The `google-mail:update-message-labels` data type transformer
builds a `ModifyMe
This is useful for modifying Gmail message labels (categorizing, archiving,
starring, moving, etc.) without manually calling the labels/list API.
+The transformer calls the Gmail API with the credentials configured on the
`google-mail` component (for example `camel.component.google-mail.clientId`,
`clientSecret` and `refreshToken`, or `serviceAccountKey` and `delegate`).
Credentials set only on an endpoint URI do not apply to the transformer.
+
=== Exchange Variables
[cols="1,1,3"]
diff --git
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/GoogleMailComponent.java
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/GoogleMailComponent.java
index 10771cc95b0a..009da6c7de62 100644
---
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/GoogleMailComponent.java
+++
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/GoogleMailComponent.java
@@ -16,6 +16,9 @@
*/
package org.apache.camel.component.google.mail;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
import com.google.api.services.gmail.Gmail;
import org.apache.camel.CamelContext;
import org.apache.camel.Endpoint;
@@ -32,9 +35,9 @@ public class GoogleMailComponent
@Metadata
GoogleMailConfiguration configuration;
@Metadata(label = "advanced")
- private Gmail client;
- @Metadata(label = "advanced")
private GoogleMailClientFactory clientFactory;
+ private final Lock clientLock = new ReentrantLock();
+ private volatile Gmail client;
public GoogleMailComponent() {
super(GoogleMailApiName.class,
GoogleMailApiCollection.getCollection());
@@ -49,22 +52,44 @@ public class GoogleMailComponent
return
getCamelContext().getTypeConverter().convertTo(GoogleMailApiName.class,
apiNameStr);
}
- public Gmail getClient(GoogleMailConfiguration config) {
- if (client == null) {
- if (config.getClientId() != null && !config.getClientId().isBlank()
- && config.getClientSecret() != null &&
!config.getClientSecret().isBlank()) {
- client = getClientFactory().makeClient(config.getClientId(),
- config.getClientSecret(), config.getScopesAsList(),
- config.getApplicationName(), config.getRefreshToken(),
config.getAccessToken());
- } else if (config.getServiceAccountKey() != null &&
!config.getServiceAccountKey().isBlank()) {
- client = getClientFactory().makeClient(getCamelContext(),
config.getServiceAccountKey(),
- config.getScopesAsList(), config.getApplicationName(),
config.getDelegate());
- } else {
- throw new IllegalArgumentException(
- "(clientId and clientSecret) or serviceAccountKey are
required to create Gmail client");
+ /**
+ * Gets the client for the configuration of the component, created on
first use and kept until the component stops.
+ * It serves callers that have no endpoint, such as the {@code
google-mail:update-message-labels} data type
+ * transformer. Endpoints do not use it: each endpoint creates its own
client from its own options.
+ */
+ public Gmail getClient() {
+ Gmail answer = client;
+ if (answer == null) {
+ clientLock.lock();
+ try {
+ if (client == null) {
+ client = getClient(getConfiguration());
+ }
+ answer = client;
+ } finally {
+ clientLock.unlock();
}
}
- return client;
+ return answer;
+ }
+
+ /**
+ * Creates a client for the given configuration with the client factory of
the component. Each endpoint creates its
+ * own client from its own options and keeps it for its lifetime.
+ */
+ public Gmail getClient(GoogleMailConfiguration config) {
+ if (config.getClientId() != null && !config.getClientId().isBlank()
+ && config.getClientSecret() != null &&
!config.getClientSecret().isBlank()) {
+ return getClientFactory().makeClient(config.getClientId(),
+ config.getClientSecret(), config.getScopesAsList(),
+ config.getApplicationName(), config.getRefreshToken(),
config.getAccessToken());
+ } else if (config.getServiceAccountKey() != null &&
!config.getServiceAccountKey().isBlank()) {
+ return getClientFactory().makeClient(getCamelContext(),
config.getServiceAccountKey(),
+ config.getScopesAsList(), config.getApplicationName(),
config.getDelegate());
+ } else {
+ throw new IllegalArgumentException(
+ "(clientId and clientSecret) or serviceAccountKey are
required to create Gmail client");
+ }
}
public GoogleMailClientFactory getClientFactory() {
@@ -105,4 +130,10 @@ public class GoogleMailComponent
endpointConfiguration.setMethodName(methodName);
return new GoogleMailEndpoint(uri, this, apiName, methodName,
endpointConfiguration);
}
+
+ @Override
+ protected void doStop() throws Exception {
+ client = null;
+ super.doStop();
+ }
}
diff --git
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/GoogleMailEndpoint.java
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/GoogleMailEndpoint.java
index 979c23067f2e..91887d07f231 100644
---
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/GoogleMailEndpoint.java
+++
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/GoogleMailEndpoint.java
@@ -17,6 +17,8 @@
package org.apache.camel.component.google.mail;
import java.util.Map;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
import com.google.api.services.gmail.Gmail;
import org.apache.camel.Category;
@@ -52,6 +54,8 @@ public class GoogleMailEndpoint extends
AbstractApiEndpoint<GoogleMailApiName, G
@UriParam
private GoogleMailConfiguration configuration;
+ private final Lock clientLock = new ReentrantLock();
+ private volatile Gmail client;
public GoogleMailEndpoint(String uri, GoogleMailComponent component,
GoogleMailApiName apiName, String methodName,
GoogleMailConfiguration endpointConfiguration) {
@@ -116,8 +120,24 @@ public class GoogleMailEndpoint extends
AbstractApiEndpoint<GoogleMailApiName, G
}
}
+ /**
+ * The client of this endpoint, created on first use from the options of
this endpoint and kept for the lifetime of
+ * the endpoint.
+ */
public Gmail getClient() {
- return ((GoogleMailComponent) getComponent()).getClient(configuration);
+ Gmail answer = client;
+ if (answer == null) {
+ clientLock.lock();
+ try {
+ if (client == null) {
+ client = ((GoogleMailComponent)
getComponent()).getClient(configuration);
+ }
+ answer = client;
+ } finally {
+ clientLock.unlock();
+ }
+ }
+ return answer;
}
@Override
diff --git
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamComponent.java
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamComponent.java
index b34760b7621f..97f96b453983 100644
---
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamComponent.java
+++
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamComponent.java
@@ -33,8 +33,6 @@ import org.apache.camel.support.HealthCheckComponent;
@Component("google-mail-stream")
public class GoogleMailStreamComponent extends HealthCheckComponent {
- @Metadata(label = "advanced")
- private Gmail client;
@Metadata(label = "advanced")
private GoogleMailClientFactory clientFactory;
@Metadata(label = "advanced")
@@ -49,20 +47,24 @@ public class GoogleMailStreamComponent extends
HealthCheckComponent {
this.configuration = new GoogleMailStreamConfiguration();
}
- public Gmail getClient(GoogleMailStreamConfiguration
googleMailConfiguration) {
- if (client == null) {
- if (googleMailConfiguration.getClientSecret() != null) {
- client =
getClientFactory().makeClient(googleMailConfiguration.getClientId(),
- googleMailConfiguration.getClientSecret(),
googleMailConfiguration.getScopesAsList(),
- googleMailConfiguration.getApplicationName(),
googleMailConfiguration.getRefreshToken(),
- googleMailConfiguration.getAccessToken());
- } else if (googleMailConfiguration.getServiceAccountKey() != null)
{
- client = getClientFactory().makeClient(getCamelContext(),
googleMailConfiguration.getServiceAccountKey(),
- googleMailConfiguration.getScopesAsList(),
googleMailConfiguration.getApplicationName(),
- googleMailConfiguration.getDelegate());
- }
+ /**
+ * Creates a client for the given configuration with the client factory of
the component. Each endpoint creates its
+ * own client from its own options and keeps it for its lifetime.
+ */
+ public Gmail getClient(GoogleMailStreamConfiguration config) {
+ if (config.getClientSecret() != null) {
+ return getClientFactory().makeClient(config.getClientId(),
+ config.getClientSecret(), config.getScopesAsList(),
+ config.getApplicationName(), config.getRefreshToken(),
+ config.getAccessToken());
+ } else if (config.getServiceAccountKey() != null) {
+ return getClientFactory().makeClient(getCamelContext(),
config.getServiceAccountKey(),
+ config.getScopesAsList(), config.getApplicationName(),
+ config.getDelegate());
+ } else {
+ throw new IllegalArgumentException(
+ "(clientId and clientSecret) or serviceAccountKey are
required to create Gmail client");
}
- return client;
}
/**
diff --git
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamEndpoint.java
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamEndpoint.java
index 3fa4d9d368ec..9888ccb7e15e 100644
---
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamEndpoint.java
+++
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamEndpoint.java
@@ -18,6 +18,8 @@ package org.apache.camel.component.google.mail.stream;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
import com.google.api.services.gmail.Gmail;
import com.google.api.services.gmail.model.Label;
@@ -47,6 +49,8 @@ public class GoogleMailStreamEndpoint extends
ScheduledPollEndpoint implements E
@UriParam
private GoogleMailStreamConfiguration configuration;
+ private final Lock clientLock = new ReentrantLock();
+ private volatile Gmail client;
public GoogleMailStreamEndpoint(String uri, GoogleMailStreamComponent
component,
GoogleMailStreamConfiguration
endpointConfiguration) {
@@ -86,8 +90,24 @@ public class GoogleMailStreamEndpoint extends
ScheduledPollEndpoint implements E
return consumer;
}
+ /**
+ * The client of this endpoint, created on first use from the options of
this endpoint and kept for the lifetime of
+ * the endpoint.
+ */
public Gmail getClient() {
- return ((GoogleMailStreamComponent)
getComponent()).getClient(configuration);
+ Gmail answer = client;
+ if (answer == null) {
+ clientLock.lock();
+ try {
+ if (client == null) {
+ client = ((GoogleMailStreamComponent)
getComponent()).getClient(configuration);
+ }
+ answer = client;
+ } finally {
+ clientLock.unlock();
+ }
+ }
+ return answer;
}
public GoogleMailClientFactory getClientFactory() {
diff --git
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/transform/GoogleMailUpdateMessageLabelsDataTypeTransformer.java
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/transform/GoogleMailUpdateMessageLabelsDataTypeTransformer.java
index 6dcf63b8cd70..d09e91228156 100644
---
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/transform/GoogleMailUpdateMessageLabelsDataTypeTransformer.java
+++
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/transform/GoogleMailUpdateMessageLabelsDataTypeTransformer.java
@@ -128,6 +128,6 @@ public class
GoogleMailUpdateMessageLabelsDataTypeTransformer extends Transforme
private Gmail getGmailClient() {
GoogleMailComponent component =
getCamelContext().getComponent("google-mail", GoogleMailComponent.class);
- return component.getClient(component.getConfiguration());
+ return component.getClient();
}
}
diff --git
a/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/GoogleMailClientPerEndpointOptionsTest.java
b/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/GoogleMailClientPerEndpointOptionsTest.java
new file mode 100644
index 000000000000..4e900ef1150a
--- /dev/null
+++
b/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/GoogleMailClientPerEndpointOptionsTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.api.services.gmail.Gmail;
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+class GoogleMailClientPerEndpointOptionsTest {
+
+ private static final String OAUTH =
"?clientId=id&clientSecret=secret&refreshToken=";
+ private static final String SERVICE_ACCOUNT =
"?serviceAccountKey=classpath:service-account.json&delegate=";
+
+ @Test
+ void shouldCreateOneClientPerEndpoint() throws Exception {
+ RecordingClientFactory factory = new RecordingClientFactory();
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.getComponent("google-mail",
GoogleMailComponent.class).setClientFactory(factory);
+ context.start();
+
+ GoogleMailEndpoint first
+ = context.getEndpoint("google-mail://messages/list" +
OAUTH + "token-a", GoogleMailEndpoint.class);
+ GoogleMailEndpoint second
+ = context.getEndpoint("google-mail://messages/list" +
OAUTH + "token-b", GoogleMailEndpoint.class);
+ GoogleMailEndpoint sameAsFirst
+ = context.getEndpoint("google-mail://messages/get" + OAUTH
+ "token-a", GoogleMailEndpoint.class);
+
+ assertEquals(Arrays.asList("token-a", "token-b", "token-a"),
factory.created);
+ assertNotSame(first.getClient(), second.getClient());
+ assertNotSame(first.getClient(), sameAsFirst.getClient());
+ assertSame(first.getClient(), first.getClient());
+ assertEquals(3, factory.created.size());
+ }
+ }
+
+ @Test
+ void shouldKeepOneClientPerDelegate() throws Exception {
+ RecordingClientFactory factory = new RecordingClientFactory();
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.getComponent("google-mail",
GoogleMailComponent.class).setClientFactory(factory);
+ context.start();
+
+ GoogleMailEndpoint first =
context.getEndpoint("google-mail://messages/list" + SERVICE_ACCOUNT + "user-a",
+ GoogleMailEndpoint.class);
+ GoogleMailEndpoint second =
context.getEndpoint("google-mail://messages/list" + SERVICE_ACCOUNT + "user-b",
+ GoogleMailEndpoint.class);
+
+ assertEquals(Arrays.asList("user-a", "user-b"), factory.created);
+ assertNotSame(first.getClient(), second.getClient());
+ }
+ }
+
+ @Test
+ void shouldKeepOneClientForTheComponentConfiguration() throws Exception {
+ RecordingClientFactory factory = new RecordingClientFactory();
+ try (CamelContext context = new DefaultCamelContext()) {
+ GoogleMailComponent component =
context.getComponent("google-mail", GoogleMailComponent.class);
+ component.setClientFactory(factory);
+ GoogleMailConfiguration configuration = new
GoogleMailConfiguration();
+ configuration.setClientId("id");
+ configuration.setClientSecret("secret");
+ configuration.setRefreshToken("token-component");
+ component.setConfiguration(configuration);
+ context.start();
+
+ Gmail componentClient = component.getClient();
+ GoogleMailEndpoint endpoint
+ = context.getEndpoint("google-mail://messages/list" +
OAUTH + "token-a", GoogleMailEndpoint.class);
+
+ assertSame(componentClient, component.getClient());
+ assertNotSame(componentClient, endpoint.getClient());
+ assertEquals(List.of("token-component", "token-a"),
factory.created);
+ }
+ }
+
+ private static final class RecordingClientFactory implements
GoogleMailClientFactory {
+
+ private final List<String> created = new ArrayList<>();
+
+ @Override
+ public Gmail makeClient(
+ String clientId, String clientSecret, Collection<String>
scopes, String applicationName,
+ String refreshToken, String accessToken) {
+ created.add(refreshToken);
+ return newClient();
+ }
+
+ @Override
+ public Gmail makeClient(
+ CamelContext camelContext, String serviceAccountKey,
Collection<String> scopes, String applicationName,
+ String delegate) {
+ created.add(delegate);
+ return newClient();
+ }
+
+ private static Gmail newClient() {
+ return new Gmail.Builder(new NetHttpTransport(),
GsonFactory.getDefaultInstance(), null)
+ .setApplicationName("test").build();
+ }
+ }
+}
diff --git
a/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamClientPerEndpointOptionsTest.java
b/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamClientPerEndpointOptionsTest.java
new file mode 100644
index 000000000000..3573d822652d
--- /dev/null
+++
b/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamClientPerEndpointOptionsTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.api.services.gmail.Gmail;
+import org.apache.camel.CamelContext;
+import org.apache.camel.component.google.mail.GoogleMailClientFactory;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class GoogleMailStreamClientPerEndpointOptionsTest {
+
+ private static final String OAUTH =
"?clientId=id&clientSecret=secret&refreshToken=";
+ private static final String SERVICE_ACCOUNT =
"?serviceAccountKey=classpath:service-account.json&delegate=";
+
+ @Test
+ void shouldCreateOneClientPerEndpoint() throws Exception {
+ RecordingClientFactory factory = new RecordingClientFactory();
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.getComponent("google-mail-stream",
GoogleMailStreamComponent.class).setClientFactory(factory);
+ context.start();
+
+ GoogleMailStreamEndpoint first =
context.getEndpoint("google-mail-stream://box-a" + OAUTH + "token-a",
+ GoogleMailStreamEndpoint.class);
+ GoogleMailStreamEndpoint second =
context.getEndpoint("google-mail-stream://box-b" + OAUTH + "token-b",
+ GoogleMailStreamEndpoint.class);
+ GoogleMailStreamEndpoint sameAsFirst =
context.getEndpoint("google-mail-stream://box-c" + OAUTH + "token-a",
+ GoogleMailStreamEndpoint.class);
+
+ Gmail firstClient = first.getClient();
+ Gmail secondClient = second.getClient();
+
+ assertNotSame(firstClient, secondClient);
+ assertNotSame(firstClient, sameAsFirst.getClient());
+ assertSame(firstClient, first.getClient());
+ assertEquals(Arrays.asList("token-a", "token-b", "token-a"),
factory.created);
+ }
+ }
+
+ @Test
+ void shouldKeepOneClientPerDelegate() throws Exception {
+ RecordingClientFactory factory = new RecordingClientFactory();
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.getComponent("google-mail-stream",
GoogleMailStreamComponent.class).setClientFactory(factory);
+ context.start();
+
+ GoogleMailStreamEndpoint first =
context.getEndpoint("google-mail-stream://box-a" + SERVICE_ACCOUNT + "user-a",
+ GoogleMailStreamEndpoint.class);
+ GoogleMailStreamEndpoint second =
context.getEndpoint("google-mail-stream://box-b" + SERVICE_ACCOUNT + "user-b",
+ GoogleMailStreamEndpoint.class);
+
+ assertNotSame(first.getClient(), second.getClient());
+ assertEquals(Arrays.asList("user-a", "user-b"), factory.created);
+ }
+ }
+
+ @Test
+ void shouldFailWithoutCredentials() throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.getComponent("google-mail-stream",
GoogleMailStreamComponent.class)
+ .setClientFactory(new RecordingClientFactory());
+ context.start();
+
+ GoogleMailStreamEndpoint endpoint
+ = context.getEndpoint("google-mail-stream://box-a",
GoogleMailStreamEndpoint.class);
+
+ IllegalArgumentException e =
assertThrows(IllegalArgumentException.class, endpoint::getClient);
+ assertTrue(e.getMessage().contains("serviceAccountKey"),
e.getMessage());
+ }
+ }
+
+ private static final class RecordingClientFactory implements
GoogleMailClientFactory {
+
+ private final List<String> created = new ArrayList<>();
+
+ @Override
+ public Gmail makeClient(
+ String clientId, String clientSecret, Collection<String>
scopes, String applicationName,
+ String refreshToken, String accessToken) {
+ created.add(refreshToken);
+ return newClient();
+ }
+
+ @Override
+ public Gmail makeClient(
+ CamelContext camelContext, String serviceAccountKey,
Collection<String> scopes, String applicationName,
+ String delegate) {
+ created.add(delegate);
+ return newClient();
+ }
+
+ private static Gmail newClient() {
+ return new Gmail.Builder(new NetHttpTransport(),
GsonFactory.getDefaultInstance(), null)
+ .setApplicationName("test").build();
+ }
+ }
+}
diff --git
a/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsComponent.java
b/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsComponent.java
index 463946c21276..f3a06901580f 100644
---
a/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsComponent.java
+++
b/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsComponent.java
@@ -33,8 +33,6 @@ public class GoogleSheetsComponent
@Metadata
GoogleSheetsConfiguration configuration;
@Metadata(label = "advanced")
- private Sheets client;
- @Metadata(label = "advanced")
private GoogleSheetsClientFactory clientFactory;
public GoogleSheetsComponent() {
@@ -50,20 +48,20 @@ public class GoogleSheetsComponent
return
getCamelContext().getTypeConverter().convertTo(GoogleSheetsApiName.class,
apiNameStr);
}
+ /**
+ * Creates a client for the given configuration with the client factory of
the component. Each endpoint creates its
+ * own client from its own options and keeps it for its lifetime.
+ */
public Sheets getClient(GoogleSheetsConfiguration config) {
- if (client == null) {
- if (config.getClientId() != null && !config.getClientId().isBlank()
- && config.getClientSecret() != null &&
!config.getClientSecret().isBlank()) {
- client = getClientFactory().makeClient(config.getClientId(),
- config.getClientSecret(), config.getScopesAsList(),
- config.getApplicationName(), config.getRefreshToken(),
config.getAccessToken());
- } else {
- // Use service account key or fall back to Application Default
Credentials
- client = getClientFactory().makeClient(getCamelContext(),
config.getServiceAccountKey(),
- config.getScopesAsList(), config.getApplicationName(),
config.getDelegate());
- }
+ if (config.getClientId() != null && !config.getClientId().isBlank()
+ && config.getClientSecret() != null &&
!config.getClientSecret().isBlank()) {
+ return getClientFactory().makeClient(config.getClientId(),
+ config.getClientSecret(), config.getScopesAsList(),
+ config.getApplicationName(), config.getRefreshToken(),
config.getAccessToken());
}
- return client;
+ // Use service account key or fall back to Application Default
Credentials
+ return getClientFactory().makeClient(getCamelContext(),
config.getServiceAccountKey(),
+ config.getScopesAsList(), config.getApplicationName(),
config.getDelegate());
}
public GoogleSheetsClientFactory getClientFactory() {
diff --git
a/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsEndpoint.java
b/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsEndpoint.java
index e8b3065dc7e2..334cce5fdcd8 100644
---
a/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsEndpoint.java
+++
b/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsEndpoint.java
@@ -17,6 +17,8 @@
package org.apache.camel.component.google.sheets;
import java.util.Map;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
import com.google.api.services.sheets.v4.Sheets;
import org.apache.camel.Category;
@@ -46,6 +48,8 @@ public class GoogleSheetsEndpoint extends
AbstractApiEndpoint<GoogleSheetsApiNam
@UriParam
private GoogleSheetsConfiguration configuration;
+ private final Lock clientLock = new ReentrantLock();
+ private volatile Sheets client;
private Object apiProxy;
@@ -96,8 +100,24 @@ public class GoogleSheetsEndpoint extends
AbstractApiEndpoint<GoogleSheetsApiNam
}
}
+ /**
+ * The client of this endpoint, created on first use from the options of
this endpoint and kept for the lifetime of
+ * the endpoint.
+ */
public Sheets getClient() {
- return ((GoogleSheetsComponent)
getComponent()).getClient(configuration);
+ Sheets answer = client;
+ if (answer == null) {
+ clientLock.lock();
+ try {
+ if (client == null) {
+ client = ((GoogleSheetsComponent)
getComponent()).getClient(configuration);
+ }
+ answer = client;
+ } finally {
+ clientLock.unlock();
+ }
+ }
+ return answer;
}
@Override
diff --git
a/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamComponent.java
b/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamComponent.java
index 9a0dfbf74030..ab7ac9e58fa4 100644
---
a/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamComponent.java
+++
b/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamComponent.java
@@ -34,8 +34,6 @@ public class GoogleSheetsStreamComponent extends
HealthCheckComponent {
@Metadata
private GoogleSheetsStreamConfiguration configuration;
@Metadata(label = "advanced")
- private Sheets client;
- @Metadata(label = "advanced")
private GoogleSheetsClientFactory clientFactory;
public GoogleSheetsStreamComponent() {
@@ -47,19 +45,19 @@ public class GoogleSheetsStreamComponent extends
HealthCheckComponent {
this.configuration = new GoogleSheetsStreamConfiguration();
}
+ /**
+ * Creates a client for the given configuration with the client factory of
the component. Each endpoint creates its
+ * own client from its own options and keeps it for its lifetime.
+ */
public Sheets getClient(GoogleSheetsStreamConfiguration config) {
- if (client == null) {
- if (config.getClientId() != null && config.getClientSecret() !=
null) {
- client = getClientFactory().makeClient(config.getClientId(),
- config.getClientSecret(), config.getScopesAsList(),
- config.getApplicationName(), config.getRefreshToken(),
config.getAccessToken());
- } else {
- // Use service account key or fall back to Application Default
Credentials
- client = getClientFactory().makeClient(getCamelContext(),
config.getServiceAccountKey(),
- config.getScopesAsList(), config.getApplicationName(),
config.getDelegate());
- }
+ if (config.getClientId() != null && config.getClientSecret() != null) {
+ return getClientFactory().makeClient(config.getClientId(),
+ config.getClientSecret(), config.getScopesAsList(),
+ config.getApplicationName(), config.getRefreshToken(),
config.getAccessToken());
}
- return client;
+ // Use service account key or fall back to Application Default
Credentials
+ return getClientFactory().makeClient(getCamelContext(),
config.getServiceAccountKey(),
+ config.getScopesAsList(), config.getApplicationName(),
config.getDelegate());
}
public GoogleSheetsClientFactory getClientFactory() {
diff --git
a/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamEndpoint.java
b/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamEndpoint.java
index dced6726c989..ed0d15bb6fe0 100644
---
a/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamEndpoint.java
+++
b/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamEndpoint.java
@@ -16,6 +16,9 @@
*/
package org.apache.camel.component.google.sheets.stream;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
import com.google.api.services.sheets.v4.Sheets;
import org.apache.camel.Category;
import org.apache.camel.Consumer;
@@ -41,6 +44,8 @@ public class GoogleSheetsStreamEndpoint extends
ScheduledPollEndpoint implements
@UriParam
private GoogleSheetsStreamConfiguration configuration;
+ private final Lock clientLock = new ReentrantLock();
+ private volatile Sheets client;
public GoogleSheetsStreamEndpoint(String uri, GoogleSheetsStreamComponent
component,
GoogleSheetsStreamConfiguration
endpointConfiguration) {
@@ -60,8 +65,24 @@ public class GoogleSheetsStreamEndpoint extends
ScheduledPollEndpoint implements
return consumer;
}
+ /**
+ * The client of this endpoint, created on first use from the options of
this endpoint and kept for the lifetime of
+ * the endpoint.
+ */
public Sheets getClient() {
- return ((GoogleSheetsStreamComponent)
getComponent()).getClient(configuration);
+ Sheets answer = client;
+ if (answer == null) {
+ clientLock.lock();
+ try {
+ if (client == null) {
+ client = ((GoogleSheetsStreamComponent)
getComponent()).getClient(configuration);
+ }
+ answer = client;
+ } finally {
+ clientLock.unlock();
+ }
+ }
+ return answer;
}
public GoogleSheetsClientFactory getClientFactory() {
diff --git
a/components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/GoogleSheetsClientPerEndpointOptionsTest.java
b/components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/GoogleSheetsClientPerEndpointOptionsTest.java
new file mode 100644
index 000000000000..7f66f3feec15
--- /dev/null
+++
b/components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/GoogleSheetsClientPerEndpointOptionsTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.sheets;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.services.sheets.v4.Sheets;
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+class GoogleSheetsClientPerEndpointOptionsTest {
+
+ private static final String OAUTH =
"?clientId=id&clientSecret=secret&refreshToken=";
+
+ @Test
+ void shouldCreateOneClientPerEndpoint() throws Exception {
+ RecordingClientFactory factory = new RecordingClientFactory();
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.getComponent("google-sheets",
GoogleSheetsComponent.class).setClientFactory(factory);
+ context.start();
+
+ GoogleSheetsEndpoint first =
context.getEndpoint("google-sheets://spreadsheets/get" + OAUTH + "token-a",
+ GoogleSheetsEndpoint.class);
+ GoogleSheetsEndpoint second =
context.getEndpoint("google-sheets://spreadsheets/get" + OAUTH + "token-b",
+ GoogleSheetsEndpoint.class);
+ GoogleSheetsEndpoint sameAsFirst = context.getEndpoint(
+ "google-sheets://spreadsheets/create" + OAUTH + "token-a",
GoogleSheetsEndpoint.class);
+
+ assertEquals(Arrays.asList("token-a", "token-b", "token-a"),
factory.created);
+ assertNotSame(first.getClient(), second.getClient());
+ assertNotSame(first.getClient(), sameAsFirst.getClient());
+ assertSame(first.getClient(), first.getClient());
+ assertEquals(3, factory.created.size());
+ }
+ }
+
+ @Test
+ void shouldNotShareTheDefaultCredentialsClient() throws Exception {
+ RecordingClientFactory factory = new RecordingClientFactory();
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.getComponent("google-sheets",
GoogleSheetsComponent.class).setClientFactory(factory);
+ context.start();
+
+ GoogleSheetsEndpoint defaultCredentials
+ = context.getEndpoint("google-sheets://spreadsheets/get",
GoogleSheetsEndpoint.class);
+ GoogleSheetsEndpoint explicitCredentials = context.getEndpoint(
+ "google-sheets://spreadsheets/get" + OAUTH + "token-b",
GoogleSheetsEndpoint.class);
+
+ assertEquals(Arrays.asList(null, "token-b"), factory.created);
+ assertNotSame(defaultCredentials.getClient(),
explicitCredentials.getClient());
+ }
+ }
+
+ private static final class RecordingClientFactory implements
GoogleSheetsClientFactory {
+
+ private final List<String> created = new ArrayList<>();
+
+ @Override
+ public Sheets makeClient(
+ String clientId, String clientSecret, Collection<String>
scopes, String applicationName,
+ String refreshToken, String accessToken) {
+ created.add(refreshToken);
+ return newClient();
+ }
+
+ @Override
+ public Sheets makeClient(
+ CamelContext camelContext, String serviceAccountKey,
Collection<String> scopes, String applicationName,
+ String delegate) {
+ created.add(delegate);
+ return newClient();
+ }
+
+ private static Sheets newClient() {
+ return new Sheets.Builder(new NetHttpTransport(), new
JacksonFactory(), null).setApplicationName("test")
+ .build();
+ }
+ }
+}
diff --git
a/components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamClientPerEndpointOptionsTest.java
b/components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamClientPerEndpointOptionsTest.java
new file mode 100644
index 000000000000..c8039e19f7db
--- /dev/null
+++
b/components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamClientPerEndpointOptionsTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.sheets.stream;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.services.sheets.v4.Sheets;
+import org.apache.camel.CamelContext;
+import org.apache.camel.component.google.sheets.GoogleSheetsClientFactory;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+class GoogleSheetsStreamClientPerEndpointOptionsTest {
+
+ private static final String OAUTH =
"?clientId=id&clientSecret=secret&refreshToken=";
+ private static final String SERVICE_ACCOUNT =
"?serviceAccountKey=classpath:service-account.json&delegate=";
+
+ @Test
+ void shouldCreateOneClientPerEndpoint() throws Exception {
+ RecordingClientFactory factory = new RecordingClientFactory();
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.getComponent("google-sheets-stream",
GoogleSheetsStreamComponent.class).setClientFactory(factory);
+ context.start();
+
+ GoogleSheetsStreamEndpoint first =
context.getEndpoint("google-sheets-stream://sheet-a" + OAUTH + "token-a",
+ GoogleSheetsStreamEndpoint.class);
+ GoogleSheetsStreamEndpoint second =
context.getEndpoint("google-sheets-stream://sheet-b" + OAUTH + "token-b",
+ GoogleSheetsStreamEndpoint.class);
+ GoogleSheetsStreamEndpoint sameAsFirst = context.getEndpoint(
+ "google-sheets-stream://sheet-c" + OAUTH + "token-a",
GoogleSheetsStreamEndpoint.class);
+
+ Sheets firstClient = first.getClient();
+ Sheets secondClient = second.getClient();
+
+ assertNotSame(firstClient, secondClient);
+ assertNotSame(firstClient, sameAsFirst.getClient());
+ assertSame(firstClient, first.getClient());
+ assertEquals(Arrays.asList("token-a", "token-b", "token-a"),
factory.created);
+ }
+ }
+
+ @Test
+ void shouldKeepOneClientPerDelegate() throws Exception {
+ RecordingClientFactory factory = new RecordingClientFactory();
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.getComponent("google-sheets-stream",
GoogleSheetsStreamComponent.class).setClientFactory(factory);
+ context.start();
+
+ GoogleSheetsStreamEndpoint first
+ = context.getEndpoint("google-sheets-stream://sheet-a" +
SERVICE_ACCOUNT + "user-a",
+ GoogleSheetsStreamEndpoint.class);
+ GoogleSheetsStreamEndpoint second
+ = context.getEndpoint("google-sheets-stream://sheet-b" +
SERVICE_ACCOUNT + "user-b",
+ GoogleSheetsStreamEndpoint.class);
+
+ assertNotSame(first.getClient(), second.getClient());
+ assertEquals(Arrays.asList("user-a", "user-b"), factory.created);
+ }
+ }
+
+ private static final class RecordingClientFactory implements
GoogleSheetsClientFactory {
+
+ private final List<String> created = new ArrayList<>();
+
+ @Override
+ public Sheets makeClient(
+ String clientId, String clientSecret, Collection<String>
scopes, String applicationName,
+ String refreshToken, String accessToken) {
+ created.add(refreshToken);
+ return newClient();
+ }
+
+ @Override
+ public Sheets makeClient(
+ CamelContext camelContext, String serviceAccountKey,
Collection<String> scopes, String applicationName,
+ String delegate) {
+ created.add(delegate);
+ return newClient();
+ }
+
+ private static Sheets newClient() {
+ return new Sheets.Builder(new NetHttpTransport(), new
JacksonFactory(), null).setApplicationName("test")
+ .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 eb7e7c67263c..52f22d3093dc 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
@@ -232,6 +232,21 @@ a JSON object when none is configured). A route that sets
the header must enable
`.unmarshal(new JsonDataFormat(JsonLibrary.Gson).allowUnmarshallType(true))` or
`<json library="Gson" allowUnmarshallType="true"/>`.
+=== camel-google-mail, camel-google-drive, camel-google-calendar,
camel-google-sheets - one API client per endpoint
+
+The `google-mail`, `google-mail-stream`, `google-drive`, `google-calendar`,
`google-calendar-stream`, `google-sheets`
+and `google-sheets-stream` components now create one API client per endpoint,
from the options of that endpoint
+(`clientId`, `clientSecret`, `refreshToken`, `accessToken`,
`serviceAccountKey`, `delegate`, `scopes` and
+`applicationName`, plus `emailAddress`, `p12FileName` and `user` on the two
calendar components), instead of a single
+client per component that every endpoint of the component shared. Each
endpoint keeps its client for its lifetime. The
+`clientFactory` option of a `google-drive` endpoint is now used to create the
client of that endpoint. In addition, a
+`google-mail-stream` endpoint configured with neither `clientSecret` nor
`serviceAccountKey` now fails with an
+`IllegalArgumentException` when its client is created, instead of getting a
`null` client and failing at the first API
+call. The `google-mail:update-message-labels` data type transformer now uses a
client created from the component-level
+configuration (the `camel.component.google-mail.*` options), kept until the
component stops. Before, it used whichever
+client the component had already created, for example the one of a
`google-mail:messages/modify` endpoint, so
+credentials set only on an endpoint URI no longer reach the transformer: set
them on the component as well.
+
=== camel-jbang - camel validate and camel overview report the same endpoints
no route consumes
`camel validate source` and `camel overview` now use the same rules for a
`direct:`, `seda:` or `disruptor:` endpoint