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 0f95633e7e7f CAMEL-25576: camel-box - getEnterpriseEvents reads every 
chunk of the event log (#27760)
0f95633e7e7f is described below

commit 0f95633e7e7fe6db4bc42d992688a3baa60e5f02
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Oct 13 12:25:45 2026 +0530

    CAMEL-25576: camel-box - getEnterpriseEvents reads every chunk of the event 
log (#27760)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../component/box/api/BoxEventLogsManager.java     |  29 +++--
 .../box/BoxEventLogsManagerPagingTest.java         | 140 +++++++++++++++++++++
 2 files changed, 162 insertions(+), 7 deletions(-)

diff --git 
a/components/camel-box/camel-box-api/src/main/java/org/apache/camel/component/box/api/BoxEventLogsManager.java
 
b/components/camel-box/camel-box-api/src/main/java/org/apache/camel/component/box/api/BoxEventLogsManager.java
index 094e42585b0f..c9ec9ddcc2c1 100644
--- 
a/components/camel-box/camel-box-api/src/main/java/org/apache/camel/component/box/api/BoxEventLogsManager.java
+++ 
b/components/camel-box/camel-box-api/src/main/java/org/apache/camel/component/box/api/BoxEventLogsManager.java
@@ -19,7 +19,9 @@ package org.apache.camel.component.box.api;
 import java.text.DateFormat;
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 
 import com.box.sdk.BoxAPIConnection;
 import com.box.sdk.BoxAPIException;
@@ -63,7 +65,9 @@ public class BoxEventLogsManager {
      * @param  before   - the upper bound on the timestamp of the events 
returned.
      * @param  types    - an optional list of event types to filter by.
      *
-     * @return          A list of all the events that met the given criteria.
+     * @return          A list of all the events that met the given criteria. 
The event log is read chunk by chunk (at
+     *                  most 500 events per request) until its end, and all 
the events are kept in memory, so use a
+     *                  narrow time window for a large event log.
      */
     public List<BoxEvent> getEnterpriseEvents(String position, Date after, 
Date before, BoxEvent.EventType... types) {
         try {
@@ -80,15 +84,26 @@ public class BoxEventLogsManager {
             }
 
             EnterpriseEventsRequest request = new EnterpriseEventsRequest();
-            
request.position(position).after(after).before(before).types(types);
-            EventLog eventLog = EventLog.getEnterpriseEvents(boxConnection, 
request);
+            request.after(after).before(before).types(types);
 
-            List<BoxEvent> results = new ArrayList<>();
-            for (BoxEvent event : eventLog) {
-                results.add(event);
+            // The service returns the events in chunks (of at most 500 
events) with the position of the next
+            // chunk. Read on from that position until a chunk is empty, which 
is the end of the events within
+            // bounds. An event that the service returns in two chunks is kept 
once.
+            Set<BoxEvent> results = new LinkedHashSet<>();
+            String streamPosition = position;
+            while (true) {
+                EventLog eventLog = 
EventLog.getEnterpriseEvents(boxConnection, request.position(streamPosition));
+                for (BoxEvent event : eventLog) {
+                    results.add(event);
+                }
+                String nextStreamPosition = eventLog.getNextStreamPosition();
+                if (eventLog.getSize() == 0 || nextStreamPosition == null || 
nextStreamPosition.equals(streamPosition)) {
+                    break;
+                }
+                streamPosition = nextStreamPosition;
             }
 
-            return results;
+            return new ArrayList<>(results);
         } catch (BoxAPIException e) {
             throw new RuntimeCamelException(
                     buildBoxApiErrorMessage(e), e);
diff --git 
a/components/camel-box/camel-box-component/src/test/java/org/apache/camel/component/box/BoxEventLogsManagerPagingTest.java
 
b/components/camel-box/camel-box-component/src/test/java/org/apache/camel/component/box/BoxEventLogsManagerPagingTest.java
new file mode 100644
index 000000000000..f4f2595f9898
--- /dev/null
+++ 
b/components/camel-box/camel-box-component/src/test/java/org/apache/camel/component/box/BoxEventLogsManagerPagingTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.box;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxAPIRequest;
+import com.box.sdk.BoxAPIResponse;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.BoxJSONResponse;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.camel.component.box.api.BoxEventLogsManager;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * The Box connection answers the enterprise events request from memory, as 
the service does: at most {@link #MAX_CHUNK}
+ * events from the requested stream position, and the position of the next 
chunk.
+ */
+class BoxEventLogsManagerPagingTest {
+
+    private static final int MAX_CHUNK = 500;
+    private static final int MAX_REQUESTS = 20;
+    private static final Date AFTER = new Date(0);
+    private static final Date BEFORE = new Date(86_400_000L);
+
+    /** The stream positions of the requests, in order ({@code null}: a 
request without position). */
+    private final List<String> positions = new ArrayList<>();
+    private int events;
+    private boolean samePositionAgain;
+    private BoxEventLogsManager manager;
+
+    @BeforeEach
+    void setUp() {
+        BoxAPIConnection connection = new BoxAPIConnection("test-token");
+        connection.setRequestInterceptor(this::respond);
+        manager = new BoxEventLogsManager(connection);
+    }
+
+    private BoxAPIResponse respond(BoxAPIRequest request) {
+        String position = null;
+        int limit = MAX_CHUNK;
+        for (String parameter : request.getUrl().getQuery().split("&")) {
+            if (parameter.startsWith("stream_position=")) {
+                position = parameter.substring("stream_position=".length());
+            } else if (parameter.startsWith("limit=")) {
+                limit = Math.min(MAX_CHUNK, 
Integer.parseInt(parameter.substring("limit=".length())));
+            }
+        }
+        positions.add(position);
+        if (positions.size() > MAX_REQUESTS) {
+            throw new IllegalStateException("The manager sent more than " + 
MAX_REQUESTS + " requests");
+        }
+        int from = position == null ? 0 : Integer.parseInt(position);
+        int to = Math.min(from + limit, events);
+        JsonArray entries = new JsonArray();
+        for (int i = from; i < to; i++) {
+            entries.add(new JsonObject().add("type", "event").add("event_id", 
"event-" + i)
+                    .add("event_type", "LOGIN").add("created_at", 
"2026-01-01T00:00:00-00:00"));
+        }
+        JsonObject body = new JsonObject()
+                .add("chunk_size", entries.size())
+                .add("next_stream_position", samePositionAgain ? 
String.valueOf(from) : String.valueOf(to))
+                .add("entries", entries);
+        return new BoxJSONResponse(200, request.getMethod(), 
request.getUrl().toString(), Map.of(), body);
+    }
+
+    private static List<String> ids(List<BoxEvent> result) {
+        List<String> answer = new ArrayList<>();
+        for (BoxEvent event : result) {
+            answer.add(event.getID());
+        }
+        return answer;
+    }
+
+    private static List<String> expectedIds(int from, int to) {
+        List<String> answer = new ArrayList<>();
+        for (int i = from; i < to; i++) {
+            answer.add("event-" + i);
+        }
+        return answer;
+    }
+
+    @Test
+    void returnsTheEventsOfEveryChunk() {
+        events = 2 * MAX_CHUNK + 200;
+
+        List<BoxEvent> result = manager.getEnterpriseEvents(null, AFTER, 
BEFORE);
+
+        assertEquals(events, result.size(), "all the events within bounds, not 
only the first chunk");
+        assertEquals(expectedIds(0, events), ids(result), "each event once and 
in the order of the service");
+        List<String> expectedPositions = new ArrayList<>();
+        expectedPositions.add(null);
+        expectedPositions.addAll(List.of("500", "1000", "1200"));
+        assertEquals(expectedPositions, positions, "each chunk is requested at 
the next position of the chunk before");
+    }
+
+    @Test
+    void startsAtThePositionOfTheCallerAndReadsToTheEnd() {
+        events = 2 * MAX_CHUNK + 200;
+
+        List<BoxEvent> result = manager.getEnterpriseEvents("100", AFTER, 
BEFORE);
+
+        assertEquals(expectedIds(100, events), ids(result));
+        assertEquals(List.of("100", "600", "1100", "1200"), positions);
+    }
+
+    @Test
+    void stopsWhenTheServiceDoesNotMoveThePosition() {
+        // a service that answers with the position it was asked for must not 
be asked for ever
+        events = 2;
+        samePositionAgain = true;
+
+        List<BoxEvent> result = manager.getEnterpriseEvents(null, AFTER, 
BEFORE);
+
+        assertEquals(expectedIds(0, 2), ids(result), "the two events once, 
although both responses contain them");
+        assertEquals(2, positions.size());
+    }
+}

Reply via email to