This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new f6fe70c71752 CAMEL-26581: camel-google-calendar - follow pages when
polling by last update (#29161)
f6fe70c71752 is described below
commit f6fe70c71752fe19f72f36fa7f857b0ea8fb5d6c
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Nov 4 16:27:20 2026 +0100
CAMEL-26581: camel-google-calendar - follow pages when polling by last
update (#29161)
Co-authored-by: OpenAI Codex <[email protected]>
---
.../catalog/components/google-calendar-stream.json | 4 +-
.../docs/google-calendar-stream-component.adoc | 5 +
.../calendar/stream/google-calendar-stream.json | 4 +-
.../docs/google-calendar-stream-component.adoc | 5 +
.../stream/GoogleCalendarStreamConfiguration.java | 3 +-
.../stream/GoogleCalendarStreamConsumer.java | 14 +-
...GoogleCalendarStreamConsumerPaginationTest.java | 188 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 7 +
...oogleCalendarStreamComponentBuilderFactory.java | 4 +-
...GoogleCalendarStreamEndpointBuilderFactory.java | 8 +-
10 files changed, 233 insertions(+), 9 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/google-calendar-stream.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/google-calendar-stream.json
index d46bb7ee6b4e..29958135f687 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/google-calendar-stream.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/google-calendar-stream.json
@@ -32,7 +32,7 @@
"considerLastUpdate": { "index": 5, "kind": "property", "displayName":
"Consider Last Update", "group": "consumer", "label": "", "required": false,
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Take into account the
lastUpdate of the last event po [...]
"consumeFromNow": { "index": 6, "kind": "property", "displayName":
"Consume From Now", "group": "consumer", "label": "", "required": false,
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": true, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Consume events in the
selected calendar from now on" },
"delegate": { "index": 7, "kind": "property", "displayName": "Delegate",
"group": "consumer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Delegate for
wide-domain service account" },
- "maxResults": { "index": 8, "kind": "property", "displayName": "Max
Results", "group": "consumer", "label": "", "required": false, "type":
"integer", "javaType": "int", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": 10, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Max results to be
returned" },
+ "maxResults": { "index": 8, "kind": "property", "displayName": "Max
Results", "group": "consumer", "label": "", "required": false, "type":
"integer", "javaType": "int", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": 10, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Maximum number of
events returned per Google Calendar API page. With syncFlow [...]
"query": { "index": 9, "kind": "property", "displayName": "Query",
"group": "consumer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "The query to execute on
calendar" },
"scopes": { "index": 10, "kind": "property", "displayName": "Scopes",
"group": "consumer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Specifies the level of
permissions you want a calendar application to have to a user account. [...]
"syncFlow": { "index": 11, "kind": "property", "displayName": "Sync Flow",
"group": "consumer", "label": "", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Sync events, see
https:\/\/developers.google.com\/calendar\/v3\/sync Note [...]
@@ -62,7 +62,7 @@
"considerLastUpdate": { "index": 4, "kind": "parameter", "displayName":
"Consider Last Update", "group": "consumer", "label": "", "required": false,
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Take into account the
lastUpdate of the last event p [...]
"consumeFromNow": { "index": 5, "kind": "parameter", "displayName":
"Consume From Now", "group": "consumer", "label": "", "required": false,
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": true, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Consume events in the
selected calendar from now on" },
"delegate": { "index": 6, "kind": "parameter", "displayName": "Delegate",
"group": "consumer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Delegate for
wide-domain service account" },
- "maxResults": { "index": 7, "kind": "parameter", "displayName": "Max
Results", "group": "consumer", "label": "", "required": false, "type":
"integer", "javaType": "int", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": 10, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Max results to be
returned" },
+ "maxResults": { "index": 7, "kind": "parameter", "displayName": "Max
Results", "group": "consumer", "label": "", "required": false, "type":
"integer", "javaType": "int", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": 10, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Maximum number of
events returned per Google Calendar API page. With syncFlo [...]
"query": { "index": 8, "kind": "parameter", "displayName": "Query",
"group": "consumer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "The query to execute on
calendar" },
"scopes": { "index": 9, "kind": "parameter", "displayName": "Scopes",
"group": "consumer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Specifies the level of
permissions you want a calendar application to have to a user account. [...]
"sendEmptyMessageWhenIdle": { "index": 10, "kind": "parameter",
"displayName": "Send Empty Message When Idle", "group": "consumer", "label":
"consumer", "required": false, "type": "boolean", "javaType": "boolean",
"deprecated": false, "autowired": false, "secret": false, "defaultValue":
false, "description": "If the polling consumer did not poll any files, you can
enable this option to send an empty message (no body) instead." },
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-calendar-stream-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-calendar-stream-component.adoc
index 9240d11cf45d..84d49dacd1dd 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-calendar-stream-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-calendar-stream-component.adoc
@@ -66,6 +66,11 @@ include::partial$component-endpoint-headers.adoc[]
== Usage
The consumer polls a calendar and sends each event as a message, with its id
in the `CamelGoogleCalendarEventId` header.
+With `syncFlow=false` and `considerLastUpdate=true`, each poll reads all
result pages. In this mode,
+`maxResults` controls the API page size rather than the total number of events
delivered by a poll.
+This lets the consumer move past a page containing only events already seen at
the last update timestamp.
+With `considerLastUpdate=false`, non-sync polling continues to read only one
page per poll.
+
With `considerLastUpdate`, each poll starts from the last update of the events
it has already seen:
[source,yaml]
diff --git
a/components/camel-google/camel-google-calendar/src/generated/resources/META-INF/org/apache/camel/component/google/calendar/stream/google-calendar-stream.json
b/components/camel-google/camel-google-calendar/src/generated/resources/META-INF/org/apache/camel/component/google/calendar/stream/google-calendar-stream.json
index d46bb7ee6b4e..29958135f687 100644
---
a/components/camel-google/camel-google-calendar/src/generated/resources/META-INF/org/apache/camel/component/google/calendar/stream/google-calendar-stream.json
+++
b/components/camel-google/camel-google-calendar/src/generated/resources/META-INF/org/apache/camel/component/google/calendar/stream/google-calendar-stream.json
@@ -32,7 +32,7 @@
"considerLastUpdate": { "index": 5, "kind": "property", "displayName":
"Consider Last Update", "group": "consumer", "label": "", "required": false,
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Take into account the
lastUpdate of the last event po [...]
"consumeFromNow": { "index": 6, "kind": "property", "displayName":
"Consume From Now", "group": "consumer", "label": "", "required": false,
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": true, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Consume events in the
selected calendar from now on" },
"delegate": { "index": 7, "kind": "property", "displayName": "Delegate",
"group": "consumer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Delegate for
wide-domain service account" },
- "maxResults": { "index": 8, "kind": "property", "displayName": "Max
Results", "group": "consumer", "label": "", "required": false, "type":
"integer", "javaType": "int", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": 10, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Max results to be
returned" },
+ "maxResults": { "index": 8, "kind": "property", "displayName": "Max
Results", "group": "consumer", "label": "", "required": false, "type":
"integer", "javaType": "int", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": 10, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Maximum number of
events returned per Google Calendar API page. With syncFlow [...]
"query": { "index": 9, "kind": "property", "displayName": "Query",
"group": "consumer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "The query to execute on
calendar" },
"scopes": { "index": 10, "kind": "property", "displayName": "Scopes",
"group": "consumer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Specifies the level of
permissions you want a calendar application to have to a user account. [...]
"syncFlow": { "index": 11, "kind": "property", "displayName": "Sync Flow",
"group": "consumer", "label": "", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Sync events, see
https:\/\/developers.google.com\/calendar\/v3\/sync Note [...]
@@ -62,7 +62,7 @@
"considerLastUpdate": { "index": 4, "kind": "parameter", "displayName":
"Consider Last Update", "group": "consumer", "label": "", "required": false,
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Take into account the
lastUpdate of the last event p [...]
"consumeFromNow": { "index": 5, "kind": "parameter", "displayName":
"Consume From Now", "group": "consumer", "label": "", "required": false,
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": true, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Consume events in the
selected calendar from now on" },
"delegate": { "index": 6, "kind": "parameter", "displayName": "Delegate",
"group": "consumer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Delegate for
wide-domain service account" },
- "maxResults": { "index": 7, "kind": "parameter", "displayName": "Max
Results", "group": "consumer", "label": "", "required": false, "type":
"integer", "javaType": "int", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": 10, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Max results to be
returned" },
+ "maxResults": { "index": 7, "kind": "parameter", "displayName": "Max
Results", "group": "consumer", "label": "", "required": false, "type":
"integer", "javaType": "int", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": 10, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Maximum number of
events returned per Google Calendar API page. With syncFlo [...]
"query": { "index": 8, "kind": "parameter", "displayName": "Query",
"group": "consumer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "The query to execute on
calendar" },
"scopes": { "index": 9, "kind": "parameter", "displayName": "Scopes",
"group": "consumer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.google.calendar.stream.GoogleCalendarStreamConfiguration",
"configurationField": "configuration", "description": "Specifies the level of
permissions you want a calendar application to have to a user account. [...]
"sendEmptyMessageWhenIdle": { "index": 10, "kind": "parameter",
"displayName": "Send Empty Message When Idle", "group": "consumer", "label":
"consumer", "required": false, "type": "boolean", "javaType": "boolean",
"deprecated": false, "autowired": false, "secret": false, "defaultValue":
false, "description": "If the polling consumer did not poll any files, you can
enable this option to send an empty message (no body) instead." },
diff --git
a/components/camel-google/camel-google-calendar/src/main/docs/google-calendar-stream-component.adoc
b/components/camel-google/camel-google-calendar/src/main/docs/google-calendar-stream-component.adoc
index 9240d11cf45d..84d49dacd1dd 100644
---
a/components/camel-google/camel-google-calendar/src/main/docs/google-calendar-stream-component.adoc
+++
b/components/camel-google/camel-google-calendar/src/main/docs/google-calendar-stream-component.adoc
@@ -66,6 +66,11 @@ include::partial$component-endpoint-headers.adoc[]
== Usage
The consumer polls a calendar and sends each event as a message, with its id
in the `CamelGoogleCalendarEventId` header.
+With `syncFlow=false` and `considerLastUpdate=true`, each poll reads all
result pages. In this mode,
+`maxResults` controls the API page size rather than the total number of events
delivered by a poll.
+This lets the consumer move past a page containing only events already seen at
the last update timestamp.
+With `considerLastUpdate=false`, non-sync polling continues to read only one
page per poll.
+
With `considerLastUpdate`, each poll starts from the last update of the events
it has already seen:
[source,yaml]
diff --git
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamConfiguration.java
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamConfiguration.java
index f8bb6f57a3b0..bfddf39951d3 100644
---
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamConfiguration.java
+++
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamConfiguration.java
@@ -217,7 +217,8 @@ public class GoogleCalendarStreamConfiguration implements
Cloneable, GoogleCommo
}
/**
- * Max results to be returned
+ * Maximum number of events returned per Google Calendar API page. With
syncFlow=false and considerLastUpdate=true,
+ * each poll reads all pages; otherwise each poll reads one page.
*/
public void setMaxResults(int maxResults) {
this.maxResults = maxResults;
diff --git
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamConsumer.java
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamConsumer.java
index 77c58243bee8..fbe73fcd249e 100644
---
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamConsumer.java
+++
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamConsumer.java
@@ -140,7 +140,19 @@ public class GoogleCalendarStreamConsumer extends
ScheduledBatchPollingConsumer
LOG.info("Sync complete.");
}
} else {
- c = request.setOrderBy("updated").execute();
+ // Keep the same filters while paging. Moving updatedMin between
pages can skip events,
+ // and requesting only the first page can leave it full of already
delivered boundary events.
+ List<Event> events = new ArrayList<>();
+ request.setOrderBy("updated");
+ do {
+ c = request.execute();
+ if (c != null && c.getItems() != null) {
+ events.addAll(c.getItems());
+ }
+ request.setPageToken(c != null ? c.getNextPageToken() : null);
+ } while (getConfiguration().isConsiderLastUpdate() &&
ObjectHelper.isNotEmpty(request.getPageToken()));
+ // Only move the cursor after every page succeeded, so a failed
listing can be retried.
+ c = new Events().setItems(events);
}
if (c != null && c.getItems() != null) {
diff --git
a/components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamConsumerPaginationTest.java
b/components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamConsumerPaginationTest.java
new file mode 100644
index 000000000000..0dd751894991
--- /dev/null
+++
b/components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamConsumerPaginationTest.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.google.calendar.stream;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Queue;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.http.LowLevelHttpResponse;
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpRequest;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.api.services.calendar.Calendar;
+import org.apache.camel.Exchange;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class GoogleCalendarStreamConsumerPaginationTest {
+
+ private DefaultCamelContext context;
+ private final List<GenericUrl> requests = new ArrayList<>();
+ private final List<String> delivered = new ArrayList<>();
+ private final List<String> responses = new ArrayList<>();
+
+ @BeforeEach
+ void setUp() throws Exception {
+ context = new DefaultCamelContext();
+ context.start();
+ }
+
+ @AfterEach
+ void tearDown() {
+ context.stop();
+ }
+
+ @Test
+ void pollsPastAnAlreadyDeliveredBoundaryEvent() throws Exception {
+ responses.add(page("a", "2023-11-14T22:13:20Z", null));
+ responses.add(page("a", "2023-11-14T22:13:20Z", "second"));
+ responses.add(page("b", "2023-11-14T22:13:25Z", null));
+ GoogleCalendarStreamConsumer consumer = consumer(false);
+
+ assertEquals(1, consumer.poll());
+ assertEquals(1, consumer.poll());
+
+ assertEquals(List.of("a", "b"), delivered);
+ assertEquals(3, requests.size());
+ assertEquals("second", requests.get(2).getFirst("pageToken"));
+ assertEquals(requests.get(1).getFirst("updatedMin"),
requests.get(2).getFirst("updatedMin"));
+ assertEquals("1", requests.get(2).getFirst("maxResults"));
+ }
+
+ @Test
+ void pollsAllEventsWithTheSameUpdateTime() throws Exception {
+ responses.add(page("a", "2023-11-14T22:13:20Z", "second"));
+ responses.add(page("b", "2023-11-14T22:13:20Z", null));
+ responses.add(page("a", "2023-11-14T22:13:20Z", "second"));
+ responses.add(page("b", "2023-11-14T22:13:20Z", null));
+ GoogleCalendarStreamConsumer consumer = consumer(false);
+
+ assertEquals(2, consumer.poll());
+ assertEquals(0, consumer.poll());
+ assertEquals(List.of("a", "b"), delivered);
+ }
+
+ @Test
+ void followsAPageTokenEvenWhenThePageHasNoItems() throws Exception {
+ responses.add("{\"nextPageToken\":\"second\"}");
+ responses.add(page("b", "2023-11-14T22:13:20Z", null));
+
+ assertEquals(1, consumer(false).poll());
+ assertEquals(List.of("b"), delivered);
+ }
+
+ @Test
+ void aFailedLaterPageDoesNotAdvanceTheCursor() throws Exception {
+ responses.add(page("a", "2023-11-14T22:13:20Z", "second"));
+ responses.add(null);
+ GoogleCalendarStreamConsumer consumer = consumer(false);
+
+ assertThrows(IOException.class, consumer::poll);
+ assertNull(consumer.getLastUpdate());
+ assertTrue(delivered.isEmpty());
+ }
+
+ @Test
+ void syncFlowKeepsOnePagePerPollAndUsesTheSyncToken() throws Exception {
+ responses.add(page("a", "2023-11-14T22:13:20Z", "second"));
+ responses.add("{\"nextSyncToken\":\"sync\",\"items\":[]}");
+ responses.add("{\"nextSyncToken\":\"sync2\",\"items\":[]}");
+ GoogleCalendarStreamConsumer consumer = consumer(true);
+
+ assertEquals(1, consumer.poll());
+ assertEquals(1, requests.size());
+ assertEquals(0, consumer.poll());
+ assertEquals("second", requests.get(1).getFirst("pageToken"));
+ assertEquals(0, consumer.poll());
+ assertEquals("sync", requests.get(2).getFirst("syncToken"));
+ assertNull(requests.get(2).getFirst("pageToken"));
+ }
+
+ @Test
+ void withoutLastUpdatePollingKeepsOnePagePerPoll() throws Exception {
+ responses.add(page("a", "2023-11-14T22:13:20Z", "second"));
+ responses.add(page("b", "2023-11-14T22:13:25Z", null));
+ GoogleCalendarStreamConsumer consumer = consumer(false, false);
+
+ assertEquals(1, consumer.poll());
+ assertEquals(1, requests.size());
+ assertEquals(List.of("a"), delivered);
+ assertNull(requests.getFirst().getFirst("updatedMin"));
+ }
+
+ private GoogleCalendarStreamConsumer consumer(boolean syncFlow) {
+ return consumer(syncFlow, !syncFlow);
+ }
+
+ private GoogleCalendarStreamConsumer consumer(boolean syncFlow, boolean
considerLastUpdate) {
+ GoogleCalendarStreamEndpoint endpoint = context.getEndpoint(
+
"google-calendar-stream://events?consumeFromNow=false&considerLastUpdate=" +
considerLastUpdate
+ +
"&syncFlow=" + syncFlow
+ +
"&maxResults=1&clientId=id&clientSecret=secret",
+ GoogleCalendarStreamEndpoint.class);
+ Calendar client = new Calendar.Builder(new MockHttpTransport() {
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url)
{
+ return new MockLowLevelHttpRequest(url) {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ requests.add(new GenericUrl(url));
+ String response = responses.removeFirst();
+ if (response == null) {
+ throw new IOException("Cannot read the next page");
+ }
+ return new
MockLowLevelHttpResponse().setContentType("application/json").setContent(response);
+ }
+ };
+ }
+ }, GsonFactory.getDefaultInstance(),
null).setApplicationName("test").build();
+ return new GoogleCalendarStreamConsumer(endpoint, exchange -> {
+ }) {
+ @Override
+ protected Calendar getClient() {
+ return client;
+ }
+
+ @Override
+ public int processBatch(Queue<Object> exchanges) {
+ int count = exchanges.size();
+ for (Object value : exchanges) {
+ Exchange exchange = (Exchange) value;
+
delivered.add(exchange.getIn().getHeader(GoogleCalendarStreamConstants.EVENT_ID,
String.class));
+ }
+ return count;
+ }
+ };
+ }
+
+ private static String page(String id, String updated, String nextToken) {
+ return "{\"items\":[{\"id\":\"" + id + "\",\"updated\":\"" + updated +
"\"}]"
+ + (nextToken == null ? "" : ",\"nextPageToken\":\"" + nextToken
+ "\"") + "}";
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 29602bae9015..7a345a2226b3 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -976,6 +976,13 @@ a JSON object when none is configured). A route that sets
the header must enable
`.unmarshal(new JsonDataFormat(JsonLibrary.Gson).allowUnmarshallType(true))` or
`<json library="Gson" allowUnmarshallType="true"/>`.
+=== camel-google-calendar-stream
+
+With `syncFlow=false` and `considerLastUpdate=true`, the consumer now follows
all result pages in each poll.
+Previously it only read the first page, which could prevent later events from
ever being delivered.
+In this mode, `maxResults` controls the API page size; a poll can now deliver
more events than this value.
+Non-sync polling with `considerLastUpdate=false` continues to read only one
page per poll.
+
=== camel-google-mail, camel-google-drive, camel-google-calendar,
camel-google-sheets - one API client per endpoint
The `google-mail`, `google-mail-stream`, `google-drive`, `google-calendar`,
`google-calendar-stream`, `google-sheets`
diff --git
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/GoogleCalendarStreamComponentBuilderFactory.java
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/GoogleCalendarStreamComponentBuilderFactory.java
index 6868535747e9..f0479d5bbafc 100644
---
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/GoogleCalendarStreamComponentBuilderFactory.java
+++
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/GoogleCalendarStreamComponentBuilderFactory.java
@@ -194,7 +194,9 @@ public interface
GoogleCalendarStreamComponentBuilderFactory {
/**
- * Max results to be returned.
+ * Maximum number of events returned per Google Calendar API page. With
+ * syncFlow=false and considerLastUpdate=true, each poll reads all
+ * pages; otherwise each poll reads one page.
*
* The option is a: <code>int</code> type.
*
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GoogleCalendarStreamEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GoogleCalendarStreamEndpointBuilderFactory.java
index 441c202b7c99..d36e19f30e99 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GoogleCalendarStreamEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GoogleCalendarStreamEndpointBuilderFactory.java
@@ -165,7 +165,9 @@ public interface GoogleCalendarStreamEndpointBuilderFactory
{
return this;
}
/**
- * Max results to be returned.
+ * Maximum number of events returned per Google Calendar API page. With
+ * syncFlow=false and considerLastUpdate=true, each poll reads all
+ * pages; otherwise each poll reads one page.
*
* The option is a: <code>int</code> type.
*
@@ -180,7 +182,9 @@ public interface GoogleCalendarStreamEndpointBuilderFactory
{
return this;
}
/**
- * Max results to be returned.
+ * Maximum number of events returned per Google Calendar API page. With
+ * syncFlow=false and considerLastUpdate=true, each poll reads all
+ * pages; otherwise each poll reads one page.
*
* The option will be converted to a <code>int</code> type.
*