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-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..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 @@ -32,8 +32,6 @@ public class GoogleCalendarComponent @Metadata GoogleCalendarConfiguration configuration; @Metadata(label = "advanced") - private Calendar client; - @Metadata(label = "advanced") private GoogleCalendarClientFactory clientFactory; public GoogleCalendarComponent() { @@ -49,23 +47,24 @@ protected GoogleCalendarApiName getApiName(String apiNameStr) { return getCamelContext().getTypeConverter().convertTo(GoogleCalendarApiName.class, apiNameStr); } + /** + * Creates a client for the given configuration with the client factory of the component. Each endpoint creates its + * own client from its own options and keeps it for its lifetime. + */ public Calendar getClient(GoogleCalendarConfiguration config) { - if (client == null) { - if (config.getClientId() != null && !config.getClientId().isBlank() - && config.getClientSecret() != null && !config.getClientSecret().isBlank()) { - client = getClientFactory().makeClient(config.getClientId(), config.getClientSecret(), config.getScopesAsList(), - config.getApplicationName(), config.getRefreshToken(), - config.getAccessToken(), config.getEmailAddress(), config.getP12FileName(), config.getUser()); - } else if (config.getServiceAccountKey() != null && !config.getServiceAccountKey().isBlank()) { - client = getClientFactory().makeClient(getCamelContext(), config.getServiceAccountKey(), - config.getScopesAsList(), - config.getApplicationName(), config.getDelegate()); - } else { - throw new IllegalArgumentException( - "(clientId and clientSecret) or serviceAccountKey are required to create Google Calendar client"); - } + if (config.getClientId() != null && !config.getClientId().isBlank() + && config.getClientSecret() != null && !config.getClientSecret().isBlank()) { + return getClientFactory().makeClient(config.getClientId(), config.getClientSecret(), config.getScopesAsList(), + config.getApplicationName(), config.getRefreshToken(), + config.getAccessToken(), config.getEmailAddress(), config.getP12FileName(), config.getUser()); + } else if (config.getServiceAccountKey() != null && !config.getServiceAccountKey().isBlank()) { + return getClientFactory().makeClient(getCamelContext(), config.getServiceAccountKey(), + config.getScopesAsList(), + config.getApplicationName(), config.getDelegate()); + } else { + throw new IllegalArgumentException( + "(clientId and clientSecret) or serviceAccountKey are required to create Google Calendar client"); } - return client; } public GoogleCalendarClientFactory getClientFactory() { diff --git a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/GoogleCalendarEndpoint.java b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/GoogleCalendarEndpoint.java index 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 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..220e5f52ba525 --- /dev/null +++ b/components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamClientPerEndpointOptionsTest.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.google.calendar.stream; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.calendar.Calendar; +import org.apache.camel.CamelContext; +import org.apache.camel.component.google.calendar.GoogleCalendarClientFactory; +import org.apache.camel.impl.DefaultCamelContext; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +class GoogleCalendarStreamClientPerEndpointOptionsTest { + + private static final String OAUTH = "?clientId=id&clientSecret=secret&refreshToken="; + private static final String SERVICE_ACCOUNT = "?serviceAccountKey=classpath:service-account.json&delegate="; + + @Test + void shouldCreateOneClientPerEndpoint() throws Exception { + RecordingClientFactory factory = new RecordingClientFactory(); + try (CamelContext context = new DefaultCamelContext()) { + context.getComponent("google-calendar-stream", GoogleCalendarStreamComponent.class).setClientFactory(factory); + context.start(); + + GoogleCalendarStreamEndpoint first = context.getEndpoint( + "google-calendar-stream://cal-a" + OAUTH + "token-a", GoogleCalendarStreamEndpoint.class); + GoogleCalendarStreamEndpoint second = context.getEndpoint( + "google-calendar-stream://cal-b" + OAUTH + "token-b", GoogleCalendarStreamEndpoint.class); + GoogleCalendarStreamEndpoint sameAsFirst = context.getEndpoint( + "google-calendar-stream://cal-c" + OAUTH + "token-a", GoogleCalendarStreamEndpoint.class); + + Calendar firstClient = first.getClient(); + Calendar secondClient = second.getClient(); + + assertNotSame(firstClient, secondClient); + assertNotSame(firstClient, sameAsFirst.getClient()); + assertSame(firstClient, first.getClient()); + assertEquals(Arrays.asList("token-a", "token-b", "token-a"), factory.created); + } + } + + @Test + void shouldKeepOneClientPerDelegate() throws Exception { + RecordingClientFactory factory = new RecordingClientFactory(); + try (CamelContext context = new DefaultCamelContext()) { + context.getComponent("google-calendar-stream", GoogleCalendarStreamComponent.class).setClientFactory(factory); + context.start(); + + GoogleCalendarStreamEndpoint first + = context.getEndpoint("google-calendar-stream://cal-a" + SERVICE_ACCOUNT + "user-a", + GoogleCalendarStreamEndpoint.class); + GoogleCalendarStreamEndpoint second + = context.getEndpoint("google-calendar-stream://cal-b" + SERVICE_ACCOUNT + "user-b", + GoogleCalendarStreamEndpoint.class); + + assertNotSame(first.getClient(), second.getClient()); + assertEquals(Arrays.asList("user-a", "user-b"), factory.created); + } + } + + private static final class RecordingClientFactory implements GoogleCalendarClientFactory { + + private final List 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..83a3b16feb443 100644 --- a/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveComponent.java +++ b/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveComponent.java @@ -37,8 +37,6 @@ public class GoogleDriveComponent @Metadata GoogleDriveConfiguration configuration; @Metadata(label = "advanced") - private Drive client; - @Metadata(label = "advanced") private GoogleDriveClientFactory clientFactory; @Metadata(label = "proxy", description = "Proxy server host") private String proxyHost; @@ -58,22 +56,34 @@ protected GoogleDriveApiName getApiName(String apiNameStr) { return getCamelContext().getTypeConverter().convertTo(GoogleDriveApiName.class, apiNameStr); } + /** + * Creates a client for the given configuration with the client factory of the component. + * + * @see #getClient(GoogleDriveConfiguration, GoogleDriveClientFactory) + */ public Drive getClient(GoogleDriveConfiguration config) { - if (client == null) { - if (config.getClientId() != null && !config.getClientId().isBlank() - && config.getClientSecret() != null && !config.getClientSecret().isBlank()) { - client = getClientFactory().makeClient(config.getClientId(), - config.getClientSecret(), config.getScopesAsList(), - config.getApplicationName(), config.getRefreshToken(), config.getAccessToken()); - } else if (config.getServiceAccountKey() != null && !config.getServiceAccountKey().isBlank()) { - client = getClientFactory().makeClient(getCamelContext(), config.getServiceAccountKey(), - config.getScopesAsList(), config.getApplicationName(), config.getDelegate()); - } else { - throw new IllegalArgumentException( - "(clientId and clientSecret) or serviceAccountKey are required to create Google Drive client"); - } + return getClient(config, null); + } + + /** + * Creates a client for the given configuration with the given client factory or, when that is null, with the client + * factory of the component. Each endpoint creates its own client from its own options and keeps it for its + * lifetime. + */ + public Drive getClient(GoogleDriveConfiguration config, GoogleDriveClientFactory factory) { + GoogleDriveClientFactory factoryToUse = factory != null ? factory : getClientFactory(); + if (config.getClientId() != null && !config.getClientId().isBlank() + && config.getClientSecret() != null && !config.getClientSecret().isBlank()) { + return factoryToUse.makeClient(config.getClientId(), + config.getClientSecret(), config.getScopesAsList(), + config.getApplicationName(), config.getRefreshToken(), config.getAccessToken()); + } else if (config.getServiceAccountKey() != null && !config.getServiceAccountKey().isBlank()) { + return factoryToUse.makeClient(getCamelContext(), config.getServiceAccountKey(), + config.getScopesAsList(), config.getApplicationName(), config.getDelegate()); + } else { + throw new IllegalArgumentException( + "(clientId and clientSecret) or serviceAccountKey are required to create Google Drive client"); } - return client; } public GoogleDriveClientFactory getClientFactory() { diff --git a/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveEndpoint.java b/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveEndpoint.java index 9a428f77400bc..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 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/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/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..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; @@ -32,9 +35,9 @@ public class GoogleMailComponent @Metadata GoogleMailConfiguration configuration; @Metadata(label = "advanced") - private Gmail client; - @Metadata(label = "advanced") private GoogleMailClientFactory clientFactory; + private final Lock clientLock = new ReentrantLock(); + private volatile Gmail client; public GoogleMailComponent() { super(GoogleMailApiName.class, GoogleMailApiCollection.getCollection()); @@ -49,22 +52,44 @@ protected GoogleMailApiName getApiName(String apiNameStr) { return getCamelContext().getTypeConverter().convertTo(GoogleMailApiName.class, apiNameStr); } - public Gmail getClient(GoogleMailConfiguration config) { - if (client == null) { - if (config.getClientId() != null && !config.getClientId().isBlank() - && config.getClientSecret() != null && !config.getClientSecret().isBlank()) { - client = getClientFactory().makeClient(config.getClientId(), - config.getClientSecret(), config.getScopesAsList(), - config.getApplicationName(), config.getRefreshToken(), config.getAccessToken()); - } else if (config.getServiceAccountKey() != null && !config.getServiceAccountKey().isBlank()) { - client = getClientFactory().makeClient(getCamelContext(), config.getServiceAccountKey(), - config.getScopesAsList(), config.getApplicationName(), config.getDelegate()); - } else { - throw new IllegalArgumentException( - "(clientId and clientSecret) or serviceAccountKey are required to create Gmail client"); + /** + * Gets the client for the configuration of the component, created on first use and kept until the component stops. + * It serves callers that have no endpoint, such as the {@code google-mail:update-message-labels} data type + * transformer. Endpoints do not use it: each endpoint creates its own client from its own options. + */ + public Gmail getClient() { + Gmail answer = client; + if (answer == null) { + clientLock.lock(); + try { + if (client == null) { + client = getClient(getConfiguration()); + } + answer = client; + } finally { + clientLock.unlock(); } } - return client; + return answer; + } + + /** + * Creates a client for the given configuration with the client factory of the component. Each endpoint creates its + * own client from its own options and keeps it for its lifetime. + */ + public Gmail getClient(GoogleMailConfiguration config) { + if (config.getClientId() != null && !config.getClientId().isBlank() + && config.getClientSecret() != null && !config.getClientSecret().isBlank()) { + return getClientFactory().makeClient(config.getClientId(), + config.getClientSecret(), config.getScopesAsList(), + config.getApplicationName(), config.getRefreshToken(), config.getAccessToken()); + } else if (config.getServiceAccountKey() != null && !config.getServiceAccountKey().isBlank()) { + return getClientFactory().makeClient(getCamelContext(), config.getServiceAccountKey(), + config.getScopesAsList(), config.getApplicationName(), config.getDelegate()); + } else { + throw new IllegalArgumentException( + "(clientId and clientSecret) or serviceAccountKey are required to create Gmail client"); + } } public GoogleMailClientFactory getClientFactory() { @@ -105,4 +130,10 @@ 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/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 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 new file mode 100644 index 0000000000000..4e900ef1150ab --- /dev/null +++ b/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/GoogleMailClientPerEndpointOptionsTest.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.google.mail; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.services.gmail.Gmail; +import org.apache.camel.CamelContext; +import org.apache.camel.impl.DefaultCamelContext; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +class GoogleMailClientPerEndpointOptionsTest { + + private static final String OAUTH = "?clientId=id&clientSecret=secret&refreshToken="; + private static final String SERVICE_ACCOUNT = "?serviceAccountKey=classpath:service-account.json&delegate="; + + @Test + void shouldCreateOneClientPerEndpoint() throws Exception { + RecordingClientFactory factory = new RecordingClientFactory(); + try (CamelContext context = new DefaultCamelContext()) { + context.getComponent("google-mail", GoogleMailComponent.class).setClientFactory(factory); + context.start(); + + GoogleMailEndpoint first + = context.getEndpoint("google-mail://messages/list" + OAUTH + "token-a", GoogleMailEndpoint.class); + GoogleMailEndpoint second + = context.getEndpoint("google-mail://messages/list" + OAUTH + "token-b", GoogleMailEndpoint.class); + GoogleMailEndpoint sameAsFirst + = context.getEndpoint("google-mail://messages/get" + OAUTH + "token-a", GoogleMailEndpoint.class); + + assertEquals(Arrays.asList("token-a", "token-b", "token-a"), factory.created); + assertNotSame(first.getClient(), second.getClient()); + assertNotSame(first.getClient(), sameAsFirst.getClient()); + assertSame(first.getClient(), first.getClient()); + assertEquals(3, factory.created.size()); + } + } + + @Test + void shouldKeepOneClientPerDelegate() throws Exception { + RecordingClientFactory factory = new RecordingClientFactory(); + try (CamelContext context = new DefaultCamelContext()) { + context.getComponent("google-mail", GoogleMailComponent.class).setClientFactory(factory); + context.start(); + + GoogleMailEndpoint first = context.getEndpoint("google-mail://messages/list" + SERVICE_ACCOUNT + "user-a", + GoogleMailEndpoint.class); + GoogleMailEndpoint second = context.getEndpoint("google-mail://messages/list" + SERVICE_ACCOUNT + "user-b", + GoogleMailEndpoint.class); + + assertEquals(Arrays.asList("user-a", "user-b"), factory.created); + assertNotSame(first.getClient(), second.getClient()); + } + } + + @Test + void shouldKeepOneClientForTheComponentConfiguration() throws Exception { + RecordingClientFactory factory = new RecordingClientFactory(); + try (CamelContext context = new DefaultCamelContext()) { + GoogleMailComponent component = context.getComponent("google-mail", GoogleMailComponent.class); + component.setClientFactory(factory); + GoogleMailConfiguration configuration = new GoogleMailConfiguration(); + configuration.setClientId("id"); + configuration.setClientSecret("secret"); + configuration.setRefreshToken("token-component"); + component.setConfiguration(configuration); + context.start(); + + Gmail componentClient = component.getClient(); + GoogleMailEndpoint endpoint + = context.getEndpoint("google-mail://messages/list" + OAUTH + "token-a", GoogleMailEndpoint.class); + + assertSame(componentClient, component.getClient()); + assertNotSame(componentClient, endpoint.getClient()); + assertEquals(List.of("token-component", "token-a"), factory.created); + } + } + + private static final class RecordingClientFactory implements GoogleMailClientFactory { + + private final List 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..3573d822652d3 --- /dev/null +++ b/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamClientPerEndpointOptionsTest.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.google.mail.stream; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.services.gmail.Gmail; +import org.apache.camel.CamelContext; +import org.apache.camel.component.google.mail.GoogleMailClientFactory; +import org.apache.camel.impl.DefaultCamelContext; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GoogleMailStreamClientPerEndpointOptionsTest { + + private static final String OAUTH = "?clientId=id&clientSecret=secret&refreshToken="; + private static final String SERVICE_ACCOUNT = "?serviceAccountKey=classpath:service-account.json&delegate="; + + @Test + void shouldCreateOneClientPerEndpoint() throws Exception { + RecordingClientFactory factory = new RecordingClientFactory(); + try (CamelContext context = new DefaultCamelContext()) { + context.getComponent("google-mail-stream", GoogleMailStreamComponent.class).setClientFactory(factory); + context.start(); + + GoogleMailStreamEndpoint first = context.getEndpoint("google-mail-stream://box-a" + OAUTH + "token-a", + GoogleMailStreamEndpoint.class); + GoogleMailStreamEndpoint second = context.getEndpoint("google-mail-stream://box-b" + OAUTH + "token-b", + GoogleMailStreamEndpoint.class); + GoogleMailStreamEndpoint sameAsFirst = context.getEndpoint("google-mail-stream://box-c" + OAUTH + "token-a", + GoogleMailStreamEndpoint.class); + + Gmail firstClient = first.getClient(); + Gmail secondClient = second.getClient(); + + assertNotSame(firstClient, secondClient); + assertNotSame(firstClient, sameAsFirst.getClient()); + assertSame(firstClient, first.getClient()); + assertEquals(Arrays.asList("token-a", "token-b", "token-a"), factory.created); + } + } + + @Test + void shouldKeepOneClientPerDelegate() throws Exception { + RecordingClientFactory factory = new RecordingClientFactory(); + try (CamelContext context = new DefaultCamelContext()) { + context.getComponent("google-mail-stream", GoogleMailStreamComponent.class).setClientFactory(factory); + context.start(); + + GoogleMailStreamEndpoint first = context.getEndpoint("google-mail-stream://box-a" + SERVICE_ACCOUNT + "user-a", + GoogleMailStreamEndpoint.class); + GoogleMailStreamEndpoint second = context.getEndpoint("google-mail-stream://box-b" + SERVICE_ACCOUNT + "user-b", + GoogleMailStreamEndpoint.class); + + assertNotSame(first.getClient(), second.getClient()); + assertEquals(Arrays.asList("user-a", "user-b"), factory.created); + } + } + + @Test + void shouldFailWithoutCredentials() throws Exception { + try (CamelContext context = new DefaultCamelContext()) { + context.getComponent("google-mail-stream", GoogleMailStreamComponent.class) + .setClientFactory(new RecordingClientFactory()); + context.start(); + + GoogleMailStreamEndpoint endpoint + = context.getEndpoint("google-mail-stream://box-a", GoogleMailStreamEndpoint.class); + + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, endpoint::getClient); + assertTrue(e.getMessage().contains("serviceAccountKey"), e.getMessage()); + } + } + + private static final class RecordingClientFactory implements GoogleMailClientFactory { + + private final List 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..f3a06901580fb 100644 --- a/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsComponent.java +++ b/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsComponent.java @@ -33,8 +33,6 @@ public class GoogleSheetsComponent @Metadata GoogleSheetsConfiguration configuration; @Metadata(label = "advanced") - private Sheets client; - @Metadata(label = "advanced") private GoogleSheetsClientFactory clientFactory; public GoogleSheetsComponent() { @@ -50,20 +48,20 @@ protected GoogleSheetsApiName getApiName(String apiNameStr) { return getCamelContext().getTypeConverter().convertTo(GoogleSheetsApiName.class, apiNameStr); } + /** + * Creates a client for the given configuration with the client factory of the component. Each endpoint creates its + * own client from its own options and keeps it for its lifetime. + */ public Sheets getClient(GoogleSheetsConfiguration config) { - if (client == null) { - if (config.getClientId() != null && !config.getClientId().isBlank() - && config.getClientSecret() != null && !config.getClientSecret().isBlank()) { - client = getClientFactory().makeClient(config.getClientId(), - config.getClientSecret(), config.getScopesAsList(), - config.getApplicationName(), config.getRefreshToken(), config.getAccessToken()); - } else { - // Use service account key or fall back to Application Default Credentials - client = getClientFactory().makeClient(getCamelContext(), config.getServiceAccountKey(), - config.getScopesAsList(), config.getApplicationName(), config.getDelegate()); - } + if (config.getClientId() != null && !config.getClientId().isBlank() + && config.getClientSecret() != null && !config.getClientSecret().isBlank()) { + return getClientFactory().makeClient(config.getClientId(), + config.getClientSecret(), config.getScopesAsList(), + config.getApplicationName(), config.getRefreshToken(), config.getAccessToken()); } - return client; + // Use service account key or fall back to Application Default Credentials + return getClientFactory().makeClient(getCamelContext(), config.getServiceAccountKey(), + config.getScopesAsList(), config.getApplicationName(), config.getDelegate()); } public GoogleSheetsClientFactory getClientFactory() { diff --git a/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsEndpoint.java b/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsEndpoint.java index 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 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..c8039e19f7db3 --- /dev/null +++ b/components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamClientPerEndpointOptionsTest.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.google.sheets.stream; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.sheets.v4.Sheets; +import org.apache.camel.CamelContext; +import org.apache.camel.component.google.sheets.GoogleSheetsClientFactory; +import org.apache.camel.impl.DefaultCamelContext; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +class GoogleSheetsStreamClientPerEndpointOptionsTest { + + private static final String OAUTH = "?clientId=id&clientSecret=secret&refreshToken="; + private static final String SERVICE_ACCOUNT = "?serviceAccountKey=classpath:service-account.json&delegate="; + + @Test + void shouldCreateOneClientPerEndpoint() throws Exception { + RecordingClientFactory factory = new RecordingClientFactory(); + try (CamelContext context = new DefaultCamelContext()) { + context.getComponent("google-sheets-stream", GoogleSheetsStreamComponent.class).setClientFactory(factory); + context.start(); + + GoogleSheetsStreamEndpoint first = context.getEndpoint("google-sheets-stream://sheet-a" + OAUTH + "token-a", + GoogleSheetsStreamEndpoint.class); + GoogleSheetsStreamEndpoint second = context.getEndpoint("google-sheets-stream://sheet-b" + OAUTH + "token-b", + GoogleSheetsStreamEndpoint.class); + GoogleSheetsStreamEndpoint sameAsFirst = context.getEndpoint( + "google-sheets-stream://sheet-c" + OAUTH + "token-a", GoogleSheetsStreamEndpoint.class); + + Sheets firstClient = first.getClient(); + Sheets secondClient = second.getClient(); + + assertNotSame(firstClient, secondClient); + assertNotSame(firstClient, sameAsFirst.getClient()); + assertSame(firstClient, first.getClient()); + assertEquals(Arrays.asList("token-a", "token-b", "token-a"), factory.created); + } + } + + @Test + void shouldKeepOneClientPerDelegate() throws Exception { + RecordingClientFactory factory = new RecordingClientFactory(); + try (CamelContext context = new DefaultCamelContext()) { + context.getComponent("google-sheets-stream", GoogleSheetsStreamComponent.class).setClientFactory(factory); + context.start(); + + GoogleSheetsStreamEndpoint first + = context.getEndpoint("google-sheets-stream://sheet-a" + SERVICE_ACCOUNT + "user-a", + GoogleSheetsStreamEndpoint.class); + GoogleSheetsStreamEndpoint second + = context.getEndpoint("google-sheets-stream://sheet-b" + SERVICE_ACCOUNT + "user-b", + GoogleSheetsStreamEndpoint.class); + + assertNotSame(first.getClient(), second.getClient()); + assertEquals(Arrays.asList("user-a", "user-b"), factory.created); + } + } + + private static final class RecordingClientFactory implements GoogleSheetsClientFactory { + + private final List 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..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 @@ -232,6 +232,21 @@ a JSON object when none is configured). A route that sets the header must enable `.unmarshal(new JsonDataFormat(JsonLibrary.Gson).allowUnmarshallType(true))` or ``. +=== camel-google-mail, camel-google-drive, camel-google-calendar, camel-google-sheets - one API client per endpoint + +The `google-mail`, `google-mail-stream`, `google-drive`, `google-calendar`, `google-calendar-stream`, `google-sheets` +and `google-sheets-stream` components now create one API client per endpoint, from the options of that endpoint +(`clientId`, `clientSecret`, `refreshToken`, `accessToken`, `serviceAccountKey`, `delegate`, `scopes` and +`applicationName`, plus `emailAddress`, `p12FileName` and `user` on the two calendar components), instead of a single +client per component that every endpoint of the component shared. Each endpoint keeps its client for its lifetime. The +`clientFactory` option of a `google-drive` endpoint is now used to create the client of that endpoint. In addition, a +`google-mail-stream` endpoint configured with neither `clientSecret` nor `serviceAccountKey` now fails with an +`IllegalArgumentException` when its client is created, instead of getting a `null` client and failing at the first API +call. The `google-mail:update-message-labels` data type transformer now uses a client created from the component-level +configuration (the `camel.component.google-mail.*` options), kept until the component stops. Before, it used whichever +client the component had already created, for example the one of a `google-mail:messages/modify` endpoint, so +credentials set only on an endpoint URI no longer reach the transformer: set them on the component as well. + === camel-jbang - camel validate and camel overview report the same endpoints no route consumes `camel validate source` and `camel overview` now use the same rules for a `direct:`, `seda:` or `disruptor:` endpoint