This is an automated email from the ASF dual-hosted git repository. acosentino pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/camel.git
commit 47f173822d6730300e28d60117d513c89f78fa2b Author: Christoph Deppisch <cdeppi...@redhat.com> AuthorDate: Thu Jun 6 10:26:45 2019 +0200 CAMEL-13617: Improve testability of google-sheets component and incorporate latest enhancements to 3.x --- components/camel-google-sheets/pom.xml | 85 +++++ .../main/docs/google-sheets-stream-component.adoc | 5 +- .../sheets/BatchGoogleSheetsClientFactory.java | 63 +++- .../google/sheets/GoogleSheetsClientFactory.java | 6 +- .../google/sheets/GoogleSheetsEndpoint.java | 40 +- .../google/sheets/GoogleSheetsProducer.java | 2 +- .../sheets/GoogleSheetsVerifierExtension.java | 9 +- .../sheets/internal/GoogleSheetsConstants.java | 4 +- .../sheets/stream/GoogleSheetsStreamComponent.java | 8 +- .../stream/GoogleSheetsStreamConfiguration.java | 22 +- .../sheets/stream/GoogleSheetsStreamConstants.java | 2 + .../sheets/stream/GoogleSheetsStreamConsumer.java | 42 ++- .../sheets/stream/GoogleSheetsStreamEndpoint.java | 18 +- .../sheets/AbstractGoogleSheetsTestSupport.java | 46 ++- .../sheets/SheetsSpreadsheetsIntegrationTest.java | 37 +- .../SheetsSpreadsheetsValuesIntegrationTest.java | 83 ++++- .../sheets/server/GoogleSheetsApiTestServer.java | 354 ++++++++++++++++++ .../server/GoogleSheetsApiTestServerAssert.java | 405 +++++++++++++++++++++ .../server/GoogleSheetsApiTestServerRule.java | 116 ++++++ .../AbstractGoogleSheetsStreamTestSupport.java | 17 + .../SheetsStreamConsumerIntegrationTest.java | 113 +++++- .../src/test/resources/googleapis.jks | Bin 0 -> 2695 bytes .../src/test/resources/test-options.properties | 8 +- parent/pom.xml | 1 + .../GoogleSheetsStreamComponentConfiguration.java | 17 +- 25 files changed, 1400 insertions(+), 103 deletions(-) diff --git a/components/camel-google-sheets/pom.xml b/components/camel-google-sheets/pom.xml index 4a36c10..7c3ac61 100644 --- a/components/camel-google-sheets/pom.xml +++ b/components/camel-google-sheets/pom.xml @@ -39,14 +39,58 @@ <componentPackage>org.apache.camel.component.google.sheets</componentPackage> <outPackage>org.apache.camel.component.google.sheets.internal</outPackage> <camel.osgi.private.pkg>org.apache.camel.component.google.sheets.internal</camel.osgi.private.pkg> + <spring-security-oauth2-version>2.3.6.RELEASE</spring-security-oauth2-version> </properties> + <dependencyManagement> + <dependencies> + <!-- Test dependencies --> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-webmvc</artifactId> + <version>${spring-version}</version> + </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-web</artifactId> + <version>${spring-security-version}</version> + </dependency> + <dependency> + <groupId>org.springframework.security.oauth</groupId> + <artifactId>spring-security-oauth2</artifactId> + <version>${spring-security-oauth2-version}</version> + </dependency> + <dependency> + <groupId>com.consol.citrus</groupId> + <artifactId>citrus-core</artifactId> + <version>${citrus.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.consol.citrus</groupId> + <artifactId>citrus-java-dsl</artifactId> + <version>${citrus.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.consol.citrus</groupId> + <artifactId>citrus-http</artifactId> + <version>${citrus.version}</version> + <scope>test</scope> + </dependency> + </dependencies> + </dependencyManagement> + <dependencies> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-support</artifactId> </dependency> <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-util</artifactId> + </dependency> + <dependency> <groupId>com.google.api-client</groupId> <artifactId>google-api-client</artifactId> <version>${google-api-client-version}</version> @@ -107,6 +151,47 @@ <artifactId>camel-test</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-mock</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.springframework.security.oauth</groupId> + <artifactId>spring-security-oauth2</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.consol.citrus</groupId> + <artifactId>citrus-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.consol.citrus</groupId> + <artifactId>citrus-java-dsl</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.consol.citrus</groupId> + <artifactId>citrus-http</artifactId> + <scope>test</scope> + </dependency> </dependencies> <build> diff --git a/components/camel-google-sheets/src/main/docs/google-sheets-stream-component.adoc b/components/camel-google-sheets/src/main/docs/google-sheets-stream-component.adoc index c2caf17..08c93f7 100644 --- a/components/camel-google-sheets/src/main/docs/google-sheets-stream-component.adoc +++ b/components/camel-google-sheets/src/main/docs/google-sheets-stream-component.adoc @@ -79,7 +79,7 @@ with the following path and query parameters: |=== -==== Query Parameters (32 parameters): +==== Query Parameters (33 parameters): [width="100%",cols="2,5,^1,2",options="header"] @@ -92,11 +92,12 @@ with the following path and query parameters: | *clientSecret* (consumer) | Client secret of the sheets application | | String | *includeGridData* (consumer) | True if grid data should be returned. | false | boolean | *majorDimension* (consumer) | Specifies the major dimension that results should use.. | ROWS | String -| *maxResults* (consumer) | Specify the maximum number of returned results. This will limit the number of rows in a returned value range data set or the number of returned value ranges in a batch request. | 10 | int +| *maxResults* (consumer) | Specify the maximum number of returned results. This will limit the number of rows in a returned value range data set or the number of returned value ranges in a batch request. | 0 | int | *range* (consumer) | Specifies the range of rows and columns in a sheet to get data from. | | String | *refreshToken* (consumer) | OAuth 2 refresh token. Using this, the Google Calendar component can obtain a new accessToken whenever the current one expires - a necessity if the application is long-lived. | | String | *scopes* (consumer) | Specifies the level of permissions you want a sheets application to have to a user account. See https://developers.google.com/identity/protocols/googlescopes for more info. | | List | *sendEmptyMessageWhenIdle* (consumer) | If the polling consumer did not poll any files, you can enable this option to send an empty message (no body) instead. | false | boolean +| *splitResults* (consumer) | True if value range result should be split into rows or columns to process each of them individually. When true each row or column is represented with a separate exchange in batch processing. Otherwise value range object is used as exchange junk size. | false | boolean | *spreadsheetId* (consumer) | Specifies the spreadsheet identifier that is used to identify the target to obtain. | | String | *valueRenderOption* (consumer) | Determines how values should be rendered in the output. | FORMATTED_VALUE | String | *exceptionHandler* (consumer) | To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this option is not in use. By default the consumer will deal with exceptions, that will be logged at WARN or ERROR level and ignored. | | ExceptionHandler diff --git a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/BatchGoogleSheetsClientFactory.java b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/BatchGoogleSheetsClientFactory.java index 327912b..a06716d 100644 --- a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/BatchGoogleSheetsClientFactory.java +++ b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/BatchGoogleSheetsClientFactory.java @@ -18,49 +18,78 @@ package org.apache.camel.component.google.sheets; import com.google.api.client.auth.oauth2.Credential; import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.http.HttpTransport; 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.RuntimeCamelException; +import org.apache.camel.util.ObjectHelper; public class BatchGoogleSheetsClientFactory implements GoogleSheetsClientFactory { - private final NetHttpTransport transport; + private final HttpTransport transport; private final JacksonFactory jsonFactory; public BatchGoogleSheetsClientFactory() { - this.transport = new NetHttpTransport(); - this.jsonFactory = new JacksonFactory(); + this(new NetHttpTransport(), new JacksonFactory()); + } + + public BatchGoogleSheetsClientFactory(HttpTransport httpTransport) { + this(httpTransport, new JacksonFactory()); + } + + public BatchGoogleSheetsClientFactory(HttpTransport httpTransport, JacksonFactory jacksonFactory) { + this.transport = httpTransport; + this.jsonFactory = jacksonFactory; } @Override - public Sheets makeClient(String clientId, String clientSecret, String applicationName, String refreshToken, String accessToken) { + public Sheets makeClient(String clientId, + String clientSecret, + String applicationName, + String refreshToken, + String accessToken) { if (clientId == null || clientSecret == null) { throw new IllegalArgumentException("clientId and clientSecret are required to create Google Sheets client."); } + try { - Credential credential = authorize(clientId, clientSecret); + Credential credential = authorize(clientId, clientSecret, refreshToken, accessToken); - if (refreshToken != null && !"".equals(refreshToken)) { - credential.setRefreshToken(refreshToken); - } - if (accessToken != null && !"".equals(accessToken)) { - credential.setAccessToken(accessToken); - } - return new Sheets.Builder(transport, jsonFactory, credential) - .setApplicationName(applicationName) - .build(); + Sheets.Builder clientBuilder = new Sheets.Builder(transport, jsonFactory, credential) + .setApplicationName(applicationName); + configure(clientBuilder); + return clientBuilder.build(); } catch (Exception e) { throw new RuntimeCamelException("Could not create Google Sheets client.", e); } } + /** + * Subclasses may add customized configuration to client builder. + * @param clientBuilder + */ + protected void configure(Sheets.Builder clientBuilder) { + clientBuilder.setRootUrl(Sheets.DEFAULT_ROOT_URL); + } + // Authorizes the installed application to access user's protected data. - private Credential authorize(String clientId, String clientSecret) { + private Credential authorize(String clientId, String clientSecret, String refreshToken, String accessToken) { // authorize - return new GoogleCredential.Builder() + Credential credential = new GoogleCredential.Builder() .setJsonFactory(jsonFactory) .setTransport(transport) - .setClientSecrets(clientId, clientSecret).build(); + .setClientSecrets(clientId, clientSecret) + .build(); + + if (ObjectHelper.isNotEmpty(refreshToken)) { + credential.setRefreshToken(refreshToken); + } + + if (ObjectHelper.isNotEmpty(accessToken)) { + credential.setAccessToken(accessToken); + } + + return credential; } } diff --git a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsClientFactory.java b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsClientFactory.java index 8cf2cc1..b602e49 100644 --- a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsClientFactory.java +++ b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsClientFactory.java @@ -20,6 +20,10 @@ import com.google.api.services.sheets.v4.Sheets; public interface GoogleSheetsClientFactory { - Sheets makeClient(String clientId, String clientSecret, String applicationName, String refreshToken, String accessToken); + Sheets makeClient(String clientId, + String clientSecret, + String applicationName, + String refreshToken, + String accessToken); } diff --git a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsEndpoint.java b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsEndpoint.java index 5a07006..5e7c46d 100644 --- a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsEndpoint.java +++ b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsEndpoint.java @@ -35,23 +35,31 @@ import org.apache.camel.support.component.ApiMethodPropertiesHelper; /** * The google-sheets component provides access to Google Sheets. */ -@UriEndpoint(firstVersion = "2.23.0", scheme = "google-sheets", title = "Google Sheets", - syntax = "google-sheets:apiName/methodName", consumerPrefix = "consumer", label = "api,cloud,sheets") +@UriEndpoint(firstVersion = "2.23.0", + scheme = "google-sheets", + title = "Google Sheets", + syntax = "google-sheets:apiName/methodName", + consumerPrefix = "consumer", + label = "api,cloud,sheets") public class GoogleSheetsEndpoint extends AbstractApiEndpoint<GoogleSheetsApiName, GoogleSheetsConfiguration> { - @UriParam - private GoogleSheetsConfiguration configuration; + @UriParam(name = "configuration") + private GoogleSheetsConfiguration endpointConfiguration; private Object apiProxy; - public GoogleSheetsEndpoint(String uri, GoogleSheetsComponent component, GoogleSheetsApiName apiName, String methodName, GoogleSheetsConfiguration endpointConfiguration) { + public GoogleSheetsEndpoint(String uri, + GoogleSheetsComponent component, + GoogleSheetsApiName apiName, + String methodName, + GoogleSheetsConfiguration endpointConfiguration) { super(uri, component, apiName, methodName, GoogleSheetsApiCollection.getCollection().getHelper(apiName), endpointConfiguration); - this.configuration = endpointConfiguration; + this.endpointConfiguration = endpointConfiguration; } @Override public Producer createProducer() throws Exception { - return new org.apache.camel.component.google.sheets.GoogleSheetsProducer(this); + return new GoogleSheetsProducer(this); } @Override @@ -79,19 +87,19 @@ public class GoogleSheetsEndpoint extends AbstractApiEndpoint<GoogleSheetsApiNam @Override protected void afterConfigureProperties() { switch (apiName) { - case SPREADSHEETS: - apiProxy = getClient().spreadsheets(); - break; - case DATA: - apiProxy = getClient().spreadsheets().values(); - break; - default: - throw new IllegalArgumentException("Invalid API name " + apiName); + case SPREADSHEETS: + apiProxy = getClient().spreadsheets(); + break; + case DATA: + apiProxy = getClient().spreadsheets().values(); + break; + default: + throw new IllegalArgumentException("Invalid API name " + apiName); } } public Sheets getClient() { - return ((GoogleSheetsComponent)getComponent()).getClient(configuration); + return ((GoogleSheetsComponent)getComponent()).getClient(endpointConfiguration); } @Override diff --git a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsProducer.java b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsProducer.java index c147c39..dad7a21 100644 --- a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsProducer.java +++ b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsProducer.java @@ -39,7 +39,7 @@ public class GoogleSheetsProducer extends AbstractApiProducer<GoogleSheetsApiNam @Override protected Object doInvokeMethod(ApiMethod method, Map<String, Object> properties) throws RuntimeCamelException { - AbstractGoogleClientRequest<?> request = (AbstractGoogleClientRequest)super.doInvokeMethod(method, properties); + AbstractGoogleClientRequest<?> request = (AbstractGoogleClientRequest) super.doInvokeMethod(method, properties); try { TypeConverter typeConverter = getEndpoint().getCamelContext().getTypeConverter(); for (Entry<String, Object> p : properties.entrySet()) { diff --git a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsVerifierExtension.java b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsVerifierExtension.java index fd6bfcd..96b1ece 100644 --- a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsVerifierExtension.java +++ b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsVerifierExtension.java @@ -61,9 +61,12 @@ public class GoogleSheetsVerifierExtension extends DefaultComponentVerifierExten try { GoogleSheetsConfiguration configuration = setProperties(new GoogleSheetsConfiguration(), parameters); GoogleSheetsClientFactory clientFactory = new BatchGoogleSheetsClientFactory(); - Sheets client = clientFactory.makeClient(configuration.getClientId(), configuration.getClientSecret(), configuration.getApplicationName(), - configuration.getRefreshToken(), configuration.getAccessToken()); - client.spreadsheets().get(Optional.ofNullable(parameters.get("spreadsheetId")).map(Object::toString).orElse(UUID.randomUUID().toString())).execute(); + Sheets client = clientFactory.makeClient(configuration.getClientId(), configuration.getClientSecret(), + configuration.getApplicationName(), + configuration.getRefreshToken(), configuration.getAccessToken()); + client.spreadsheets().get(Optional.ofNullable(parameters.get("spreadsheetId")) + .map(Object::toString) + .orElse(UUID.randomUUID().toString())).execute(); } catch (Exception e) { ResultErrorBuilder errorBuilder = ResultErrorBuilder.withCodeAndDescription(VerificationError.StandardCode.AUTHENTICATION, e.getMessage()) .detail("google_sheets_exception_message", e.getMessage()).detail(VerificationError.ExceptionAttribute.EXCEPTION_CLASS, e.getClass().getName()) diff --git a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/internal/GoogleSheetsConstants.java b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/internal/GoogleSheetsConstants.java index 2c03d35..11cd297 100644 --- a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/internal/GoogleSheetsConstants.java +++ b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/internal/GoogleSheetsConstants.java @@ -29,5 +29,7 @@ public final class GoogleSheetsConstants { /** * Prevent instantiation. */ - private GoogleSheetsConstants() { } + private GoogleSheetsConstants() { + super(); + } } diff --git a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamComponent.java b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamComponent.java index 32f7236..1b9f8d9 100644 --- a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamComponent.java +++ b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamComponent.java @@ -54,9 +54,11 @@ public class GoogleSheetsStreamComponent extends DefaultComponent { public Sheets getClient(GoogleSheetsStreamConfiguration endpointConfiguration) { if (client == null) { - client = getClientFactory().makeClient(endpointConfiguration.getClientId(), endpointConfiguration.getClientSecret(), - endpointConfiguration.getApplicationName(), endpointConfiguration.getRefreshToken(), - endpointConfiguration.getAccessToken()); + client = getClientFactory().makeClient(endpointConfiguration.getClientId(), + endpointConfiguration.getClientSecret(), + endpointConfiguration.getApplicationName(), + endpointConfiguration.getRefreshToken(), + endpointConfiguration.getAccessToken()); } return client; } diff --git a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamConfiguration.java b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamConfiguration.java index ac94812..50b8eab 100644 --- a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamConfiguration.java +++ b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamConfiguration.java @@ -57,8 +57,8 @@ public class GoogleSheetsStreamConfiguration implements Cloneable { @UriParam private String spreadsheetId; - @UriParam(defaultValue = "10") - private int maxResults = 10; + @UriParam(defaultValue = "0") + private int maxResults; @UriParam private String range; @@ -66,6 +66,9 @@ public class GoogleSheetsStreamConfiguration implements Cloneable { @UriParam private boolean includeGridData; + @UriParam + private boolean splitResults; + @UriParam(enums = "ROWS,COLUMNS,DIMENSION_UNSPECIFIED", defaultValue = "ROWS") private String majorDimension = "ROWS"; @@ -240,6 +243,21 @@ public class GoogleSheetsStreamConfiguration implements Cloneable { this.includeGridData = includeGridData; } + public boolean isSplitResults() { + return splitResults; + } + + /** + * True if value range result should be split into rows or columns to process each of them individually. When true + * each row or column is represented with a separate exchange in batch processing. Otherwise value range object is used + * as exchange junk size. + * + * @param splitResults + */ + public void setSplitResults(boolean splitResults) { + this.splitResults = splitResults; + } + // ************************************************* // // ************************************************* diff --git a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamConstants.java b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamConstants.java index 60f80c0..6e803c9 100644 --- a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamConstants.java +++ b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamConstants.java @@ -27,6 +27,8 @@ public final class GoogleSheetsStreamConstants { public static final String SPREADSHEET_URL = PROPERTY_PREFIX + "SpreadsheetUrl"; public static final String MAJOR_DIMENSION = PROPERTY_PREFIX + "MajorDimension"; public static final String RANGE = PROPERTY_PREFIX + "Range"; + public static final String RANGE_INDEX = PROPERTY_PREFIX + "RangeIndex"; + public static final String VALUE_INDEX = PROPERTY_PREFIX + "ValueIndex"; /** * Prevent instantiation. diff --git a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamConsumer.java b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamConsumer.java index 1b20594..c4bf414 100644 --- a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamConsumer.java +++ b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamConsumer.java @@ -20,11 +20,13 @@ import java.util.ArrayDeque; import java.util.Arrays; import java.util.Collections; import java.util.Queue; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import com.google.api.services.sheets.v4.Sheets; import com.google.api.services.sheets.v4.model.BatchGetValuesResponse; import com.google.api.services.sheets.v4.model.Spreadsheet; +import com.google.api.services.sheets.v4.model.ValueRange; import org.apache.camel.Endpoint; import org.apache.camel.Exchange; import org.apache.camel.Processor; @@ -70,8 +72,8 @@ public class GoogleSheetsStreamConsumer extends ScheduledBatchPollingConsumer { if (getConfiguration().getRange().contains(",")) { request.setRanges(Arrays.stream(getConfiguration().getRange().split(",")) - .map(String::trim) - .collect(Collectors.toList())); + .map(String::trim) + .collect(Collectors.toList())); } else { request.setRanges(Collections.singletonList(getConfiguration().getRange())); } @@ -79,11 +81,37 @@ public class GoogleSheetsStreamConsumer extends ScheduledBatchPollingConsumer { BatchGetValuesResponse response = request.execute(); if (response.getValueRanges() != null) { - response.getValueRanges() - .stream() - .limit(getConfiguration().getMaxResults()) - .map(valueRange -> getEndpoint().createExchange(valueRange)) - .forEach(answer::add); + if (getConfiguration().isSplitResults()) { + for (ValueRange valueRange : response.getValueRanges()) { + AtomicInteger rangeIndex = new AtomicInteger(1); + AtomicInteger valueIndex = new AtomicInteger(); + if (getConfiguration().getMaxResults() > 0) { + valueRange.getValues().stream() + .limit(getConfiguration().getMaxResults()) + .map(values -> getEndpoint().createExchange(rangeIndex.get(), valueIndex.incrementAndGet(), valueRange.getRange(), valueRange.getMajorDimension(), values)) + .forEach(answer::add); + } else { + valueRange.getValues().stream() + .map(values -> getEndpoint().createExchange(rangeIndex.get(), valueIndex.incrementAndGet(), valueRange.getRange(), valueRange.getMajorDimension(), values)) + .forEach(answer::add); + } + rangeIndex.incrementAndGet(); + } + } else { + AtomicInteger rangeIndex = new AtomicInteger(); + response.getValueRanges() + .stream() + .peek(valueRange -> { + if (getConfiguration().getMaxResults() > 0) { + valueRange.setValues(valueRange.getValues() + .stream() + .limit(getConfiguration().getMaxResults()) + .collect(Collectors.toList())); + } + }) + .map(valueRange -> getEndpoint().createExchange(rangeIndex.incrementAndGet(), valueRange)) + .forEach(answer::add); + } } } else { Sheets.Spreadsheets.Get request = getClient().spreadsheets().get(getConfiguration().getSpreadsheetId()); diff --git a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamEndpoint.java b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamEndpoint.java index f13f141..3dff538 100644 --- a/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamEndpoint.java +++ b/components/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamEndpoint.java @@ -16,10 +16,11 @@ */ package org.apache.camel.component.google.sheets.stream; +import java.util.List; + import com.google.api.services.sheets.v4.Sheets; import com.google.api.services.sheets.v4.model.Spreadsheet; import com.google.api.services.sheets.v4.model.ValueRange; - import org.apache.camel.Consumer; import org.apache.camel.Exchange; import org.apache.camel.Message; @@ -77,16 +78,29 @@ public class GoogleSheetsStreamEndpoint extends ScheduledPollEndpoint { return configuration; } - public Exchange createExchange(ValueRange valueRange) { + public Exchange createExchange(int rangeIndex, ValueRange valueRange) { Exchange exchange = super.createExchange(getExchangePattern()); Message message = exchange.getIn(); exchange.getIn().setHeader(GoogleSheetsStreamConstants.SPREADSHEET_ID, configuration.getSpreadsheetId()); exchange.getIn().setHeader(GoogleSheetsStreamConstants.RANGE, valueRange.getRange()); + exchange.getIn().setHeader(GoogleSheetsStreamConstants.RANGE_INDEX, rangeIndex); exchange.getIn().setHeader(GoogleSheetsStreamConstants.MAJOR_DIMENSION, valueRange.getMajorDimension()); message.setBody(valueRange); return exchange; } + public Exchange createExchange(int rangeIndex, int valueIndex, String range, String majorDimension, List<Object> values) { + Exchange exchange = super.createExchange(getExchangePattern()); + Message message = exchange.getIn(); + exchange.getIn().setHeader(GoogleSheetsStreamConstants.SPREADSHEET_ID, configuration.getSpreadsheetId()); + exchange.getIn().setHeader(GoogleSheetsStreamConstants.RANGE_INDEX, rangeIndex); + exchange.getIn().setHeader(GoogleSheetsStreamConstants.VALUE_INDEX, valueIndex); + exchange.getIn().setHeader(GoogleSheetsStreamConstants.RANGE, range); + exchange.getIn().setHeader(GoogleSheetsStreamConstants.MAJOR_DIMENSION, majorDimension); + message.setBody(values); + return exchange; + } + public Exchange createExchange(Spreadsheet spreadsheet) { Exchange exchange = super.createExchange(getExchangePattern()); Message message = exchange.getIn(); diff --git a/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/AbstractGoogleSheetsTestSupport.java b/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/AbstractGoogleSheetsTestSupport.java index 1ca893c..4df7eb3 100644 --- a/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/AbstractGoogleSheetsTestSupport.java +++ b/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/AbstractGoogleSheetsTestSupport.java @@ -24,6 +24,9 @@ import java.util.Map; import java.util.Properties; import java.util.Random; +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 com.google.api.services.sheets.v4.model.Sheet; import com.google.api.services.sheets.v4.model.SheetProperties; import com.google.api.services.sheets.v4.model.Spreadsheet; @@ -31,9 +34,12 @@ import com.google.api.services.sheets.v4.model.SpreadsheetProperties; import com.google.api.services.sheets.v4.model.ValueRange; import org.apache.camel.CamelContext; import org.apache.camel.CamelExecutionException; -import org.apache.camel.support.PropertyBindingSupport; +import org.apache.camel.component.google.sheets.internal.GoogleSheetsConstants; +import org.apache.camel.component.google.sheets.server.GoogleSheetsApiTestServer; +import org.apache.camel.component.google.sheets.server.GoogleSheetsApiTestServerRule; import org.apache.camel.test.junit4.CamelTestSupport; - +import org.apache.camel.support.PropertyBindingSupport; +import org.junit.ClassRule; /** * Abstract base class for GoogleSheets Integration tests generated by Camel @@ -46,13 +52,16 @@ public class AbstractGoogleSheetsTestSupport extends CamelTestSupport { private Spreadsheet spreadsheet; + @ClassRule + public static GoogleSheetsApiTestServerRule googleSheetsApiTestServerRule = new GoogleSheetsApiTestServerRule(TEST_OPTIONS_PROPERTIES); + /** * Create test spreadsheet that is used throughout all tests. */ private void createTestSpreadsheet() { Spreadsheet spreadsheet = new Spreadsheet(); SpreadsheetProperties spreadsheetProperties = new SpreadsheetProperties(); - spreadsheetProperties.setTitle("camel-sheets-" + Math.abs(new Random().nextInt())); + spreadsheetProperties.setTitle("camel-sheets-" + new Random().nextInt(Integer.MAX_VALUE)); spreadsheet.setProperties(spreadsheetProperties); @@ -79,12 +88,12 @@ public class AbstractGoogleSheetsTestSupport extends CamelTestSupport { final Map<String, Object> headers = new HashMap<>(); // parameter type is String - headers.put("CamelGoogleSheets.spreadsheetId", spreadsheet.getSpreadsheetId()); + headers.put(GoogleSheetsConstants.PROPERTY_PREFIX + "spreadsheetId", spreadsheet.getSpreadsheetId()); // parameter type is String - headers.put("CamelGoogleSheets.range", TEST_SHEET + "!A1:B2"); + headers.put(GoogleSheetsConstants.PROPERTY_PREFIX + "range", TEST_SHEET + "!A1:B2"); // parameter type is String - headers.put("CamelGoogleSheets.valueInputOption", "USER_ENTERED"); + headers.put(GoogleSheetsConstants.PROPERTY_PREFIX + "valueInputOption", "USER_ENTERED"); requestBodyAndHeaders("google-sheets://data/update?inBody=values", valueRange, headers); } @@ -97,8 +106,20 @@ public class AbstractGoogleSheetsTestSupport extends CamelTestSupport { final GoogleSheetsConfiguration configuration = new GoogleSheetsConfiguration(); PropertyBindingSupport.bindProperties(context, configuration, getTestOptions()); - // add GoogleSheetsComponent to Camel context + // add GoogleSheetsComponent to Camel context and use localhost url final GoogleSheetsComponent component = new GoogleSheetsComponent(context); + component.setClientFactory(new BatchGoogleSheetsClientFactory( + new NetHttpTransport.Builder() + .trustCertificatesFromJavaKeyStore( + getClass().getResourceAsStream("/" + GoogleSheetsApiTestServerRule.SERVER_KEYSTORE), + GoogleSheetsApiTestServerRule.SERVER_KEYSTORE_PASSWORD) + .build(), + new JacksonFactory()) { + @Override + protected void configure(Sheets.Builder clientBuilder) { + clientBuilder.setRootUrl(String.format("https://localhost:%s/", googleSheetsApiTestServerRule.getServerPort())); + } + }); component.setConfiguration(configuration); context.addComponent("google-sheets", component); @@ -149,17 +170,16 @@ public class AbstractGoogleSheetsTestSupport extends CamelTestSupport { return spreadsheet; } - public Spreadsheet getSpreadsheetWithTestData() { - if (spreadsheet == null) { - createTestSpreadsheet(); - } - + public Spreadsheet applyTestData(Spreadsheet spreadsheet) { createTestData(); - return spreadsheet; } public void setSpreadsheet(Spreadsheet sheet) { this.spreadsheet = sheet; } + + public GoogleSheetsApiTestServer getGoogleApiTestServer() { + return googleSheetsApiTestServerRule.getGoogleApiTestServer(); + } } diff --git a/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/SheetsSpreadsheetsIntegrationTest.java b/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/SheetsSpreadsheetsIntegrationTest.java index 4efbcfc..4be016b 100644 --- a/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/SheetsSpreadsheetsIntegrationTest.java +++ b/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/SheetsSpreadsheetsIntegrationTest.java @@ -27,14 +27,16 @@ import com.google.api.services.sheets.v4.model.Request; import com.google.api.services.sheets.v4.model.Spreadsheet; import com.google.api.services.sheets.v4.model.SpreadsheetProperties; import com.google.api.services.sheets.v4.model.UpdateSpreadsheetPropertiesRequest; - import org.apache.camel.builder.RouteBuilder; import org.apache.camel.component.google.sheets.internal.GoogleSheetsApiCollection; +import org.apache.camel.component.google.sheets.internal.GoogleSheetsConstants; import org.apache.camel.component.google.sheets.internal.SheetsSpreadsheetsApiMethod; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.apache.camel.component.google.sheets.server.GoogleSheetsApiTestServerAssert.assertThatGoogleApi; + /** * Test class for {@link com.google.api.services.sheets.v4.Sheets.Spreadsheets} APIs. */ @@ -45,14 +47,18 @@ public class SheetsSpreadsheetsIntegrationTest extends AbstractGoogleSheetsTestS @Test public void testCreate() throws Exception { - String title = "camel-sheets-" + Math.abs(new Random().nextInt()); + String title = "camel-sheets-" + new Random().nextInt(Integer.MAX_VALUE); Spreadsheet sheetToCreate = new Spreadsheet(); SpreadsheetProperties sheetProperties = new SpreadsheetProperties(); sheetProperties.setTitle(title); sheetToCreate.setProperties(sheetProperties); - // using com.google.api.services.sheets.v4.model.Spreadsheet message body for single parameter "content" + assertThatGoogleApi(getGoogleApiTestServer()) + .createSpreadsheetRequest() + .hasTitle(title) + .andReturnRandomSpreadsheet(); + final Spreadsheet result = requestBody("direct://CREATE", sheetToCreate); assertNotNull("create result is null", result); @@ -63,8 +69,17 @@ public class SheetsSpreadsheetsIntegrationTest extends AbstractGoogleSheetsTestS @Test public void testGet() throws Exception { + assertThatGoogleApi(getGoogleApiTestServer()) + .createSpreadsheetRequest() + .hasSheetTitle("TestData") + .andReturnRandomSpreadsheet(); + Spreadsheet testSheet = getSpreadsheet(); + assertThatGoogleApi(getGoogleApiTestServer()) + .getSpreadsheetRequest(testSheet.getSpreadsheetId()) + .andReturnSpreadsheet(testSheet); + // using String message body for single parameter "spreadsheetId" final Spreadsheet result = requestBody("direct://GET", testSheet.getSpreadsheetId()); @@ -76,14 +91,24 @@ public class SheetsSpreadsheetsIntegrationTest extends AbstractGoogleSheetsTestS @Test public void testBatchUpdate() throws Exception { + assertThatGoogleApi(getGoogleApiTestServer()) + .createSpreadsheetRequest() + .hasSheetTitle("TestData") + .andReturnRandomSpreadsheet(); + Spreadsheet testSheet = getSpreadsheet(); String updateTitle = "updated-" + testSheet.getProperties().getTitle(); + assertThatGoogleApi(getGoogleApiTestServer()) + .batchUpdateSpreadsheetRequest(testSheet.getSpreadsheetId()) + .updateTitle(updateTitle) + .andReturnUpdated(); + final Map<String, Object> headers = new HashMap<>(); // parameter type is String - headers.put("CamelGoogleSheets.spreadsheetId", testSheet.getSpreadsheetId()); + headers.put(GoogleSheetsConstants.PROPERTY_PREFIX + "spreadsheetId", testSheet.getSpreadsheetId()); // parameter type is com.google.api.services.sheets.v4.model.BatchUpdateSpreadsheetRequest - headers.put("CamelGoogleSheets.batchUpdateSpreadsheetRequest", new BatchUpdateSpreadsheetRequest() + headers.put(GoogleSheetsConstants.PROPERTY_PREFIX + "batchUpdateSpreadsheetRequest", new BatchUpdateSpreadsheetRequest() .setIncludeSpreadsheetInResponse(true) .setRequests(Collections.singletonList(new Request().setUpdateSpreadsheetProperties(new UpdateSpreadsheetPropertiesRequest() .setProperties(new SpreadsheetProperties().setTitle(updateTitle)) @@ -91,7 +116,7 @@ public class SheetsSpreadsheetsIntegrationTest extends AbstractGoogleSheetsTestS final BatchUpdateSpreadsheetResponse result = requestBodyAndHeaders("direct://BATCHUPDATE", null, headers); - assertNotNull("batchUpdate result in null", result); + assertNotNull("batchUpdate result is null", result); assertEquals(updateTitle, result.getUpdatedSpreadsheet().getProperties().getTitle()); LOG.debug("batchUpdate: " + result); diff --git a/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/SheetsSpreadsheetsValuesIntegrationTest.java b/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/SheetsSpreadsheetsValuesIntegrationTest.java index d1589b2..ceffd5b 100644 --- a/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/SheetsSpreadsheetsValuesIntegrationTest.java +++ b/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/SheetsSpreadsheetsValuesIntegrationTest.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import com.google.api.services.sheets.v4.model.AppendValuesResponse; import com.google.api.services.sheets.v4.model.ClearValuesRequest; @@ -28,14 +29,17 @@ import com.google.api.services.sheets.v4.model.ClearValuesResponse; import com.google.api.services.sheets.v4.model.Spreadsheet; import com.google.api.services.sheets.v4.model.UpdateValuesResponse; import com.google.api.services.sheets.v4.model.ValueRange; - import org.apache.camel.builder.RouteBuilder; import org.apache.camel.component.google.sheets.internal.GoogleSheetsApiCollection; +import org.apache.camel.component.google.sheets.internal.GoogleSheetsConstants; import org.apache.camel.component.google.sheets.internal.SheetsSpreadsheetsValuesApiMethod; +import org.apache.camel.util.ObjectHelper; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.apache.camel.component.google.sheets.server.GoogleSheetsApiTestServerAssert.assertThatGoogleApi; + /** * Test class for {@link com.google.api.services.sheets.v4.Sheets.Spreadsheets.Values} APIs. */ @@ -46,44 +50,63 @@ public class SheetsSpreadsheetsValuesIntegrationTest extends AbstractGoogleSheet @Test public void testGet() throws Exception { + assertThatGoogleApi(getGoogleApiTestServer()) + .createSpreadsheetRequest() + .hasSheetTitle("TestData") + .andReturnRandomSpreadsheet(); + Spreadsheet testSheet = getSpreadsheet(); + assertThatGoogleApi(getGoogleApiTestServer()) + .getValuesRequest(testSheet.getSpreadsheetId(), TEST_SHEET + "!A1:B2") + .andReturnValues(Collections.emptyList()); + final Map<String, Object> headers = new HashMap<>(); // parameter type is String - headers.put("CamelGoogleSheets.spreadsheetId", testSheet.getSpreadsheetId()); + headers.put(GoogleSheetsConstants.PROPERTY_PREFIX + "spreadsheetId", testSheet.getSpreadsheetId()); // parameter type is String - headers.put("CamelGoogleSheets.range", TEST_SHEET + "!A1:B2"); + headers.put(GoogleSheetsConstants.PROPERTY_PREFIX + "range", TEST_SHEET + "!A1:B2"); final ValueRange result = requestBodyAndHeaders("direct://GET", null, headers); assertNotNull("get result is null", result); assertEquals(TEST_SHEET + "!A1:B2", result.getRange()); - assertNull("expected empty value range but found entries", result.getValues()); + assertTrue("expected empty value range but found entries", ObjectHelper.isEmpty(result.getValues())); LOG.debug("get: " + result); } @Test public void testUpdate() throws Exception { + assertThatGoogleApi(getGoogleApiTestServer()) + .createSpreadsheetRequest() + .hasSheetTitle("TestData") + .andReturnRandomSpreadsheet(); + Spreadsheet testSheet = getSpreadsheet(); List<List<Object>> data = Arrays.asList( Arrays.asList("A1", "B1"), Arrays.asList("A2", "B2") ); + + assertThatGoogleApi(getGoogleApiTestServer()) + .updateValuesRequest(testSheet.getSpreadsheetId(), TEST_SHEET + "!A1:B2", data) + .andReturnUpdateResponse(); + ValueRange values = new ValueRange(); values.setValues(data); final Map<String, Object> headers = new HashMap<>(); // parameter type is String - headers.put("CamelGoogleSheets.spreadsheetId", testSheet.getSpreadsheetId()); + headers.put(GoogleSheetsConstants.PROPERTY_PREFIX + "spreadsheetId", testSheet.getSpreadsheetId()); // parameter type is String - headers.put("CamelGoogleSheets.range", TEST_SHEET + "!A1:B2"); + headers.put(GoogleSheetsConstants.PROPERTY_PREFIX + "range", TEST_SHEET + "!A1:B2"); // parameter type is com.google.api.services.sheets.v4.model.ValueRange - headers.put("CamelGoogleSheets.values", values); + headers.put(GoogleSheetsConstants.PROPERTY_PREFIX + "values", values); // parameter type is String - headers.put("CamelGoogleSheets.valueInputOption", "USER_ENTERED"); + headers.put(GoogleSheetsConstants.PROPERTY_PREFIX + "valueInputOption", "USER_ENTERED"); final UpdateValuesResponse result = requestBodyAndHeaders("direct://UPDATE", null, headers); @@ -98,18 +121,29 @@ public class SheetsSpreadsheetsValuesIntegrationTest extends AbstractGoogleSheet @Test public void testAppend() throws Exception { + assertThatGoogleApi(getGoogleApiTestServer()) + .createSpreadsheetRequest() + .hasSheetTitle("TestData") + .andReturnRandomSpreadsheet(); + Spreadsheet testSheet = getSpreadsheet(); + List<List<Object>> data = Collections.singletonList(Arrays.asList("A10", "B10", "C10")); + + assertThatGoogleApi(getGoogleApiTestServer()) + .appendValuesRequest(testSheet.getSpreadsheetId(), TEST_SHEET + "!A10", data) + .andReturnAppendResponse(TEST_SHEET + "!A10:C10"); + final Map<String, Object> headers = new HashMap<>(); // parameter type is String - headers.put("CamelGoogleSheets.spreadsheetId", testSheet.getSpreadsheetId()); + headers.put(GoogleSheetsConstants.PROPERTY_PREFIX + "spreadsheetId", testSheet.getSpreadsheetId()); // parameter type is String - headers.put("CamelGoogleSheets.range", TEST_SHEET + "!A10"); + headers.put(GoogleSheetsConstants.PROPERTY_PREFIX + "range", TEST_SHEET + "!A10"); // parameter type is com.google.api.services.sheets.v4.model.ValueRange - headers.put("CamelGoogleSheets.values", new ValueRange().setValues(Collections.singletonList(Arrays.asList("A10", "B10", "C10")))); + headers.put(GoogleSheetsConstants.PROPERTY_PREFIX + "values", new ValueRange().setValues(data)); // parameter type is String - headers.put("CamelGoogleSheets.valueInputOption", "USER_ENTERED"); + headers.put(GoogleSheetsConstants.PROPERTY_PREFIX + "valueInputOption", "USER_ENTERED"); final AppendValuesResponse result = requestBodyAndHeaders("direct://APPEND", null, headers); @@ -124,15 +158,32 @@ public class SheetsSpreadsheetsValuesIntegrationTest extends AbstractGoogleSheet @Test public void testClear() throws Exception { - Spreadsheet testSheet = getSpreadsheetWithTestData(); + String spreadsheetId = UUID.randomUUID().toString(); + + assertThatGoogleApi(getGoogleApiTestServer()) + .createSpreadsheetRequest() + .hasSheetTitle("TestData") + .andReturnSpreadsheet(spreadsheetId); + + Spreadsheet testSheet = getSpreadsheet(); + + assertThatGoogleApi(getGoogleApiTestServer()) + .updateValuesRequest(spreadsheetId, TEST_SHEET + "!A1:B2", Arrays.asList(Arrays.asList("a1", "b1"), Arrays.asList("a2", "b2"))) + .andReturnUpdateResponse(); + + applyTestData(testSheet); + + assertThatGoogleApi(getGoogleApiTestServer()) + .clearValuesRequest(testSheet.getSpreadsheetId(), TEST_SHEET + "!A1:B2") + .andReturnClearResponse(TEST_SHEET + "!A1:B2"); final Map<String, Object> headers = new HashMap<>(); // parameter type is String - headers.put("CamelGoogleSheets.spreadsheetId", testSheet.getSpreadsheetId()); + headers.put(GoogleSheetsConstants.PROPERTY_PREFIX + "spreadsheetId", testSheet.getSpreadsheetId()); // parameter type is String - headers.put("CamelGoogleSheets.range", TEST_SHEET + "!A1:B2"); + headers.put(GoogleSheetsConstants.PROPERTY_PREFIX + "range", TEST_SHEET + "!A1:B2"); // parameter type is com.google.api.services.sheets.v4.model.ClearValuesRequest - headers.put("CamelGoogleSheets.clearValuesRequest", new ClearValuesRequest()); + headers.put(GoogleSheetsConstants.PROPERTY_PREFIX + "clearValuesRequest", new ClearValuesRequest()); final ClearValuesResponse result = requestBodyAndHeaders("direct://CLEAR", null, headers); diff --git a/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/server/GoogleSheetsApiTestServer.java b/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/server/GoogleSheetsApiTestServer.java new file mode 100644 index 0000000..00bb91c --- /dev/null +++ b/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/server/GoogleSheetsApiTestServer.java @@ -0,0 +1,354 @@ +/* + * 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.server; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ReadListener; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.zip.GZIPInputStream; + +import com.consol.citrus.Citrus; +import com.consol.citrus.dsl.runner.DefaultTestRunner; +import com.consol.citrus.dsl.runner.TestRunner; +import com.consol.citrus.exceptions.CitrusRuntimeException; +import com.consol.citrus.http.server.HttpServer; +import com.consol.citrus.http.server.HttpServerBuilder; +import com.consol.citrus.http.servlet.GzipHttpServletResponseWrapper; +import com.consol.citrus.http.servlet.RequestCachingServletFilter; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; +import org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken; +import org.springframework.security.oauth2.provider.AuthorizationRequest; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationManager; +import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; +import org.springframework.security.oauth2.provider.client.InMemoryClientDetailsService; +import org.springframework.security.oauth2.provider.token.DefaultTokenServices; +import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; +import org.springframework.web.filter.OncePerRequestFilter; + +public class GoogleSheetsApiTestServer { + + private static Citrus citrus = Citrus.newInstance(); + + private final HttpServer httpServer; + private TestRunner runner; + + /** + * Prevent direct instantiation. + */ + private GoogleSheetsApiTestServer(HttpServer httpServer) { + super(); + this.httpServer = httpServer; + } + + /** + * Initialize new test run. + */ + public void init() { + runner = new DefaultTestRunner(citrus.getApplicationContext(), citrus.createTestContext()); + } + + /** + * Stop and reset current test run if any. + */ + public void reset() { + if (runner != null) { + runner.purgeEndpoints(action -> action.endpoint(httpServer)); + runner.stop(); + } + } + + /** + * Obtains the httpServer. + * @return + */ + public HttpServer getHttpServer() { + return httpServer; + } + + public void afterPropertiesSet() throws Exception { + httpServer.afterPropertiesSet(); + } + + public TestRunner getRunner() { + return runner; + } + + /** + * Builder builds server instance from given http server builder adding more setting options in fluent + * builder pattern style. + */ + public static class Builder { + private final HttpServerBuilder serverBuilder; + + private Path keyStorePath; + private String keyStorePassword; + private int securePort = 8443; + + private String basePath = ""; + + private String clientId; + private String clientSecret; + + private String accessToken; + private String refreshToken; + + public Builder(HttpServerBuilder serverBuilder) { + this.serverBuilder = serverBuilder; + } + + public Builder securePort(int securePort) { + this.securePort = securePort; + return this; + } + + public Builder keyStorePath(Path keyStorePath) { + this.keyStorePath = keyStorePath; + return this; + } + + public Builder keyStorePassword(String keyStorePass) { + this.keyStorePassword = keyStorePass; + return this; + } + + public Builder basePath(String basePath) { + this.basePath = basePath; + return this; + } + + public Builder clientId(String clientId) { + this.clientId = clientId; + return this; + } + + public Builder clientSecret(String clientSecret) { + this.clientSecret = clientSecret; + return this; + } + + public Builder accessToken(String accessToken) { + this.accessToken = accessToken; + return this; + } + + public Builder refreshToken(String refreshToken) { + this.refreshToken = refreshToken; + return this; + } + + public GoogleSheetsApiTestServer build() throws Exception { + SslContextFactory sslContextFactory = new SslContextFactory(true); + sslContextFactory.setKeyStorePath(keyStorePath.toAbsolutePath().toString()); + sslContextFactory.setKeyStorePassword(keyStorePassword); + + HttpConfiguration parent = new HttpConfiguration(); + parent.setSecureScheme("https"); + parent.setSecurePort(securePort); + HttpConfiguration httpConfiguration = new HttpConfiguration(parent); + httpConfiguration.setCustomizers(Collections.singletonList(new SecureRequestCustomizer())); + + ServerConnector sslConnector = new ServerConnector(new org.eclipse.jetty.server.Server(), + new SslConnectionFactory(sslContextFactory, "http/1.1"), + new HttpConnectionFactory(httpConfiguration)); + sslConnector.setPort(securePort); + + serverBuilder.connector(sslConnector); + + Map<String, Filter> filterMap = new LinkedHashMap<>(); + filterMap.put("request-caching-filter", new RequestCachingServletFilter()); + filterMap.put("gzip-filter", new GzipServletFilter()); + filterMap.put("oauth2-filter", oauth2Filter()); + + Map<String, String> filterMapings = new LinkedHashMap<>(); + filterMapings.put("oauth2-filter", "/" + Optional.ofNullable(basePath).map(path -> path + "/*").orElse("*")); + serverBuilder.filterMappings(filterMapings); + + serverBuilder.filters(filterMap); + + serverBuilder.applicationContext(citrus.getApplicationContext()); + + GoogleSheetsApiTestServer server = new GoogleSheetsApiTestServer(serverBuilder.build()); + server.afterPropertiesSet(); + return server; + } + + private Filter oauth2Filter() { + BaseClientDetails clientDetails = new BaseClientDetails(); + clientDetails.setClientId(clientId); + clientDetails.setClientSecret(clientSecret); + clientDetails.setAccessTokenValiditySeconds(3000); + clientDetails.setAutoApproveScopes(Arrays.asList("read", "write")); + clientDetails.setScope(Arrays.asList("read", "write")); + clientDetails.setAuthorities(Arrays.asList(new SimpleGrantedAuthority("client_credentials"), + new SimpleGrantedAuthority("authorization_code"), + new SimpleGrantedAuthority("password"), + new SimpleGrantedAuthority("refresh_token"))); + + OAuth2AuthenticationProcessingFilter filter = new OAuth2AuthenticationProcessingFilter(); + OAuth2AuthenticationManager oauth2AuthenticationManager = new OAuth2AuthenticationManager(); + + InMemoryClientDetailsService clientDetailsService = new InMemoryClientDetailsService(); + Map<String, ClientDetails> clientDetailsStore = new HashMap<>(); + clientDetailsStore.put(clientId, clientDetails); + clientDetailsService.setClientDetailsStore(clientDetailsStore); + oauth2AuthenticationManager.setClientDetailsService(clientDetailsService); + + InMemoryTokenStore tokenStore = new InMemoryTokenStore(); + AuthorizationRequest authorizationRequest = new AuthorizationRequest(); + authorizationRequest.setClientId(clientDetails.getClientId()); + authorizationRequest.setAuthorities(clientDetails.getAuthorities()); + authorizationRequest.setApproved(true); + + OAuth2Authentication authentication = new OAuth2Authentication(authorizationRequest.createOAuth2Request(), null); + + tokenStore.storeAccessToken(new DefaultOAuth2AccessToken(accessToken), authentication); + tokenStore.storeRefreshToken(new DefaultOAuth2RefreshToken(refreshToken), authentication); + + DefaultTokenServices tokenServices = new DefaultTokenServices(); + tokenServices.setTokenStore(tokenStore); + tokenServices.setClientDetailsService(clientDetailsService); + tokenServices.setSupportRefreshToken(true); + oauth2AuthenticationManager.setTokenServices(tokenServices); + + filter.setAuthenticationManager(oauth2AuthenticationManager); + return filter; + } + } + + private static class GzipServletFilter extends OncePerRequestFilter { + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + HttpServletRequest filteredRequest = request; + HttpServletResponse filteredResponse = response; + + String contentEncoding = request.getHeader(HttpHeaders.CONTENT_ENCODING); + if (contentEncoding != null && contentEncoding.contains("gzip")) { + filteredRequest = new GzipHttpServletRequestWrapper(request); + } + + String acceptEncoding = request.getHeader(HttpHeaders.ACCEPT_ENCODING); + if (acceptEncoding != null && acceptEncoding.contains("gzip")) { + filteredResponse = new GzipHttpServletResponseWrapper(response); + } + + filterChain.doFilter(filteredRequest, filteredResponse); + + if (filteredResponse instanceof GzipHttpServletResponseWrapper) { + ((GzipHttpServletResponseWrapper) filteredResponse).finish(); + } + } + } + + private static class GzipHttpServletRequestWrapper extends HttpServletRequestWrapper { + /** + * Constructs a request adaptor wrapping the given request. + * + * @param request + * @throws IllegalArgumentException if the request is null + */ + public GzipHttpServletRequestWrapper(HttpServletRequest request) { + super(request); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + return new GzipServletInputStream(getRequest()); + } + + /** + * Gzip enabled servlet input stream. + */ + private static class GzipServletInputStream extends ServletInputStream { + private final GZIPInputStream gzipStream; + + /** + * Default constructor using wrapped input stream. + * + * @param request + * @throws IOException + */ + public GzipServletInputStream(ServletRequest request) throws IOException { + super(); + gzipStream = new GZIPInputStream(request.getInputStream()); + } + + @Override + public boolean isFinished() { + try { + return gzipStream.available() == 0; + } catch (IOException e) { + throw new CitrusRuntimeException("Failed to check gzip intput stream availability", e); + } + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(final ReadListener readListener) { + throw new UnsupportedOperationException("Unsupported operation"); + } + + @Override + public int read() { + try { + return gzipStream.read(); + } catch (IOException e) { + throw new CitrusRuntimeException("Failed to read gzip input stream", e); + } + } + + @Override + public int read(byte[] b) throws IOException { + return gzipStream.read(b); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return gzipStream.read(b, off, len); + } + } + } +} diff --git a/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/server/GoogleSheetsApiTestServerAssert.java b/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/server/GoogleSheetsApiTestServerAssert.java new file mode 100644 index 0000000..a89edae --- /dev/null +++ b/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/server/GoogleSheetsApiTestServerAssert.java @@ -0,0 +1,405 @@ +/* + * 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.server; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +import com.consol.citrus.message.MessageType; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.google.api.services.sheets.v4.model.Spreadsheet; +import com.google.api.services.sheets.v4.model.ValueRange; +import org.apache.camel.util.ObjectHelper; +import org.assertj.core.api.AbstractAssert; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +public class GoogleSheetsApiTestServerAssert extends AbstractAssert<GoogleSheetsApiTestServerAssert, GoogleSheetsApiTestServer> { + + private ObjectMapper mapper = new ObjectMapper() + .setDefaultPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_EMPTY, JsonInclude.Include.NON_EMPTY)) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING) + .enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING) + .disable(JsonParser.Feature.AUTO_CLOSE_SOURCE); + + private GoogleSheetsApiTestServerAssert(GoogleSheetsApiTestServer server) { + super(server, GoogleSheetsApiTestServerAssert.class); + } + + /** + * A fluent entry point to the assertion class. + * @param server the target server to perform assertions to. + * @return + */ + public static GoogleSheetsApiTestServerAssert assertThatGoogleApi(GoogleSheetsApiTestServer server) { + return new GoogleSheetsApiTestServerAssert(server); + } + + public GetSpreadsheetAssert getSpreadsheetRequest(String spreadsheetId) { + return new GetSpreadsheetAssert(spreadsheetId); + } + + public void isRunning() { + isRunning(5000, TimeUnit.MILLISECONDS); + } + + public void isRunning(long timeout, TimeUnit timeUnit) { + ScheduledFuture<?> schedule = null; + try { + CompletableFuture<Boolean> runningProbe = new CompletableFuture<>(); + schedule = Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> { + if (actual.getHttpServer().isRunning()) { + runningProbe.complete(true); + } + }, 0, timeout / 10, timeUnit); + + runningProbe.get(timeout, timeUnit); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new IllegalStateException(e); + } finally { + Optional.ofNullable(schedule) + .ifPresent(future -> future.cancel(true)); + } + } + + public class GetSpreadsheetAssert { + GetSpreadsheetAssert(String spreadsheetId) { + actual.getRunner().createVariable("spreadsheetId", spreadsheetId); + } + + public void andReturnSpreadsheet(Spreadsheet spreadsheet) throws IOException { + String spreadsheetJson = spreadsheet.toPrettyString(); + actual.getRunner().async().actions( + actual.getRunner().http(action -> action.server(actual.getHttpServer()) + .receive() + .get("/v4/spreadsheets/${spreadsheetId}")), + actual.getRunner().http(action -> action.server(actual.getHttpServer()) + .send() + .response(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .payload(spreadsheetJson)) + ); + } + } + + public ClearValuesAssert clearValuesRequest(String spreadsheetId, String range) { + return new ClearValuesAssert(spreadsheetId, range); + } + + public class ClearValuesAssert { + ClearValuesAssert(String spreadsheetId, String range) { + actual.getRunner().createVariable("spreadsheetId", spreadsheetId); + actual.getRunner().createVariable("range", range); + } + + public void andReturnClearResponse(String clearedRange) throws IOException { + actual.getRunner().async().actions( + actual.getRunner().http(action -> action.server(actual.getHttpServer()) + .receive() + .post("/v4/spreadsheets/${spreadsheetId}/values/${range}:clear")), + actual.getRunner().http(action -> action.server(actual.getHttpServer()) + .send() + .response(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .payload("{" + + "\"spreadsheetId\": \"${spreadsheetId}\"," + + "\"clearedRange\": \"" + clearedRange + "\"" + + "}")) + ); + } + } + + public UpdateValuesAssert updateValuesRequest(String spreadsheetId, String range, List<List<Object>> data) { + return new UpdateValuesAssert(spreadsheetId, range, data); + } + + public class UpdateValuesAssert { + private final List<List<Object>> data; + + UpdateValuesAssert(String spreadsheetId, String range, List<List<Object>> data) { + actual.getRunner().createVariable("spreadsheetId", spreadsheetId); + actual.getRunner().createVariable("range", range); + this.data = data; + } + + public void andReturnUpdateResponse() throws IOException { + String valuesJson = mapper.writer().writeValueAsString(data); + + actual.getRunner().async().actions( + actual.getRunner().http(action -> action.server(actual.getHttpServer()) + .receive() + .put("/v4/spreadsheets/${spreadsheetId}/values/${range}") + .validate("$.values.toString()", valuesJson)), + actual.getRunner().http(action -> action.server(actual.getHttpServer()) + .send() + .response(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .payload("{" + + "\"spreadsheetId\": \"${spreadsheetId}\"," + + "\"updatedRange\": \"${range}\"," + + "\"updatedRows\": " + data.size() + "," + + "\"updatedColumns\": " + Optional.ofNullable(data.get(0)).map(Collection::size).orElse(0) + "," + + "\"updatedCells\": " + data.size() * Optional.ofNullable(data.get(0)).map(Collection::size).orElse(0) + + "}")) + ); + } + } + + public AppendValuesAssert appendValuesRequest(String spreadsheetId, String range, List<List<Object>> data) { + return new AppendValuesAssert(spreadsheetId, range, data); + } + + public class AppendValuesAssert { + private final List<List<Object>> data; + + AppendValuesAssert(String spreadsheetId, String range, List<List<Object>> data) { + actual.getRunner().createVariable("spreadsheetId", spreadsheetId); + actual.getRunner().createVariable("range", range); + this.data = data; + } + + public void andReturnAppendResponse(String updatedRange) throws IOException { + String valuesJson = mapper.writer().writeValueAsString(data); + + actual.getRunner().async().actions( + actual.getRunner().http(action -> action.server(actual.getHttpServer()) + .receive() + .post("/v4/spreadsheets/${spreadsheetId}/values/${range}:append") + .validate("$.values.toString()", valuesJson)), + actual.getRunner().http(action -> action.server(actual.getHttpServer()) + .send() + .response(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .payload("{" + + "\"spreadsheetId\": \"${spreadsheetId}\"," + + "\"updates\":" + + "{" + + "\"spreadsheetId\": \"${spreadsheetId}\"," + + "\"updatedRange\": \"" + updatedRange + "\"," + + "\"updatedRows\": " + data.size() + "," + + "\"updatedColumns\": " + Optional.ofNullable(data.get(0)).map(Collection::size).orElse(0) + "," + + "\"updatedCells\": " + data.size() * Optional.ofNullable(data.get(0)).map(Collection::size).orElse(0) + + "}" + + "}")) + ); + } + } + + public GetValuesAssert getValuesRequest(String spreadsheetId, String range) { + return new GetValuesAssert(spreadsheetId, range); + } + + public class GetValuesAssert { + GetValuesAssert(String spreadsheetId, String range) { + actual.getRunner().createVariable("spreadsheetId", spreadsheetId); + actual.getRunner().createVariable("range", range); + } + + public void andReturnValueRange(ValueRange valueRange) throws IOException { + String valueJson = valueRange.toPrettyString(); + actual.getRunner().async().actions( + actual.getRunner().http(action -> action.server(actual.getHttpServer()) + .receive() + .get("/v4/spreadsheets/${spreadsheetId}/values/${range}")), + actual.getRunner().http(action -> action.server(actual.getHttpServer()) + .send() + .response(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .payload(valueJson)) + ); + } + + public void andReturnValues(List<List<Object>> data) throws JsonProcessingException { + String valueRangeJson; + if (ObjectHelper.isEmpty(data)) { + valueRangeJson = "{" + + "\"range\": \"${range}\"," + + "\"majorDimension\": \"ROWS\"" + + "}"; + } else { + valueRangeJson = "{" + + "\"range\": \"${range}\"," + + "\"majorDimension\": \"ROWS\"," + + "\"values\":" + mapper.writer().writeValueAsString(data) + + "}"; + } + + actual.getRunner().async().actions( + actual.getRunner().http(action -> action.server(actual.getHttpServer()) + .receive() + .get("/v4/spreadsheets/${spreadsheetId}/values/${range}")), + actual.getRunner().http(action -> action.server(actual.getHttpServer()) + .send() + .response(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .payload(valueRangeJson)) + ); + } + } + + public BatchGetValuesAssert batchGetValuesRequest(String spreadsheetId, String range) { + return new BatchGetValuesAssert(spreadsheetId, range); + } + + public class BatchGetValuesAssert { + BatchGetValuesAssert(String spreadsheetId, String range) { + actual.getRunner().createVariable("spreadsheetId", spreadsheetId); + actual.getRunner().createVariable("range", range); + } + + public void andReturnValues(List<List<Object>> data) throws JsonProcessingException { + String valueRangeJson; + if (ObjectHelper.isEmpty(data)) { + valueRangeJson = "{\"spreadsheetId\": \"${spreadsheetId}\"," + + "\"valueRanges\": [" + + "{" + + "\"range\": \"${range}\"," + + "\"majorDimension\": \"ROWS\"" + + "}" + + "]}"; + } else { + valueRangeJson = "{\"spreadsheetId\": \"${spreadsheetId}\"," + + "\"valueRanges\": [" + + "{" + + "\"range\": \"${range}\"," + + "\"majorDimension\": \"ROWS\"," + + "\"values\":" + mapper.writer().writeValueAsString(data) + + "}" + + "]}"; + } + + actual.getRunner().async().actions( + actual.getRunner().http(action -> action.server(actual.getHttpServer()) + .receive() + .get("/v4/spreadsheets/${spreadsheetId}/values:batchGet")), + actual.getRunner().http(action -> action.server(actual.getHttpServer()) + .send() + .response(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .payload(valueRangeJson)) + ); + } + } + + public CreateSpreadsheetAssert createSpreadsheetRequest() { + return new CreateSpreadsheetAssert(); + } + + public class CreateSpreadsheetAssert { + private String title = "@ignore@"; + private String sheetTitle; + + public CreateSpreadsheetAssert hasTitle(String title) { + this.title = title; + return this; + } + + public CreateSpreadsheetAssert hasSheetTitle(String sheetTitle) { + this.sheetTitle = sheetTitle; + return this; + } + + public void andReturnRandomSpreadsheet() { + andReturnSpreadsheet("citrus:randomString(44)"); + } + + public void andReturnSpreadsheet(String spreadsheetId) { + actual.getRunner().createVariable("spreadsheetId", spreadsheetId); + actual.getRunner().createVariable("title", title); + + String spreadsheetJson; + if (ObjectHelper.isNotEmpty(sheetTitle)) { + actual.getRunner().createVariable("sheetTitle", sheetTitle); + spreadsheetJson = "{\"properties\":{\"title\":\"${title}\"},\"sheets\":[{\"properties\":{\"title\":\"${sheetTitle}\"}}]}"; + } else { + spreadsheetJson = "{\"properties\":{\"title\":\"${title}\"}}"; + } + + actual.getRunner().async().actions( + actual.getRunner().http(action -> action.server(actual.getHttpServer()) + .receive() + .post("/v4/spreadsheets") + .name("create.request") + .messageType(MessageType.JSON) + .payload(spreadsheetJson)), + actual.getRunner().http(action -> action.server(actual.getHttpServer()) + .send() + .response(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .payload("{\"spreadsheetId\":\"${spreadsheetId}\",\"properties\":{\"title\":\"citrus:jsonPath(citrus:message(create.request.payload()), '$.properties.title')\"}}")) + ); + } + } + + public BatchUpdateSpreadsheetAssert batchUpdateSpreadsheetRequest(String spreadsheetId) { + return new BatchUpdateSpreadsheetAssert(spreadsheetId); + } + + public class BatchUpdateSpreadsheetAssert { + private List<String> fields = new ArrayList<>(); + + BatchUpdateSpreadsheetAssert(String spreadsheetId) { + actual.getRunner().createVariable("spreadsheetId", spreadsheetId); + } + + public BatchUpdateSpreadsheetAssert updateTitle(String title) { + actual.getRunner().createVariable("title", title); + fields.add("title"); + return this; + } + + public void andReturnUpdated() { + actual.getRunner().async().actions( + actual.getRunner().http(action -> action.server(actual.getHttpServer()) + .receive() + .post("/v4/spreadsheets/${spreadsheetId}:batchUpdate") + .messageType(MessageType.JSON) + .payload("{" + + "\"includeSpreadsheetInResponse\":true," + + "\"requests\":[" + + "{" + + "\"updateSpreadsheetProperties\": {" + + "\"fields\":\"" + String.join(",", fields) + "\"," + + "\"properties\":{" + fields.stream().map(field -> String.format("\"%s\":\"${%s}\"", field, field)).collect(Collectors.joining(",")) + "}" + + "}" + + "}" + + "]}")), + actual.getRunner().http(action -> action.server(actual.getHttpServer()) + .send() + .response(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .payload("{\"spreadsheetId\":\"${spreadsheetId}\",\"updatedSpreadsheet\":{\"properties\":{\"title\":\"${title}\"},\"spreadsheetId\":\"${spreadsheetId}\"}}")) + ); + } + } +} diff --git a/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/server/GoogleSheetsApiTestServerRule.java b/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/server/GoogleSheetsApiTestServerRule.java new file mode 100644 index 0000000..7ecdc53 --- /dev/null +++ b/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/server/GoogleSheetsApiTestServerRule.java @@ -0,0 +1,116 @@ +/* + * 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.server; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import com.consol.citrus.dsl.endpoint.CitrusEndpoints; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.HttpStatus; +import org.springframework.util.SocketUtils; + +import static org.apache.camel.component.google.sheets.server.GoogleSheetsApiTestServerAssert.assertThatGoogleApi; + +public class GoogleSheetsApiTestServerRule implements TestRule { + + public static final String SERVER_KEYSTORE = "googleapis.jks"; + public static final String SERVER_KEYSTORE_PASSWORD = "secret"; + + private GoogleSheetsApiTestServer googleApiTestServer; + private int serverPort = SocketUtils.findAvailableTcpPort(); + + public GoogleSheetsApiTestServerRule(String optionFile) { + try { + Map<String, Object> testOptions = getTestOptions(optionFile); + + googleApiTestServer = new GoogleSheetsApiTestServer.Builder(CitrusEndpoints.http() + .server() + .port(serverPort) + .timeout(15000) + .defaultStatus(HttpStatus.REQUEST_TIMEOUT) + .autoStart(true)) + .keyStorePath(new ClassPathResource(SERVER_KEYSTORE).getFile().toPath()) + .keyStorePassword(SERVER_KEYSTORE_PASSWORD) + .securePort(serverPort) + .clientId(testOptions.get("clientId").toString()) + .clientSecret(testOptions.get("clientSecret").toString()) + .accessToken(testOptions.get("accessToken").toString()) + .refreshToken(testOptions.get("refreshToken").toString()) + .build(); + + assertThatGoogleApi(googleApiTestServer).isRunning(); + } catch (Exception e) { + throw new IllegalStateException("Error while reading server keystore file", e); + } + } + + @Override + public Statement apply(Statement base, Description description) { + return new GoogleSheetsApiTestServerStatement(base); + } + + /** + * Read component configuration from TEST_OPTIONS_PROPERTIES. + * @return Map of component options. + */ + private Map<String, Object> getTestOptions(String optionFile) throws IOException { + final Properties properties = new Properties(); + properties.load(getClass().getResourceAsStream(optionFile)); + + Map<String, Object> options = new HashMap<>(); + for (Map.Entry<Object, Object> entry : properties.entrySet()) { + options.put(entry.getKey().toString(), entry.getValue()); + } + + return options; + } + + /** + * Rule statement initializes and resets test server after each method. + */ + private class GoogleSheetsApiTestServerStatement extends Statement { + private final Statement base; + + GoogleSheetsApiTestServerStatement( Statement base ) { + this.base = base; + } + + @Override + public void evaluate() throws Throwable { + googleApiTestServer.init(); + try { + base.evaluate(); + } finally { + googleApiTestServer.reset(); + } + } + } + + public GoogleSheetsApiTestServer getGoogleApiTestServer() { + return googleApiTestServer; + } + + public int getServerPort() { + return serverPort; + } +} diff --git a/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/stream/AbstractGoogleSheetsStreamTestSupport.java b/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/stream/AbstractGoogleSheetsStreamTestSupport.java index 1e9bf35..fe32ae6 100644 --- a/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/stream/AbstractGoogleSheetsStreamTestSupport.java +++ b/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/stream/AbstractGoogleSheetsStreamTestSupport.java @@ -16,8 +16,13 @@ */ package org.apache.camel.component.google.sheets.stream; +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.AbstractGoogleSheetsTestSupport; +import org.apache.camel.component.google.sheets.BatchGoogleSheetsClientFactory; +import org.apache.camel.component.google.sheets.server.GoogleSheetsApiTestServerRule; import org.apache.camel.support.PropertyBindingSupport; /** @@ -36,6 +41,18 @@ public class AbstractGoogleSheetsStreamTestSupport extends AbstractGoogleSheetsT // add GoogleSheetsComponent to Camel context final GoogleSheetsStreamComponent component = new GoogleSheetsStreamComponent(context); + component.setClientFactory(new BatchGoogleSheetsClientFactory( + new NetHttpTransport.Builder() + .trustCertificatesFromJavaKeyStore( + getClass().getResourceAsStream("/" + GoogleSheetsApiTestServerRule.SERVER_KEYSTORE), + GoogleSheetsApiTestServerRule.SERVER_KEYSTORE_PASSWORD) + .build(), + new JacksonFactory()) { + @Override + protected void configure(Sheets.Builder clientBuilder) { + clientBuilder.setRootUrl(String.format("https://localhost:%s/", googleSheetsApiTestServerRule.getServerPort())); + } + }); component.setConfiguration(configuration); context.addComponent("google-sheets-stream", component); diff --git a/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/stream/SheetsStreamConsumerIntegrationTest.java b/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/stream/SheetsStreamConsumerIntegrationTest.java index 113d077..3e9c5a8 100644 --- a/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/stream/SheetsStreamConsumerIntegrationTest.java +++ b/components/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/stream/SheetsStreamConsumerIntegrationTest.java @@ -16,6 +16,10 @@ */ package org.apache.camel.component.google.sheets.stream; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + import com.google.api.services.sheets.v4.model.Spreadsheet; import com.google.api.services.sheets.v4.model.ValueRange; import org.apache.camel.Exchange; @@ -24,20 +28,44 @@ import org.apache.camel.component.mock.MockEndpoint; import org.junit.Assert; import org.junit.Test; +import static org.apache.camel.component.google.sheets.server.GoogleSheetsApiTestServerAssert.assertThatGoogleApi; import static org.apache.camel.component.google.sheets.stream.GoogleSheetsStreamConstants.MAJOR_DIMENSION; import static org.apache.camel.component.google.sheets.stream.GoogleSheetsStreamConstants.RANGE; +import static org.apache.camel.component.google.sheets.stream.GoogleSheetsStreamConstants.RANGE_INDEX; import static org.apache.camel.component.google.sheets.stream.GoogleSheetsStreamConstants.SPREADSHEET_ID; +import static org.apache.camel.component.google.sheets.stream.GoogleSheetsStreamConstants.VALUE_INDEX; public class SheetsStreamConsumerIntegrationTest extends AbstractGoogleSheetsStreamTestSupport { - private String range = "A1:B2"; + private String range = TEST_SHEET + "!A1:B2"; @Test public void testConsumeValueRange() throws Exception { - Spreadsheet testSheet = getSpreadsheetWithTestData(); + String spreadsheetId = UUID.randomUUID().toString(); + + assertThatGoogleApi(getGoogleApiTestServer()) + .createSpreadsheetRequest() + .hasSheetTitle("TestData") + .andReturnSpreadsheet(spreadsheetId); + + Spreadsheet testSheet = getSpreadsheet(); + + List<List<Object>> data = Arrays.asList( + Arrays.asList("a1", "b1"), + Arrays.asList("a2", "b2") + ); + + assertThatGoogleApi(getGoogleApiTestServer()) + .updateValuesRequest(spreadsheetId, range, data) + .andReturnUpdateResponse(); - context().addRoutes(createGoogleStreamRouteBuilder(testSheet.getSpreadsheetId())); - context().start(); + applyTestData(testSheet); + + assertThatGoogleApi(getGoogleApiTestServer()) + .batchGetValuesRequest(testSheet.getSpreadsheetId(), range) + .andReturnValues(data); + + context().addRoutes(createGoogleStreamRouteBuilder(testSheet.getSpreadsheetId(), false)); MockEndpoint mock = getMockEndpoint("mock:result"); mock.expectedMinimumMessageCount(1); @@ -46,9 +74,11 @@ public class SheetsStreamConsumerIntegrationTest extends AbstractGoogleSheetsStr Exchange exchange = mock.getReceivedExchanges().get(0); Assert.assertTrue(exchange.getIn().getHeaders().containsKey(SPREADSHEET_ID)); Assert.assertTrue(exchange.getIn().getHeaders().containsKey(RANGE)); + Assert.assertTrue(exchange.getIn().getHeaders().containsKey(RANGE_INDEX)); Assert.assertTrue(exchange.getIn().getHeaders().containsKey(MAJOR_DIMENSION)); Assert.assertEquals(testSheet.getSpreadsheetId(), exchange.getIn().getHeaders().get(SPREADSHEET_ID)); - Assert.assertEquals(TEST_SHEET + "!" + range, exchange.getIn().getHeaders().get(RANGE)); + Assert.assertEquals(range, exchange.getIn().getHeaders().get(RANGE)); + Assert.assertEquals(1, exchange.getIn().getHeaders().get(RANGE_INDEX)); Assert.assertEquals("ROWS", exchange.getIn().getHeaders().get(MAJOR_DIMENSION)); ValueRange values = (ValueRange) exchange.getIn().getBody(); @@ -59,12 +89,79 @@ public class SheetsStreamConsumerIntegrationTest extends AbstractGoogleSheetsStr Assert.assertEquals("b2", values.getValues().get(1).get(1)); } - private RouteBuilder createGoogleStreamRouteBuilder(String spreadsheetId) throws Exception { + @Test + public void testConsumeValueRangeSplitResults() throws Exception { + String spreadsheetId = UUID.randomUUID().toString(); + + assertThatGoogleApi(getGoogleApiTestServer()) + .createSpreadsheetRequest() + .hasSheetTitle("TestData") + .andReturnSpreadsheet(spreadsheetId); + + Spreadsheet testSheet = getSpreadsheet(); + + List<List<Object>> data = Arrays.asList( + Arrays.asList("a1", "b1"), + Arrays.asList("a2", "b2") + ); + + assertThatGoogleApi(getGoogleApiTestServer()) + .updateValuesRequest(spreadsheetId, range, data) + .andReturnUpdateResponse(); + + applyTestData(testSheet); + + assertThatGoogleApi(getGoogleApiTestServer()) + .batchGetValuesRequest(testSheet.getSpreadsheetId(), range) + .andReturnValues(data); + + context().addRoutes(createGoogleStreamRouteBuilder(testSheet.getSpreadsheetId(), true)); + context().getRouteController().startRoute("google-stream-test"); + + MockEndpoint mock = getMockEndpoint("mock:result"); + mock.expectedMinimumMessageCount(2); + assertMockEndpointsSatisfied(); + + Exchange exchange = mock.getReceivedExchanges().get(0); + Assert.assertTrue(exchange.getIn().getHeaders().containsKey(SPREADSHEET_ID)); + Assert.assertTrue(exchange.getIn().getHeaders().containsKey(RANGE)); + Assert.assertTrue(exchange.getIn().getHeaders().containsKey(RANGE_INDEX)); + Assert.assertTrue(exchange.getIn().getHeaders().containsKey(VALUE_INDEX)); + Assert.assertTrue(exchange.getIn().getHeaders().containsKey(MAJOR_DIMENSION)); + Assert.assertEquals(testSheet.getSpreadsheetId(), exchange.getIn().getHeaders().get(SPREADSHEET_ID)); + Assert.assertEquals(range, exchange.getIn().getHeaders().get(RANGE)); + Assert.assertEquals(1, exchange.getIn().getHeaders().get(RANGE_INDEX)); + Assert.assertEquals(1, exchange.getIn().getHeaders().get(VALUE_INDEX)); + Assert.assertEquals("ROWS", exchange.getIn().getHeaders().get(MAJOR_DIMENSION)); + + List<?> values = (List) exchange.getIn().getBody(); + Assert.assertEquals(2L, values.size()); + Assert.assertEquals("a1", values.get(0)); + Assert.assertEquals("b1", values.get(1)); + + exchange = mock.getReceivedExchanges().get(1); + Assert.assertTrue(exchange.getIn().getHeaders().containsKey(SPREADSHEET_ID)); + Assert.assertTrue(exchange.getIn().getHeaders().containsKey(RANGE)); + Assert.assertTrue(exchange.getIn().getHeaders().containsKey(RANGE_INDEX)); + Assert.assertTrue(exchange.getIn().getHeaders().containsKey(VALUE_INDEX)); + Assert.assertTrue(exchange.getIn().getHeaders().containsKey(MAJOR_DIMENSION)); + Assert.assertEquals(testSheet.getSpreadsheetId(), exchange.getIn().getHeaders().get(SPREADSHEET_ID)); + Assert.assertEquals(1, exchange.getIn().getHeaders().get(RANGE_INDEX)); + Assert.assertEquals(2, exchange.getIn().getHeaders().get(VALUE_INDEX)); + + values = (List) exchange.getIn().getBody(); + Assert.assertEquals(2L, values.size()); + Assert.assertEquals("a2", values.get(0)); + Assert.assertEquals("b2", values.get(1)); + } + + private RouteBuilder createGoogleStreamRouteBuilder(String spreadsheetId, boolean splitResults) throws Exception { return new RouteBuilder() { @Override public void configure() { - from("google-sheets-stream://data?spreadsheetId=" + spreadsheetId - + "&range=" + range + "&delay=2000&maxResults=5&splitResults=true").routeId("google-stream-values-test").to("mock:rows"); + from(String.format("google-sheets-stream://data?spreadsheetId=%s&range=%s&delay=20000&maxResults=5&splitResults=%s", spreadsheetId, range, splitResults)) + .routeId("google-stream-test") + .to("mock:result"); } }; } diff --git a/components/camel-google-sheets/src/test/resources/googleapis.jks b/components/camel-google-sheets/src/test/resources/googleapis.jks new file mode 100644 index 0000000..0d6097c Binary files /dev/null and b/components/camel-google-sheets/src/test/resources/googleapis.jks differ diff --git a/components/camel-google-sheets/src/test/resources/test-options.properties b/components/camel-google-sheets/src/test/resources/test-options.properties index b21908c..f2e4a6f 100644 --- a/components/camel-google-sheets/src/test/resources/test-options.properties +++ b/components/camel-google-sheets/src/test/resources/test-options.properties @@ -19,8 +19,8 @@ ## Login properties for Google Sheets Component ##################################### ## Application client id and secret -clientId= -clientSecret= +clientId=syndesis-client +clientSecret=syndesis applicationName=camel-google-sheets/1.0 -accessToken= -refreshToken= +accessToken=cd887efc-7c7d-4e8e-9580-f7502123badf +refreshToken=bdbbe5ec-6081-4c6c-8974-9c4abfc0fdcc diff --git a/parent/pom.xml b/parent/pom.xml index 5814302..c12e094 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -124,6 +124,7 @@ <cglib-version>3.2.12</cglib-version> <chunk-templates-version>3.5.0</chunk-templates-version> <chunk-templates-bundle-version>3.5.0_1</chunk-templates-bundle-version> + <citrus.version>2.8.0</citrus.version> <cmis-version>1.1.0</cmis-version> <cometd-bayeux-version>6.1.11</cometd-bayeux-version> <cometd-java-client-version>3.1.2</cometd-java-client-version> diff --git a/platforms/spring-boot/components-starter/camel-google-sheets-starter/src/main/java/org/apache/camel/component/google/sheets/stream/springboot/GoogleSheetsStreamComponentConfiguration.java b/platforms/spring-boot/components-starter/camel-google-sheets-starter/src/main/java/org/apache/camel/component/google/sheets/stream/springboot/GoogleSheetsStreamComponentConfiguration.java index bfb787a..89fd12f 100644 --- a/platforms/spring-boot/components-starter/camel-google-sheets-starter/src/main/java/org/apache/camel/component/google/sheets/stream/springboot/GoogleSheetsStreamComponentConfiguration.java +++ b/platforms/spring-boot/components-starter/camel-google-sheets-starter/src/main/java/org/apache/camel/component/google/sheets/stream/springboot/GoogleSheetsStreamComponentConfiguration.java @@ -140,7 +140,7 @@ public class GoogleSheetsStreamComponentConfiguration * number of rows in a returned value range data set or the number of * returned value ranges in a batch request. */ - private Integer maxResults = 10; + private Integer maxResults = 0; /** * Specifies the range of rows and columns in a sheet to get data from. */ @@ -157,6 +157,13 @@ public class GoogleSheetsStreamComponentConfiguration * True if grid data should be returned. */ private Boolean includeGridData = false; + /** + * True if value range result should be split into rows or columns to + * process each of them individually. When true each row or column is + * represented with a separate exchange in batch processing. Otherwise + * value range object is used as exchange junk size. + */ + private Boolean splitResults = false; public String getClientId() { return clientId; @@ -261,5 +268,13 @@ public class GoogleSheetsStreamComponentConfiguration public void setIncludeGridData(Boolean includeGridData) { this.includeGridData = includeGridData; } + + public Boolean getSplitResults() { + return splitResults; + } + + public void setSplitResults(Boolean splitResults) { + this.splitResults = splitResults; + } } } \ No newline at end of file