From 989ddb489fb94cf6cdea71f8f6c366100c6e6905 Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Wed, 21 Oct 2026 10:19:12 +0200 Subject: [PATCH 1/6] CAMEL-25889: camel-google-mail/drive/calendar/sheets - create the API client per endpoint credentials instead of one per component The google-mail, google-mail-stream, google-drive, google-calendar, google-calendar-stream, google-sheets and google-sheets-stream components kept a single API client per component, created from the configuration of the first endpoint that asked for one and returned to every later endpoint whatever its own options were. Each component now keeps one client per distinct set of the options the client is created from (clientId, clientSecret, refreshToken, accessToken, serviceAccountKey, delegate, scopes, applicationName, and emailAddress, p12FileName and user on the calendar components), in a thread-safe map that is cleared when the component stops. Endpoints with the same options keep sharing a client; endpoints with different options get their own. The google-drive component also honours the endpoint-level clientFactory option, which it ignored before, and google-mail-stream now fails with a clear message instead of returning a null client when no credentials are configured. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Andrea Cosentino --- .../calendar/GoogleCalendarComponent.java | 56 +++++++--- .../stream/GoogleCalendarStreamComponent.java | 52 ++++++--- ...eCalendarClientPerEndpointOptionsTest.java | 102 +++++++++++++++++ ...darStreamClientPerEndpointOptionsTest.java | 88 +++++++++++++++ .../google/drive/GoogleDriveComponent.java | 65 ++++++++--- .../google/drive/GoogleDriveEndpoint.java | 2 +- ...ogleDriveClientPerEndpointOptionsTest.java | 105 ++++++++++++++++++ .../google/mail/GoogleMailComponent.java | 54 ++++++--- .../stream/GoogleMailStreamComponent.java | 53 ++++++--- ...oogleMailClientPerEndpointOptionsTest.java | 102 +++++++++++++++++ ...ailStreamClientPerEndpointOptionsTest.java | 88 +++++++++++++++ .../google/sheets/GoogleSheetsComponent.java | 49 +++++--- .../stream/GoogleSheetsStreamComponent.java | 45 +++++--- ...gleSheetsClientPerEndpointOptionsTest.java | 101 +++++++++++++++++ ...etsStreamClientPerEndpointOptionsTest.java | 88 +++++++++++++++ .../pages/camel-4x-upgrade-guide-4_24.adoc | 10 ++ 16 files changed, 952 insertions(+), 108 deletions(-) create mode 100644 components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/GoogleCalendarClientPerEndpointOptionsTest.java create mode 100644 components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamClientPerEndpointOptionsTest.java create mode 100644 components/camel-google/camel-google-drive/src/test/java/org/apache/camel/component/google/drive/GoogleDriveClientPerEndpointOptionsTest.java create mode 100644 components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/GoogleMailClientPerEndpointOptionsTest.java create mode 100644 components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamClientPerEndpointOptionsTest.java create mode 100644 components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/GoogleSheetsClientPerEndpointOptionsTest.java create mode 100644 components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamClientPerEndpointOptionsTest.java 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 902a6fc685344..15a9bea49a927 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 @@ -16,6 +16,11 @@ */ package org.apache.camel.component.google.calendar; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + import com.google.api.services.calendar.Calendar; import org.apache.camel.CamelContext; import org.apache.camel.Endpoint; @@ -32,9 +37,8 @@ public class GoogleCalendarComponent @Metadata GoogleCalendarConfiguration configuration; @Metadata(label = "advanced") - private Calendar client; - @Metadata(label = "advanced") private GoogleCalendarClientFactory clientFactory; + private final Map, Calendar> clients = new ConcurrentHashMap<>(); public GoogleCalendarComponent() { super(GoogleCalendarApiName.class, GoogleCalendarApiCollection.getCollection()); @@ -49,23 +53,35 @@ protected GoogleCalendarApiName getApiName(String apiNameStr) { return getCamelContext().getTypeConverter().convertTo(GoogleCalendarApiName.class, apiNameStr); } + /** + * Gets the client for the given configuration. One client is kept per distinct set of the options the client is + * created from, so endpoints with the same options share a client and endpoints with different options get their + * own. + */ 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"); - } + return clients.computeIfAbsent(clientKey(config), key -> createClient(config)); + } + + private Calendar createClient(GoogleCalendarConfiguration 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(), 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; + } + + private static List clientKey(GoogleCalendarConfiguration config) { + return Arrays.asList(config.getClientId(), config.getClientSecret(), config.getRefreshToken(), + config.getAccessToken(), config.getServiceAccountKey(), config.getDelegate(), config.getScopes(), + config.getApplicationName(), config.getEmailAddress(), config.getP12FileName(), config.getUser()); } public GoogleCalendarClientFactory getClientFactory() { @@ -107,4 +123,10 @@ protected Endpoint createEndpoint( endpointConfiguration.setMethodName(methodName); return new GoogleCalendarEndpoint(uri, this, apiName, methodName, endpointConfiguration); } + + @Override + protected void doStop() throws Exception { + clients.clear(); + super.doStop(); + } } 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 e02ccb129489f..d3f7bcd59ffd6 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 @@ -16,7 +16,10 @@ */ package org.apache.camel.component.google.calendar.stream; +import java.util.Arrays; +import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import com.google.api.services.calendar.Calendar; import org.apache.camel.CamelContext; @@ -30,12 +33,11 @@ @Component("google-calendar-stream") public class GoogleCalendarStreamComponent extends HealthCheckComponent { - @Metadata(label = "advanced") - private Calendar client; @Metadata(label = "advanced") private GoogleCalendarClientFactory clientFactory; @Metadata private GoogleCalendarStreamConfiguration configuration; + private final Map, Calendar> clients = new ConcurrentHashMap<>(); public GoogleCalendarStreamComponent() { this(null); @@ -47,22 +49,34 @@ public GoogleCalendarStreamComponent(CamelContext context) { this.configuration = new GoogleCalendarStreamConfiguration(); } + /** + * Gets the client for the given configuration. One client is kept per distinct set of the options the client is + * created from, so endpoints with the same options share a client and endpoints with different options get their + * own. + */ 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"); - } + return clients.computeIfAbsent(clientKey(config), key -> createClient(config)); + } + + private Calendar createClient(GoogleCalendarStreamConfiguration config) { + 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; + } + + private static List clientKey(GoogleCalendarStreamConfiguration config) { + return Arrays.asList(config.getClientId(), config.getClientSecret(), config.getRefreshToken(), + config.getAccessToken(), config.getServiceAccountKey(), config.getDelegate(), config.getScopes(), + config.getApplicationName(), config.getEmailAddress(), config.getP12FileName(), config.getUser()); } /** @@ -97,4 +111,10 @@ protected Endpoint createEndpoint(String uri, String remaining, Map created = new ArrayList<>(); + + @Override + public Calendar makeClient( + String clientId, String clientSecret, Collection 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 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 0000000000000..ba2b2af879e11 --- /dev/null +++ b/components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamClientPerEndpointOptionsTest.java @@ -0,0 +1,88 @@ +/* + * 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="; + + @Test + void shouldKeepOneClientPerDistinctOptions() 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(); + + assertSame(firstClient, sameAsFirst.getClient()); + assertNotSame(firstClient, secondClient); + assertEquals(Arrays.asList("token-a", "token-b"), factory.created); + } + } + + private static final class RecordingClientFactory implements GoogleCalendarClientFactory { + + private final List created = new ArrayList<>(); + + @Override + public Calendar makeClient( + String clientId, String clientSecret, Collection 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 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 74d14d0103552..4962890108aff 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 @@ -16,6 +16,11 @@ */ package org.apache.camel.component.google.drive; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + import com.google.api.services.drive.Drive; import org.apache.camel.CamelContext; import org.apache.camel.Endpoint; @@ -37,13 +42,12 @@ 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; @Metadata(label = "proxy", description = "Proxy server port") private Integer proxyPort; + private final Map, Drive> clients = new ConcurrentHashMap<>(); public GoogleDriveComponent() { super(GoogleDriveApiName.class, GoogleDriveApiCollection.getCollection()); @@ -58,22 +62,45 @@ protected GoogleDriveApiName getApiName(String apiNameStr) { return getCamelContext().getTypeConverter().convertTo(GoogleDriveApiName.class, apiNameStr); } + /** + * Gets the client for the given configuration, created by 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); + } + + /** + * Gets the client for the given configuration, created by the given client factory or, when that is null, by the + * client factory of the component. One client is kept per client factory and distinct set of the options the client + * is created from, so endpoints with the same options share a client and endpoints with different options get their + * own. + */ + public Drive getClient(GoogleDriveConfiguration config, GoogleDriveClientFactory factory) { + GoogleDriveClientFactory factoryToUse = factory != null ? factory : getClientFactory(); + return clients.computeIfAbsent(clientKey(config, factoryToUse), key -> createClient(config, factoryToUse)); + } + + private Drive createClient(GoogleDriveConfiguration config, GoogleDriveClientFactory factory) { + if (config.getClientId() != null && !config.getClientId().isBlank() + && config.getClientSecret() != null && !config.getClientSecret().isBlank()) { + return factory.makeClient(config.getClientId(), + config.getClientSecret(), config.getScopesAsList(), + config.getApplicationName(), config.getRefreshToken(), config.getAccessToken()); + } else if (config.getServiceAccountKey() != null && !config.getServiceAccountKey().isBlank()) { + return factory.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; + } + + private static List clientKey(GoogleDriveConfiguration config, GoogleDriveClientFactory factory) { + return Arrays.asList(factory, config.getClientId(), config.getClientSecret(), config.getRefreshToken(), + config.getAccessToken(), config.getServiceAccountKey(), config.getDelegate(), config.getScopes(), + config.getApplicationName()); } public GoogleDriveClientFactory getClientFactory() { @@ -149,4 +176,10 @@ protected Endpoint createEndpoint( endpoint.setClientFactory(clientFactory); return endpoint; } + + @Override + protected void doStop() throws Exception { + clients.clear(); + super.doStop(); + } } 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 9a428f77400bc..dc3ad4316397c 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 @@ -121,7 +121,7 @@ protected void afterConfigureProperties() { } public Drive getClient() { - return ((GoogleDriveComponent) getComponent()).getClient(configuration); + return ((GoogleDriveComponent) getComponent()).getClient(configuration, clientFactory); } @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 0000000000000..4b6a24ee97651 --- /dev/null +++ b/components/camel-google/camel-google-drive/src/test/java/org/apache/camel/component/google/drive/GoogleDriveClientPerEndpointOptionsTest.java @@ -0,0 +1,105 @@ +/* + * 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 shouldKeepOneClientPerDistinctOptions() 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"), factory.created); + assertSame(first.getClient(), sameAsFirst.getClient()); + assertNotSame(first.getClient(), second.getClient()); + } + } + + @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 created = new ArrayList<>(); + + @Override + public Drive makeClient( + String clientId, String clientSecret, Collection scopes, String applicationName, + String refreshToken, String accessToken) { + created.add(refreshToken); + return newClient(); + } + + @Override + public Drive makeClient( + CamelContext camelContext, String serviceAccountKey, Collection 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/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 10771cc95b0ab..2b3c5c29d7eff 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,11 @@ */ package org.apache.camel.component.google.mail; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + import com.google.api.services.gmail.Gmail; import org.apache.camel.CamelContext; import org.apache.camel.Endpoint; @@ -32,9 +37,8 @@ public class GoogleMailComponent @Metadata GoogleMailConfiguration configuration; @Metadata(label = "advanced") - private Gmail client; - @Metadata(label = "advanced") private GoogleMailClientFactory clientFactory; + private final Map, Gmail> clients = new ConcurrentHashMap<>(); public GoogleMailComponent() { super(GoogleMailApiName.class, GoogleMailApiCollection.getCollection()); @@ -49,22 +53,34 @@ protected GoogleMailApiName getApiName(String apiNameStr) { return getCamelContext().getTypeConverter().convertTo(GoogleMailApiName.class, apiNameStr); } + /** + * Gets the client for the given configuration. One client is kept per distinct set of the options the client is + * created from, so endpoints with the same options share a client and endpoints with different options get their + * own. + */ 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"); - } + return clients.computeIfAbsent(clientKey(config), key -> createClient(config)); + } + + private Gmail createClient(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"); } - return client; + } + + private static List clientKey(GoogleMailConfiguration config) { + return Arrays.asList(config.getClientId(), config.getClientSecret(), config.getRefreshToken(), + config.getAccessToken(), config.getServiceAccountKey(), config.getDelegate(), config.getScopes(), + config.getApplicationName()); } public GoogleMailClientFactory getClientFactory() { @@ -105,4 +121,10 @@ protected Endpoint createEndpoint( endpointConfiguration.setMethodName(methodName); return new GoogleMailEndpoint(uri, this, apiName, methodName, endpointConfiguration); } + + @Override + protected void doStop() throws Exception { + clients.clear(); + super.doStop(); + } } 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 b34760b7621fd..0f9dc560d6b1e 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 @@ -16,7 +16,10 @@ */ package org.apache.camel.component.google.mail.stream; +import java.util.Arrays; +import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import com.google.api.services.gmail.Gmail; import org.apache.camel.CamelContext; @@ -33,12 +36,11 @@ @Component("google-mail-stream") public class GoogleMailStreamComponent extends HealthCheckComponent { - @Metadata(label = "advanced") - private Gmail client; @Metadata(label = "advanced") private GoogleMailClientFactory clientFactory; @Metadata(label = "advanced") private GoogleMailStreamConfiguration configuration; + private final Map, Gmail> clients = new ConcurrentHashMap<>(); public GoogleMailStreamComponent() { this(null); @@ -49,20 +51,35 @@ public GoogleMailStreamComponent(CamelContext context) { 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()); - } + /** + * Gets the client for the given configuration. One client is kept per distinct set of the options the client is + * created from, so endpoints with the same options share a client and endpoints with different options get their + * own. + */ + public Gmail getClient(GoogleMailStreamConfiguration config) { + return clients.computeIfAbsent(clientKey(config), key -> createClient(config)); + } + + private Gmail createClient(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; + } + + private static List clientKey(GoogleMailStreamConfiguration config) { + return Arrays.asList(config.getClientId(), config.getClientSecret(), config.getRefreshToken(), + config.getAccessToken(), config.getServiceAccountKey(), config.getDelegate(), config.getScopes(), + config.getApplicationName()); } /** @@ -98,4 +115,10 @@ protected Endpoint createEndpoint(String uri, String remaining, Map created = new ArrayList<>(); + + @Override + public Gmail makeClient( + String clientId, String clientSecret, Collection scopes, String applicationName, + String refreshToken, String accessToken) { + created.add(refreshToken); + return newClient(); + } + + @Override + public Gmail makeClient( + CamelContext camelContext, String serviceAccountKey, Collection 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 0000000000000..8c149198e4007 --- /dev/null +++ b/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamClientPerEndpointOptionsTest.java @@ -0,0 +1,88 @@ +/* + * 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; + +class GoogleMailStreamClientPerEndpointOptionsTest { + + private static final String OAUTH = "?clientId=id&clientSecret=secret&refreshToken="; + + @Test + void shouldKeepOneClientPerDistinctOptions() 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(); + + assertSame(firstClient, sameAsFirst.getClient()); + assertNotSame(firstClient, secondClient); + assertEquals(Arrays.asList("token-a", "token-b"), factory.created); + } + } + + private static final class RecordingClientFactory implements GoogleMailClientFactory { + + private final List created = new ArrayList<>(); + + @Override + public Gmail makeClient( + String clientId, String clientSecret, Collection scopes, String applicationName, + String refreshToken, String accessToken) { + created.add(refreshToken); + return newClient(); + } + + @Override + public Gmail makeClient( + CamelContext camelContext, String serviceAccountKey, Collection 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 463946c21276e..f2e8002861fc3 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 @@ -16,6 +16,11 @@ */ package org.apache.camel.component.google.sheets; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + import com.google.api.services.sheets.v4.Sheets; import org.apache.camel.CamelContext; import org.apache.camel.Endpoint; @@ -33,9 +38,8 @@ public class GoogleSheetsComponent @Metadata GoogleSheetsConfiguration configuration; @Metadata(label = "advanced") - private Sheets client; - @Metadata(label = "advanced") private GoogleSheetsClientFactory clientFactory; + private final Map, Sheets> clients = new ConcurrentHashMap<>(); public GoogleSheetsComponent() { super(GoogleSheetsApiName.class, GoogleSheetsApiCollection.getCollection()); @@ -50,20 +54,31 @@ protected GoogleSheetsApiName getApiName(String apiNameStr) { return getCamelContext().getTypeConverter().convertTo(GoogleSheetsApiName.class, apiNameStr); } + /** + * Gets the client for the given configuration. One client is kept per distinct set of the options the client is + * created from, so endpoints with the same options share a client and endpoints with different options get their + * own. + */ 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()); - } + return clients.computeIfAbsent(clientKey(config), key -> createClient(config)); + } + + private Sheets createClient(GoogleSheetsConfiguration 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()); } - 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()); + } + + private static List clientKey(GoogleSheetsConfiguration config) { + return Arrays.asList(config.getClientId(), config.getClientSecret(), config.getRefreshToken(), + config.getAccessToken(), config.getServiceAccountKey(), config.getDelegate(), config.getScopes(), + config.getApplicationName()); } public GoogleSheetsClientFactory getClientFactory() { @@ -105,4 +120,10 @@ protected Endpoint createEndpoint( endpointConfiguration.setMethodName(methodName); return new GoogleSheetsEndpoint(uri, this, apiName, methodName, endpointConfiguration); } + + @Override + protected void doStop() throws Exception { + clients.clear(); + super.doStop(); + } } 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 9a0dfbf740308..a338bc518ced8 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 @@ -16,7 +16,10 @@ */ package org.apache.camel.component.google.sheets.stream; +import java.util.Arrays; +import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import com.google.api.services.sheets.v4.Sheets; import org.apache.camel.CamelContext; @@ -34,9 +37,8 @@ public class GoogleSheetsStreamComponent extends HealthCheckComponent { @Metadata private GoogleSheetsStreamConfiguration configuration; @Metadata(label = "advanced") - private Sheets client; - @Metadata(label = "advanced") private GoogleSheetsClientFactory clientFactory; + private final Map, Sheets> clients = new ConcurrentHashMap<>(); public GoogleSheetsStreamComponent() { this(null); @@ -47,19 +49,30 @@ public GoogleSheetsStreamComponent(CamelContext context) { this.configuration = new GoogleSheetsStreamConfiguration(); } + /** + * Gets the client for the given configuration. One client is kept per distinct set of the options the client is + * created from, so endpoints with the same options share a client and endpoints with different options get their + * own. + */ 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()); - } + return clients.computeIfAbsent(clientKey(config), key -> createClient(config)); + } + + private Sheets createClient(GoogleSheetsStreamConfiguration config) { + 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()); + } + + private static List clientKey(GoogleSheetsStreamConfiguration config) { + return Arrays.asList(config.getClientId(), config.getClientSecret(), config.getRefreshToken(), + config.getAccessToken(), config.getServiceAccountKey(), config.getDelegate(), config.getScopes(), + config.getApplicationName()); } public GoogleSheetsClientFactory getClientFactory() { @@ -96,4 +109,10 @@ protected Endpoint createEndpoint(String uri, String remaining, Map created = new ArrayList<>(); + + @Override + public Sheets makeClient( + String clientId, String clientSecret, Collection scopes, String applicationName, + String refreshToken, String accessToken) { + created.add(refreshToken); + return newClient(); + } + + @Override + public Sheets makeClient( + CamelContext camelContext, String serviceAccountKey, Collection 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 0000000000000..913aad4d70a0f --- /dev/null +++ b/components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamClientPerEndpointOptionsTest.java @@ -0,0 +1,88 @@ +/* + * 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="; + + @Test + void shouldKeepOneClientPerDistinctOptions() 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(); + + assertSame(firstClient, sameAsFirst.getClient()); + assertNotSame(firstClient, secondClient); + assertEquals(Arrays.asList("token-a", "token-b"), factory.created); + } + } + + private static final class RecordingClientFactory implements GoogleSheetsClientFactory { + + private final List created = new ArrayList<>(); + + @Override + public Sheets makeClient( + String clientId, String clientSecret, Collection scopes, String applicationName, + String refreshToken, String accessToken) { + created.add(refreshToken); + return newClient(); + } + + @Override + public Sheets makeClient( + CamelContext camelContext, String serviceAccountKey, Collection 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 46379cf15944b..1f780cb38fa0f 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,16 @@ a JSON object when none is configured). A route that sets the header must enable `.unmarshal(new JsonDataFormat(JsonLibrary.Gson).allowUnmarshallType(true))` or ``. +=== camel-google-mail, camel-google-drive, camel-google-calendar, camel-google-sheets - one API client per endpoint options + +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 distinct set of the options the client is created from +(`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. Endpoints configured with the same options keep sharing one client; endpoints configured with +different options now get their own client, created from their own options. The `clientFactory` option of a +`google-drive` endpoint is now used to create the client of that endpoint. + === 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 From ef8e88dd8ec50a39db9f418e75cf3bab464aed7b Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Wed, 21 Oct 2026 11:00:15 +0200 Subject: [PATCH 2/6] CAMEL-25889: document the google-mail-stream failure on missing credentials and test the per-delegate clients of the stream components Co-Authored-By: Claude Fable 5.1 Signed-off-by: Andrea Cosentino --- ...darStreamClientPerEndpointOptionsTest.java | 20 +++++++++++++++++++ ...ailStreamClientPerEndpointOptionsTest.java | 18 +++++++++++++++++ ...etsStreamClientPerEndpointOptionsTest.java | 20 +++++++++++++++++++ .../pages/camel-4x-upgrade-guide-4_24.adoc | 4 +++- 4 files changed, 61 insertions(+), 1 deletion(-) 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 index ba2b2af879e11..68ba5230ed420 100644 --- 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 @@ -36,6 +36,7 @@ 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 shouldKeepOneClientPerDistinctOptions() throws Exception { @@ -60,6 +61,25 @@ void shouldKeepOneClientPerDistinctOptions() throws Exception { } } + @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 created = new ArrayList<>(); 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 index 8c149198e4007..c069d466dfce7 100644 --- 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 @@ -36,6 +36,7 @@ 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 shouldKeepOneClientPerDistinctOptions() throws Exception { @@ -60,6 +61,23 @@ void shouldKeepOneClientPerDistinctOptions() throws Exception { } } + @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); + } + } + private static final class RecordingClientFactory implements GoogleMailClientFactory { private final List created = new ArrayList<>(); 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 index 913aad4d70a0f..524836b7cdcdd 100644 --- 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 @@ -36,6 +36,7 @@ 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 shouldKeepOneClientPerDistinctOptions() throws Exception { @@ -60,6 +61,25 @@ void shouldKeepOneClientPerDistinctOptions() throws Exception { } } + @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 created = new ArrayList<>(); 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 1f780cb38fa0f..602eec58775e6 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 @@ -240,7 +240,9 @@ The `google-mail`, `google-mail-stream`, `google-drive`, `google-calendar`, `goo `applicationName`, plus `emailAddress`, `p12FileName` and `user` on the two calendar components), instead of a single client per component. Endpoints configured with the same options keep sharing one client; endpoints configured with different options now get their own client, created from their own options. The `clientFactory` option of a -`google-drive` endpoint is now used to create the client of that endpoint. +`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. === camel-jbang - camel validate and camel overview report the same endpoints no route consumes From 2afbfd4fd8719a7f65d5fb8770dd2dc3f920494b Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Wed, 21 Oct 2026 11:38:26 +0200 Subject: [PATCH 3/6] CAMEL-25889: test that google-mail-stream fails without credentials Co-Authored-By: Claude Fable 5.1 Signed-off-by: Andrea Cosentino --- ...eMailStreamClientPerEndpointOptionsTest.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 index c069d466dfce7..7861294e75657 100644 --- 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 @@ -32,6 +32,8 @@ 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 { @@ -78,6 +80,21 @@ void shouldKeepOneClientPerDelegate() throws Exception { } } + @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 created = new ArrayList<>(); From b153d1dea9a1772953a00d61d2f75a70debbdcf8 Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Wed, 21 Oct 2026 14:38:02 +0200 Subject: [PATCH 4/6] CAMEL-25889: let each endpoint own its client instead of a per-component client map Following review: the client's lifetime now follows the endpoint's, so clients of endpoints created with per-message credentials go away with the endpoint, and the stream consumers no longer look the client up on every poll. The components' getClient(configuration) creates a client for the given configuration. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Andrea Cosentino --- .../calendar/GoogleCalendarComponent.java | 27 ++------------ .../calendar/GoogleCalendarEndpoint.java | 22 +++++++++++- .../stream/GoogleCalendarStreamComponent.java | 25 ++----------- .../stream/GoogleCalendarStreamEndpoint.java | 22 +++++++++++- ...eCalendarClientPerEndpointOptionsTest.java | 8 +++-- ...darStreamClientPerEndpointOptionsTest.java | 7 ++-- .../google/drive/GoogleDriveComponent.java | 35 ++++--------------- .../google/drive/GoogleDriveEndpoint.java | 22 +++++++++++- ...ogleDriveClientPerEndpointOptionsTest.java | 8 +++-- .../google/mail/GoogleMailComponent.java | 27 ++------------ .../google/mail/GoogleMailEndpoint.java | 22 +++++++++++- .../stream/GoogleMailStreamComponent.java | 25 ++----------- .../mail/stream/GoogleMailStreamEndpoint.java | 22 +++++++++++- ...oogleMailClientPerEndpointOptionsTest.java | 8 +++-- ...ailStreamClientPerEndpointOptionsTest.java | 7 ++-- .../google/sheets/GoogleSheetsComponent.java | 27 ++------------ .../google/sheets/GoogleSheetsEndpoint.java | 22 +++++++++++- .../stream/GoogleSheetsStreamComponent.java | 25 ++----------- .../stream/GoogleSheetsStreamEndpoint.java | 23 +++++++++++- ...gleSheetsClientPerEndpointOptionsTest.java | 8 +++-- ...etsStreamClientPerEndpointOptionsTest.java | 7 ++-- .../pages/camel-4x-upgrade-guide-4_24.adoc | 17 +++++---- 22 files changed, 206 insertions(+), 210 deletions(-) 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 15a9bea49a927..960bbdfc263fe 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 @@ -16,11 +16,6 @@ */ package org.apache.camel.component.google.calendar; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - import com.google.api.services.calendar.Calendar; import org.apache.camel.CamelContext; import org.apache.camel.Endpoint; @@ -38,7 +33,6 @@ public class GoogleCalendarComponent GoogleCalendarConfiguration configuration; @Metadata(label = "advanced") private GoogleCalendarClientFactory clientFactory; - private final Map, Calendar> clients = new ConcurrentHashMap<>(); public GoogleCalendarComponent() { super(GoogleCalendarApiName.class, GoogleCalendarApiCollection.getCollection()); @@ -54,15 +48,10 @@ protected GoogleCalendarApiName getApiName(String apiNameStr) { } /** - * Gets the client for the given configuration. One client is kept per distinct set of the options the client is - * created from, so endpoints with the same options share a client and endpoints with different options get their - * own. + * 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) { - return clients.computeIfAbsent(clientKey(config), key -> createClient(config)); - } - - private Calendar createClient(GoogleCalendarConfiguration config) { if (config.getClientId() != null && !config.getClientId().isBlank() && config.getClientSecret() != null && !config.getClientSecret().isBlank()) { return getClientFactory().makeClient(config.getClientId(), config.getClientSecret(), config.getScopesAsList(), @@ -78,12 +67,6 @@ private Calendar createClient(GoogleCalendarConfiguration config) { } } - private static List clientKey(GoogleCalendarConfiguration config) { - return Arrays.asList(config.getClientId(), config.getClientSecret(), config.getRefreshToken(), - config.getAccessToken(), config.getServiceAccountKey(), config.getDelegate(), config.getScopes(), - config.getApplicationName(), config.getEmailAddress(), config.getP12FileName(), config.getUser()); - } - public GoogleCalendarClientFactory getClientFactory() { if (clientFactory == null) { clientFactory = new BatchGoogleCalendarClientFactory(); @@ -123,10 +106,4 @@ protected Endpoint createEndpoint( endpointConfiguration.setMethodName(methodName); return new GoogleCalendarEndpoint(uri, this, apiName, methodName, endpointConfiguration); } - - @Override - protected void doStop() throws Exception { - clients.clear(); - super.doStop(); - } } 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 ce1874bdf5449..18bb41040a40e 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, Calendar> clients = new ConcurrentHashMap<>(); public GoogleCalendarStreamComponent() { this(null); @@ -50,15 +46,10 @@ public GoogleCalendarStreamComponent(CamelContext context) { } /** - * Gets the client for the given configuration. One client is kept per distinct set of the options the client is - * created from, so endpoints with the same options share a client and endpoints with different options get their - * own. + * 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) { - return clients.computeIfAbsent(clientKey(config), key -> createClient(config)); - } - - private Calendar createClient(GoogleCalendarStreamConfiguration config) { if (config.getClientId() != null && config.getClientSecret() != null) { return getClientFactory().makeClient(config.getClientId(), config.getClientSecret(), config.getScopesAsList(), config.getApplicationName(), config.getRefreshToken(), @@ -73,12 +64,6 @@ private Calendar createClient(GoogleCalendarStreamConfiguration config) { } } - private static List clientKey(GoogleCalendarStreamConfiguration config) { - return Arrays.asList(config.getClientId(), config.getClientSecret(), config.getRefreshToken(), - config.getAccessToken(), config.getServiceAccountKey(), config.getDelegate(), config.getScopes(), - config.getApplicationName(), config.getEmailAddress(), config.getP12FileName(), config.getUser()); - } - /** * The client Factory */ @@ -111,10 +96,4 @@ protected Endpoint createEndpoint(String uri, String remaining, Map, Drive> clients = new ConcurrentHashMap<>(); public GoogleDriveComponent() { super(GoogleDriveApiName.class, GoogleDriveApiCollection.getCollection()); @@ -63,7 +57,7 @@ protected GoogleDriveApiName getApiName(String apiNameStr) { } /** - * Gets the client for the given configuration, created by the client factory of the component. + * Creates a client for the given configuration with the client factory of the component. * * @see #getClient(GoogleDriveConfiguration, GoogleDriveClientFactory) */ @@ -72,24 +66,19 @@ public Drive getClient(GoogleDriveConfiguration config) { } /** - * Gets the client for the given configuration, created by the given client factory or, when that is null, by the - * client factory of the component. One client is kept per client factory and distinct set of the options the client - * is created from, so endpoints with the same options share a client and endpoints with different options get their - * own. + * 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(); - return clients.computeIfAbsent(clientKey(config, factoryToUse), key -> createClient(config, factoryToUse)); - } - - private Drive createClient(GoogleDriveConfiguration config, GoogleDriveClientFactory factory) { if (config.getClientId() != null && !config.getClientId().isBlank() && config.getClientSecret() != null && !config.getClientSecret().isBlank()) { - return factory.makeClient(config.getClientId(), + return factoryToUse.makeClient(config.getClientId(), config.getClientSecret(), config.getScopesAsList(), config.getApplicationName(), config.getRefreshToken(), config.getAccessToken()); } else if (config.getServiceAccountKey() != null && !config.getServiceAccountKey().isBlank()) { - return factory.makeClient(getCamelContext(), config.getServiceAccountKey(), + return factoryToUse.makeClient(getCamelContext(), config.getServiceAccountKey(), config.getScopesAsList(), config.getApplicationName(), config.getDelegate()); } else { throw new IllegalArgumentException( @@ -97,12 +86,6 @@ private Drive createClient(GoogleDriveConfiguration config, GoogleDriveClientFac } } - private static List clientKey(GoogleDriveConfiguration config, GoogleDriveClientFactory factory) { - return Arrays.asList(factory, config.getClientId(), config.getClientSecret(), config.getRefreshToken(), - config.getAccessToken(), config.getServiceAccountKey(), config.getDelegate(), config.getScopes(), - config.getApplicationName()); - } - public GoogleDriveClientFactory getClientFactory() { if (clientFactory == null) { // configure https proxy from camelContext @@ -176,10 +159,4 @@ protected Endpoint createEndpoint( endpoint.setClientFactory(clientFactory); return endpoint; } - - @Override - protected void doStop() throws Exception { - clients.clear(); - super.doStop(); - } } 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 dc3ad4316397c..def2ac80d3c50 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, Gmail> clients = new ConcurrentHashMap<>(); public GoogleMailComponent() { super(GoogleMailApiName.class, GoogleMailApiCollection.getCollection()); @@ -54,15 +48,10 @@ protected GoogleMailApiName getApiName(String apiNameStr) { } /** - * Gets the client for the given configuration. One client is kept per distinct set of the options the client is - * created from, so endpoints with the same options share a client and endpoints with different options get their - * own. + * 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) { - return clients.computeIfAbsent(clientKey(config), key -> createClient(config)); - } - - private Gmail createClient(GoogleMailConfiguration config) { if (config.getClientId() != null && !config.getClientId().isBlank() && config.getClientSecret() != null && !config.getClientSecret().isBlank()) { return getClientFactory().makeClient(config.getClientId(), @@ -77,12 +66,6 @@ private Gmail createClient(GoogleMailConfiguration config) { } } - private static List clientKey(GoogleMailConfiguration config) { - return Arrays.asList(config.getClientId(), config.getClientSecret(), config.getRefreshToken(), - config.getAccessToken(), config.getServiceAccountKey(), config.getDelegate(), config.getScopes(), - config.getApplicationName()); - } - public GoogleMailClientFactory getClientFactory() { if (clientFactory == null) { clientFactory = new BatchGoogleMailClientFactory(); @@ -121,10 +104,4 @@ protected Endpoint createEndpoint( endpointConfiguration.setMethodName(methodName); return new GoogleMailEndpoint(uri, this, apiName, methodName, endpointConfiguration); } - - @Override - protected void doStop() throws Exception { - clients.clear(); - 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 979c23067f2e3..91887d07f2317 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, Gmail> clients = new ConcurrentHashMap<>(); public GoogleMailStreamComponent() { this(null); @@ -52,15 +48,10 @@ public GoogleMailStreamComponent(CamelContext context) { } /** - * Gets the client for the given configuration. One client is kept per distinct set of the options the client is - * created from, so endpoints with the same options share a client and endpoints with different options get their - * own. + * 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) { - return clients.computeIfAbsent(clientKey(config), key -> createClient(config)); - } - - private Gmail createClient(GoogleMailStreamConfiguration config) { if (config.getClientSecret() != null) { return getClientFactory().makeClient(config.getClientId(), config.getClientSecret(), config.getScopesAsList(), @@ -76,12 +67,6 @@ private Gmail createClient(GoogleMailStreamConfiguration config) { } } - private static List clientKey(GoogleMailStreamConfiguration config) { - return Arrays.asList(config.getClientId(), config.getClientSecret(), config.getRefreshToken(), - config.getAccessToken(), config.getServiceAccountKey(), config.getDelegate(), config.getScopes(), - config.getApplicationName()); - } - /** * The client Factory */ @@ -115,10 +100,4 @@ protected Endpoint createEndpoint(String uri, String remaining, Map, Sheets> clients = new ConcurrentHashMap<>(); public GoogleSheetsComponent() { super(GoogleSheetsApiName.class, GoogleSheetsApiCollection.getCollection()); @@ -55,15 +49,10 @@ protected GoogleSheetsApiName getApiName(String apiNameStr) { } /** - * Gets the client for the given configuration. One client is kept per distinct set of the options the client is - * created from, so endpoints with the same options share a client and endpoints with different options get their - * own. + * 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) { - return clients.computeIfAbsent(clientKey(config), key -> createClient(config)); - } - - private Sheets createClient(GoogleSheetsConfiguration config) { if (config.getClientId() != null && !config.getClientId().isBlank() && config.getClientSecret() != null && !config.getClientSecret().isBlank()) { return getClientFactory().makeClient(config.getClientId(), @@ -75,12 +64,6 @@ private Sheets createClient(GoogleSheetsConfiguration config) { config.getScopesAsList(), config.getApplicationName(), config.getDelegate()); } - private static List clientKey(GoogleSheetsConfiguration config) { - return Arrays.asList(config.getClientId(), config.getClientSecret(), config.getRefreshToken(), - config.getAccessToken(), config.getServiceAccountKey(), config.getDelegate(), config.getScopes(), - config.getApplicationName()); - } - public GoogleSheetsClientFactory getClientFactory() { if (clientFactory == null) { clientFactory = new BatchGoogleSheetsClientFactory(); @@ -120,10 +103,4 @@ protected Endpoint createEndpoint( endpointConfiguration.setMethodName(methodName); return new GoogleSheetsEndpoint(uri, this, apiName, methodName, endpointConfiguration); } - - @Override - protected void doStop() throws Exception { - clients.clear(); - super.doStop(); - } } 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 e8b3065dc7e2e..334cce5fdcd8c 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, Sheets> clients = new ConcurrentHashMap<>(); public GoogleSheetsStreamComponent() { this(null); @@ -50,15 +46,10 @@ public GoogleSheetsStreamComponent(CamelContext context) { } /** - * Gets the client for the given configuration. One client is kept per distinct set of the options the client is - * created from, so endpoints with the same options share a client and endpoints with different options get their - * own. + * 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) { - return clients.computeIfAbsent(clientKey(config), key -> createClient(config)); - } - - private Sheets createClient(GoogleSheetsStreamConfiguration config) { if (config.getClientId() != null && config.getClientSecret() != null) { return getClientFactory().makeClient(config.getClientId(), config.getClientSecret(), config.getScopesAsList(), @@ -69,12 +60,6 @@ private Sheets createClient(GoogleSheetsStreamConfiguration config) { config.getScopesAsList(), config.getApplicationName(), config.getDelegate()); } - private static List clientKey(GoogleSheetsStreamConfiguration config) { - return Arrays.asList(config.getClientId(), config.getClientSecret(), config.getRefreshToken(), - config.getAccessToken(), config.getServiceAccountKey(), config.getDelegate(), config.getScopes(), - config.getApplicationName()); - } - public GoogleSheetsClientFactory getClientFactory() { if (clientFactory == null) { clientFactory = new BatchGoogleSheetsClientFactory(); @@ -109,10 +94,4 @@ protected Endpoint createEndpoint(String uri, String remaining, Map`. -=== camel-google-mail, camel-google-drive, camel-google-calendar, camel-google-sheets - one API client per endpoint options +=== 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 distinct set of the options the client is created from -(`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. Endpoints configured with the same options keep sharing one client; endpoints configured with -different options now get their own client, created from their own options. 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. +`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. === camel-jbang - camel validate and camel overview report the same endpoints no route consumes From 25fc54e878ee957cd0f85f1e8a98af11c9e3168b Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Wed, 21 Oct 2026 18:59:43 +0200 Subject: [PATCH 5/6] CAMEL-25889: keep one client for the google-mail component configuration for the update-message-labels transformer Following review: the transformer asked the component for a client on every exchange, which created a client per message once the components stopped caching. The component now keeps one lazily created client for its own configuration, released on stop, and the transformer uses it. The upgrade guide notes that the transformer authenticates with the component-level configuration. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Andrea Cosentino --- .../google/mail/GoogleMailComponent.java | 32 +++++++++++++++++++ ...pdateMessageLabelsDataTypeTransformer.java | 2 +- ...oogleMailClientPerEndpointOptionsTest.java | 23 +++++++++++++ .../pages/camel-4x-upgrade-guide-4_24.adoc | 6 +++- 4 files changed, 61 insertions(+), 2 deletions(-) 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 738415e73c97e..009da6c7de626 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; @@ -33,6 +36,8 @@ public class GoogleMailComponent GoogleMailConfiguration configuration; @Metadata(label = "advanced") private GoogleMailClientFactory clientFactory; + private final Lock clientLock = new ReentrantLock(); + private volatile Gmail client; public GoogleMailComponent() { super(GoogleMailApiName.class, GoogleMailApiCollection.getCollection()); @@ -47,6 +52,27 @@ protected GoogleMailApiName getApiName(String apiNameStr) { return getCamelContext().getTypeConverter().convertTo(GoogleMailApiName.class, apiNameStr); } + /** + * 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 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. @@ -104,4 +130,10 @@ protected Endpoint createEndpoint( 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/transform/GoogleMailUpdateMessageLabelsDataTypeTransformer.java b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/transform/GoogleMailUpdateMessageLabelsDataTypeTransformer.java index 6dcf63b8cd705..d09e912281567 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 @@ private Map fetchLabelMap(String userId) throws Exception { 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 index 15915981c9840..4e900ef1150ab 100644 --- 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 @@ -76,6 +76,29 @@ void shouldKeepOneClientPerDelegate() throws Exception { } } + @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 created = new ArrayList<>(); 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 56028b7aa89ca..d4759b1be244b 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 @@ -241,7 +241,11 @@ The `google-mail`, `google-mail-stream`, `google-drive`, `google-calendar`, `goo 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. +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 From 4ba336ec1b519d8c273cbb65ad66b0439f40f868 Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Thu, 22 Oct 2026 09:14:47 +0200 Subject: [PATCH 6/6] CAMEL-25889: document the credentials the update-message-labels transformer uses and re-wrap the upgrade note Co-Authored-By: Claude Fable 5.1 Signed-off-by: Andrea Cosentino --- .../catalog/docs/google-mail-component.adoc | 2 ++ .../src/main/docs/google-mail-component.adoc | 2 ++ .../pages/camel-4x-upgrade-guide-4_24.adoc | 24 +++++++++---------- 3 files changed, 16 insertions(+), 12 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 eeb80e9529856..52dd7ab525233 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-mail/src/main/docs/google-mail-component.adoc b/components/camel-google/camel-google-mail/src/main/docs/google-mail-component.adoc index eeb80e9529856..52dd7ab525233 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/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 d4759b1be244b..79f1faaf929c6 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 @@ -234,18 +234,18 @@ a JSON object when none is configured). A route that sets the header must enable === 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. +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