You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@nifi.apache.org by "pvillard31 (via GitHub)" <gi...@apache.org> on 2025/02/05 15:08:30 UTC

[PR] NIFI-14238 - Add CaptureBoxEvents processor [nifi]

pvillard31 opened a new pull request, #9695:
URL: https://github.com/apache/nifi/pull/9695

   # Summary
   
   [NIFI-14238](https://issues.apache.org/jira/browse/NIFI-14238) - Add CaptureBoxEvents processor
   
   This pull request adds a new processor that will consume all Box events and output the content of the events as an array of JSON records. In addition to this processor, the pull request also adds another controller service to connect to the Box API using the developer token option. This option is used only for testing as this token expires after 60 minutes but it allows testing the Box components in NiFi without the need for a Business/Enterprise account.
   
   # Tracking
   
   Please complete the following tracking steps prior to pull request creation.
   
   ### Issue Tracking
   
   - [ ] [Apache NiFi Jira](https://issues.apache.org/jira/browse/NIFI) issue created
   
   ### Pull Request Tracking
   
   - [ ] Pull Request title starts with Apache NiFi Jira issue number, such as `NIFI-00000`
   - [ ] Pull Request commit message starts with Apache NiFi Jira issue number, as such `NIFI-00000`
   
   ### Pull Request Formatting
   
   - [ ] Pull Request based on current revision of the `main` branch
   - [ ] Pull Request refers to a feature branch with one commit containing changes
   
   # Verification
   
   Please indicate the verification steps performed prior to pull request creation.
   
   ### Build
   
   - [ ] Build completed using `mvn clean install -P contrib-check`
     - [ ] JDK 21
   
   ### Licensing
   
   - [ ] New dependencies are compatible with the [Apache License 2.0](https://apache.org/licenses/LICENSE-2.0) according to the [License Policy](https://www.apache.org/legal/resolved.html)
   - [ ] New dependencies are documented in applicable `LICENSE` and `NOTICE` files
   
   ### Documentation
   
   - [ ] Documentation formatting appears as expected in rendered files
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-14238 - Add ConsumeBoxEvents processor [nifi]

Posted by "exceptionfactory (via GitHub)" <gi...@apache.org>.
exceptionfactory merged PR #9695:
URL: https://github.com/apache/nifi/pull/9695


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-14238 - Add CaptureBoxEvents processor [nifi]

Posted by "pvillard31 (via GitHub)" <gi...@apache.org>.
pvillard31 commented on code in PR #9695:
URL: https://github.com/apache/nifi/pull/9695#discussion_r1945556207


##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Max Size of Message Queue")
+            .description("The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor. " +
+                    "Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total " +
+                    "memory used by the processor during these surges.")
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+        eventStream = new EventStream(boxAPIConnection);
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {
+            events = new LinkedBlockingQueue<>(eventsCapacity);
+        }
+
+        eventStream.addListener(new EventListener() {
+            @Override
+            public void onEvent(BoxEvent event) {
+                if (!events.offer(event)) {
+                    getLogger().warn("Failed to add event to queue. Queue is full.");
+                }
+            }
+
+            @Override
+            public void onNextPosition(long position) {
+                getLogger().debug("Next position: {}", position);
+            }
+
+            @Override
+            public boolean onException(Throwable e) {
+                getLogger().warn("An error has been received from the stream.", e);

Review Comment:
   Yep, understood. I looked a bit more into the Box code to see how this position is tracked and I see how it can be useful in certain scenarios.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-14238 - Add CaptureBoxEvents processor [nifi]

Posted by "pvillard31 (via GitHub)" <gi...@apache.org>.
pvillard31 commented on code in PR #9695:
URL: https://github.com/apache/nifi/pull/9695#discussion_r1945558971


##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Max Size of Message Queue")
+            .description("The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor. " +
+                    "Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total " +
+                    "memory used by the processor during these surges.")
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+        eventStream = new EventStream(boxAPIConnection);
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {

Review Comment:
   done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-14238 - Add CaptureBoxEvents processor [nifi]

Posted by "exceptionfactory (via GitHub)" <gi...@apache.org>.
exceptionfactory commented on code in PR #9695:
URL: https://github.com/apache/nifi/pull/9695#discussion_r1943205210


##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Max Size of Message Queue")
+            .description("The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor. " +
+                    "Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total " +
+                    "memory used by the processor during these surges.")
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+        eventStream = new EventStream(boxAPIConnection);
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {
+            events = new LinkedBlockingQueue<>(eventsCapacity);
+        }
+
+        eventStream.addListener(new EventListener() {
+            @Override
+            public void onEvent(BoxEvent event) {
+                if (!events.offer(event)) {
+                    getLogger().warn("Failed to add event to queue. Queue is full.");
+                }
+            }
+
+            @Override
+            public void onNextPosition(long position) {
+                getLogger().debug("Next position: {}", position);
+            }
+
+            @Override
+            public boolean onException(Throwable e) {
+                getLogger().warn("An error has been received from the stream.", e);
+                return true;
+            }
+        });
+
+        eventStream.start();
+    }
+
+    @OnStopped
+    public void stopped() {
+        if (eventStream != null && eventStream.isStarted()) {
+            eventStream.stop();
+        }
+    }
+
+    @Override
+    public List<ConfigVerificationResult> verify(ProcessContext context, ComponentLog verificationLogger, Map<String, String> attributes) {
+
+        final List<ConfigVerificationResult> results = new ArrayList<>();
+        BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+
+        try {
+            boxAPIConnection.refresh();
+            results.add(new ConfigVerificationResult.Builder()
+                    .verificationStepName("Box API Connection")
+                    .outcome(Outcome.SUCCESSFUL)
+                    .explanation("Successfully validated Box connection")
+                    .build());
+        } catch (Exception e) {
+            getLogger().warn("Failed to verify configuration.", e);
+            results.add(new ConfigVerificationResult.Builder()
+                    .verificationStepName("Box API Connection")
+                    .outcome(Outcome.FAILED)
+                    .explanation(String.format("Failed to validate Box connection: %s", e.getMessage()))
+                    .build());
+        }
+
+        return results;
+    }
+
+    @Override
+    public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException {
+        if (events.isEmpty()) {
+            context.yield();
+            return;
+        }
+
+        final FlowFile flowFile = session.create();
+        final List<BoxEvent> eventList = new ArrayList<>();

Review Comment:
   Recommend naming this `boxEvents`.
   ```suggestion
           final List<BoxEvent> boxEvents = new ArrayList<>();
   ```



##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Max Size of Message Queue")
+            .description("The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor. " +
+                    "Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total " +
+                    "memory used by the processor during these surges.")
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+        eventStream = new EventStream(boxAPIConnection);
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {
+            events = new LinkedBlockingQueue<>(eventsCapacity);
+        }
+
+        eventStream.addListener(new EventListener() {
+            @Override
+            public void onEvent(BoxEvent event) {
+                if (!events.offer(event)) {
+                    getLogger().warn("Failed to add event to queue. Queue is full.");
+                }
+            }
+
+            @Override
+            public void onNextPosition(long position) {
+                getLogger().debug("Next position: {}", position);
+            }
+
+            @Override
+            public boolean onException(Throwable e) {
+                getLogger().warn("An error has been received from the stream.", e);
+                return true;
+            }
+        });
+
+        eventStream.start();
+    }
+
+    @OnStopped
+    public void stopped() {
+        if (eventStream != null && eventStream.isStarted()) {
+            eventStream.stop();
+        }
+    }
+
+    @Override
+    public List<ConfigVerificationResult> verify(ProcessContext context, ComponentLog verificationLogger, Map<String, String> attributes) {
+
+        final List<ConfigVerificationResult> results = new ArrayList<>();
+        BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+
+        try {
+            boxAPIConnection.refresh();
+            results.add(new ConfigVerificationResult.Builder()
+                    .verificationStepName("Box API Connection")
+                    .outcome(Outcome.SUCCESSFUL)
+                    .explanation("Successfully validated Box connection")
+                    .build());
+        } catch (Exception e) {
+            getLogger().warn("Failed to verify configuration.", e);
+            results.add(new ConfigVerificationResult.Builder()
+                    .verificationStepName("Box API Connection")
+                    .outcome(Outcome.FAILED)
+                    .explanation(String.format("Failed to validate Box connection: %s", e.getMessage()))
+                    .build());
+        }
+
+        return results;
+    }
+
+    @Override
+    public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException {
+        if (events.isEmpty()) {
+            context.yield();
+            return;
+        }
+
+        final FlowFile flowFile = session.create();
+        final List<BoxEvent> eventList = new ArrayList<>();
+        final int nbEvents = events.drainTo(eventList);
+
+        try {
+            // converting events to JSON using the library coming with Box SDK
+            final JsonArray jsonEvents = Json.array();
+            for (BoxEvent event : eventList) {
+                jsonEvents.add(toRecord(event));
+            }

Review Comment:
   This approach ends up creating a new object for every `BoxEvent`, which has some memory impacts. Instead of this approach, it would be much more efficient to use a streaming JSON writer to add each event to the output stream.



##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Max Size of Message Queue")
+            .description("The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor. " +
+                    "Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total " +
+                    "memory used by the processor during these surges.")
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+        eventStream = new EventStream(boxAPIConnection);
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {
+            events = new LinkedBlockingQueue<>(eventsCapacity);
+        }
+
+        eventStream.addListener(new EventListener() {
+            @Override
+            public void onEvent(BoxEvent event) {
+                if (!events.offer(event)) {
+                    getLogger().warn("Failed to add event to queue. Queue is full.");
+                }
+            }
+
+            @Override
+            public void onNextPosition(long position) {
+                getLogger().debug("Next position: {}", position);
+            }
+
+            @Override
+            public boolean onException(Throwable e) {
+                getLogger().warn("An error has been received from the stream.", e);
+                return true;
+            }
+        });
+
+        eventStream.start();
+    }
+
+    @OnStopped
+    public void stopped() {
+        if (eventStream != null && eventStream.isStarted()) {
+            eventStream.stop();
+        }
+    }
+
+    @Override
+    public List<ConfigVerificationResult> verify(ProcessContext context, ComponentLog verificationLogger, Map<String, String> attributes) {
+
+        final List<ConfigVerificationResult> results = new ArrayList<>();
+        BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+
+        try {
+            boxAPIConnection.refresh();
+            results.add(new ConfigVerificationResult.Builder()
+                    .verificationStepName("Box API Connection")
+                    .outcome(Outcome.SUCCESSFUL)
+                    .explanation("Successfully validated Box connection")
+                    .build());
+        } catch (Exception e) {
+            getLogger().warn("Failed to verify configuration.", e);

Review Comment:
   Recommend avoiding log messages ending with a period `.` character.



##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Max Size of Message Queue")
+            .description("The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor. " +
+                    "Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total " +
+                    "memory used by the processor during these surges.")
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+        eventStream = new EventStream(boxAPIConnection);
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {
+            events = new LinkedBlockingQueue<>(eventsCapacity);
+        }
+
+        eventStream.addListener(new EventListener() {
+            @Override
+            public void onEvent(BoxEvent event) {
+                if (!events.offer(event)) {
+                    getLogger().warn("Failed to add event to queue. Queue is full.");
+                }
+            }
+
+            @Override
+            public void onNextPosition(long position) {
+                getLogger().debug("Next position: {}", position);
+            }
+
+            @Override
+            public boolean onException(Throwable e) {
+                getLogger().warn("An error has been received from the stream.", e);
+                return true;
+            }
+        });
+
+        eventStream.start();
+    }
+
+    @OnStopped
+    public void stopped() {
+        if (eventStream != null && eventStream.isStarted()) {
+            eventStream.stop();
+        }
+    }
+
+    @Override
+    public List<ConfigVerificationResult> verify(ProcessContext context, ComponentLog verificationLogger, Map<String, String> attributes) {
+
+        final List<ConfigVerificationResult> results = new ArrayList<>();
+        BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+
+        try {
+            boxAPIConnection.refresh();
+            results.add(new ConfigVerificationResult.Builder()
+                    .verificationStepName("Box API Connection")
+                    .outcome(Outcome.SUCCESSFUL)
+                    .explanation("Successfully validated Box connection")
+                    .build());
+        } catch (Exception e) {
+            getLogger().warn("Failed to verify configuration.", e);
+            results.add(new ConfigVerificationResult.Builder()
+                    .verificationStepName("Box API Connection")
+                    .outcome(Outcome.FAILED)
+                    .explanation(String.format("Failed to validate Box connection: %s", e.getMessage()))
+                    .build());
+        }
+
+        return results;
+    }
+
+    @Override
+    public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException {
+        if (events.isEmpty()) {
+            context.yield();
+            return;
+        }
+
+        final FlowFile flowFile = session.create();
+        final List<BoxEvent> eventList = new ArrayList<>();
+        final int nbEvents = events.drainTo(eventList);
+
+        try {
+            // converting events to JSON using the library coming with Box SDK
+            final JsonArray jsonEvents = Json.array();
+            for (BoxEvent event : eventList) {
+                jsonEvents.add(toRecord(event));
+            }
+
+            try (final OutputStream out = session.write(flowFile)) {
+                final Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
+                jsonEvents.writeTo(writer);
+                writer.flush();
+            }
+        } catch (Exception e) {
+            getLogger().error("Failed to write events to FlowFile due to {}; will re-queue events and try again", e.getMessage(), e);

Review Comment:
   Recommend avoiding the `due to {}` approach since the message is already included in the exception stack trace.



##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Max Size of Message Queue")
+            .description("The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor. " +
+                    "Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total " +
+                    "memory used by the processor during these surges.")
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+        eventStream = new EventStream(boxAPIConnection);
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {

Review Comment:
   Is the purpose of the null check to avoid clearing any queued events?



##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Max Size of Message Queue")
+            .description("The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor. " +
+                    "Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total " +
+                    "memory used by the processor during these surges.")
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+        eventStream = new EventStream(boxAPIConnection);
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {
+            events = new LinkedBlockingQueue<>(eventsCapacity);
+        }
+
+        eventStream.addListener(new EventListener() {
+            @Override
+            public void onEvent(BoxEvent event) {
+                if (!events.offer(event)) {
+                    getLogger().warn("Failed to add event to queue. Queue is full.");
+                }
+            }
+
+            @Override
+            public void onNextPosition(long position) {
+                getLogger().debug("Next position: {}", position);
+            }
+
+            @Override
+            public boolean onException(Throwable e) {
+                getLogger().warn("An error has been received from the stream.", e);
+                return true;
+            }
+        });
+
+        eventStream.start();
+    }
+
+    @OnStopped
+    public void stopped() {
+        if (eventStream != null && eventStream.isStarted()) {
+            eventStream.stop();
+        }
+    }
+
+    @Override
+    public List<ConfigVerificationResult> verify(ProcessContext context, ComponentLog verificationLogger, Map<String, String> attributes) {
+
+        final List<ConfigVerificationResult> results = new ArrayList<>();
+        BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+
+        try {
+            boxAPIConnection.refresh();
+            results.add(new ConfigVerificationResult.Builder()
+                    .verificationStepName("Box API Connection")
+                    .outcome(Outcome.SUCCESSFUL)
+                    .explanation("Successfully validated Box connection")
+                    .build());
+        } catch (Exception e) {
+            getLogger().warn("Failed to verify configuration.", e);
+            results.add(new ConfigVerificationResult.Builder()
+                    .verificationStepName("Box API Connection")
+                    .outcome(Outcome.FAILED)
+                    .explanation(String.format("Failed to validate Box connection: %s", e.getMessage()))
+                    .build());
+        }
+
+        return results;
+    }
+
+    @Override
+    public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException {
+        if (events.isEmpty()) {
+            context.yield();
+            return;
+        }
+
+        final FlowFile flowFile = session.create();
+        final List<BoxEvent> eventList = new ArrayList<>();
+        final int nbEvents = events.drainTo(eventList);

Review Comment:
   Recommend `recordCount` to align with the attribute naming.
   ```suggestion
           final int recordCount = events.drainTo(eventList);
   ```



##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Max Size of Message Queue")
+            .description("The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor. " +
+                    "Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total " +
+                    "memory used by the processor during these surges.")

Review Comment:
   Multiline strings can be used.



##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Max Size of Message Queue")
+            .description("The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor. " +
+                    "Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total " +
+                    "memory used by the processor during these surges.")
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+        eventStream = new EventStream(boxAPIConnection);
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {
+            events = new LinkedBlockingQueue<>(eventsCapacity);
+        }
+
+        eventStream.addListener(new EventListener() {
+            @Override
+            public void onEvent(BoxEvent event) {
+                if (!events.offer(event)) {
+                    getLogger().warn("Failed to add event to queue. Queue is full.");

Review Comment:
   Does the EventListener contract support throwing any kind of exception? It seems like this could end up generating a lot of messages in some cases. Also recommend including the event ID or some marker instead of the static message.



##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Max Size of Message Queue")
+            .description("The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor. " +
+                    "Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total " +
+                    "memory used by the processor during these surges.")
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+        eventStream = new EventStream(boxAPIConnection);
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {
+            events = new LinkedBlockingQueue<>(eventsCapacity);
+        }
+
+        eventStream.addListener(new EventListener() {
+            @Override
+            public void onEvent(BoxEvent event) {
+                if (!events.offer(event)) {
+                    getLogger().warn("Failed to add event to queue. Queue is full.");
+                }
+            }
+
+            @Override
+            public void onNextPosition(long position) {
+                getLogger().debug("Next position: {}", position);
+            }
+
+            @Override
+            public boolean onException(Throwable e) {
+                getLogger().warn("An error has been received from the stream.", e);

Review Comment:
   Recommend tracking the `position` so that it could be logged.
   ```suggestion
                   getLogger().warn("Event stream processing failed at position {}", position, e);
   ```



##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")

Review Comment:
   Multiline strings can be used instead of concatenation.



##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Max Size of Message Queue")

Review Comment:
   Recommend `Queue Capacity` for a shorter name.
   ```suggestion
               .name("Queue Capacity")
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-14238 - Add CaptureBoxEvents processor [nifi]

Posted by "pvillard31 (via GitHub)" <gi...@apache.org>.
pvillard31 commented on code in PR #9695:
URL: https://github.com/apache/nifi/pull/9695#discussion_r1943253489


##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Max Size of Message Queue")
+            .description("The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor. " +
+                    "Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total " +
+                    "memory used by the processor during these surges.")
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+        eventStream = new EventStream(boxAPIConnection);
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {
+            events = new LinkedBlockingQueue<>(eventsCapacity);
+        }
+
+        eventStream.addListener(new EventListener() {
+            @Override
+            public void onEvent(BoxEvent event) {
+                if (!events.offer(event)) {
+                    getLogger().warn("Failed to add event to queue. Queue is full.");
+                }
+            }
+
+            @Override
+            public void onNextPosition(long position) {
+                getLogger().debug("Next position: {}", position);
+            }
+
+            @Override
+            public boolean onException(Throwable e) {
+                getLogger().warn("An error has been received from the stream.", e);

Review Comment:
   We do not have the position when an exception is thrown. We have it in `onNextPosition` and it's already logged there. Or am I missing something?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-14238 - Add CaptureBoxEvents processor [nifi]

Posted by "pvillard31 (via GitHub)" <gi...@apache.org>.
pvillard31 commented on code in PR #9695:
URL: https://github.com/apache/nifi/pull/9695#discussion_r1943252321


##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Max Size of Message Queue")
+            .description("The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor. " +
+                    "Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total " +
+                    "memory used by the processor during these surges.")
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+        eventStream = new EventStream(boxAPIConnection);
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {

Review Comment:
   Yes, I figured that in case we stop the processor, we may not want to clear the queued events. Should I instead clear the queue and just drop what we already consumed? In any case we would have ignored the events while the processor was stopped...



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-14238 - Add CaptureBoxEvents processor [nifi]

Posted by "pvillard31 (via GitHub)" <gi...@apache.org>.
pvillard31 commented on code in PR #9695:
URL: https://github.com/apache/nifi/pull/9695#discussion_r1943259086


##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Max Size of Message Queue")
+            .description("The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor. " +
+                    "Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total " +
+                    "memory used by the processor during these surges.")
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+        eventStream = new EventStream(boxAPIConnection);
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {
+            events = new LinkedBlockingQueue<>(eventsCapacity);
+        }
+
+        eventStream.addListener(new EventListener() {
+            @Override
+            public void onEvent(BoxEvent event) {
+                if (!events.offer(event)) {
+                    getLogger().warn("Failed to add event to queue. Queue is full.");

Review Comment:
   I don't see anything unfortunately - if we throw an exception here, it looks like we would just drop all of the events that were pulled in the background. Will add the event ID in the log message.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-14238 - Add CaptureBoxEvents processor [nifi]

Posted by "pvillard31 (via GitHub)" <gi...@apache.org>.
pvillard31 commented on code in PR #9695:
URL: https://github.com/apache/nifi/pull/9695#discussion_r1943290416


##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Max Size of Message Queue")
+            .description("The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor. " +
+                    "Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total " +
+                    "memory used by the processor during these surges.")
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+        eventStream = new EventStream(boxAPIConnection);
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {
+            events = new LinkedBlockingQueue<>(eventsCapacity);
+        }
+
+        eventStream.addListener(new EventListener() {
+            @Override
+            public void onEvent(BoxEvent event) {
+                if (!events.offer(event)) {
+                    getLogger().warn("Failed to add event to queue. Queue is full.");
+                }
+            }
+
+            @Override
+            public void onNextPosition(long position) {
+                getLogger().debug("Next position: {}", position);
+            }
+
+            @Override
+            public boolean onException(Throwable e) {
+                getLogger().warn("An error has been received from the stream.", e);
+                return true;
+            }
+        });
+
+        eventStream.start();
+    }
+
+    @OnStopped
+    public void stopped() {
+        if (eventStream != null && eventStream.isStarted()) {
+            eventStream.stop();
+        }
+    }
+
+    @Override
+    public List<ConfigVerificationResult> verify(ProcessContext context, ComponentLog verificationLogger, Map<String, String> attributes) {
+
+        final List<ConfigVerificationResult> results = new ArrayList<>();
+        BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+
+        try {
+            boxAPIConnection.refresh();
+            results.add(new ConfigVerificationResult.Builder()
+                    .verificationStepName("Box API Connection")
+                    .outcome(Outcome.SUCCESSFUL)
+                    .explanation("Successfully validated Box connection")
+                    .build());
+        } catch (Exception e) {
+            getLogger().warn("Failed to verify configuration.", e);
+            results.add(new ConfigVerificationResult.Builder()
+                    .verificationStepName("Box API Connection")
+                    .outcome(Outcome.FAILED)
+                    .explanation(String.format("Failed to validate Box connection: %s", e.getMessage()))
+                    .build());
+        }
+
+        return results;
+    }
+
+    @Override
+    public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException {
+        if (events.isEmpty()) {
+            context.yield();
+            return;
+        }
+
+        final FlowFile flowFile = session.create();
+        final List<BoxEvent> eventList = new ArrayList<>();
+        final int nbEvents = events.drainTo(eventList);
+
+        try {
+            // converting events to JSON using the library coming with Box SDK
+            final JsonArray jsonEvents = Json.array();
+            for (BoxEvent event : eventList) {
+                jsonEvents.add(toRecord(event));
+            }

Review Comment:
   very good point!



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-14238 - Add CaptureBoxEvents processor [nifi]

Posted by "exceptionfactory (via GitHub)" <gi...@apache.org>.
exceptionfactory commented on code in PR #9695:
URL: https://github.com/apache/nifi/pull/9695#discussion_r1949325051


##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,270 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.Stateful;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.state.Scope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("""
+        Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc.
+        The content of the events is sent to the 'success' relationship as a JSON array. Events can be dropped in case of NiFi restart
+        or if the queue capacity is exceeded. The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """)
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+@Stateful(description = """
+        The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """, scopes = { Scope.CLUSTER })
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    private final static String POSITION_KEY = "position";
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()

Review Comment:
   ```suggestion
       public static final PropertyDescriptor QUEUE_CAPACITY = new PropertyDescriptor.Builder()
   ```



##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,270 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.Stateful;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.state.Scope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("""
+        Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc.
+        The content of the events is sent to the 'success' relationship as a JSON array. Events can be dropped in case of NiFi restart
+        or if the queue capacity is exceeded. The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """)
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+@Stateful(description = """
+        The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """, scopes = { Scope.CLUSTER })
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    private final static String POSITION_KEY = "position";
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Queue Capacity")
+            .description("""
+                    The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor.
+                    Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total
+                    memory used by the processor during these surges.
+                    """)
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+    private volatile AtomicLong position = new AtomicLong(0);
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+
+        try {
+            final String position = context.getStateManager().getState(Scope.CLUSTER).get(POSITION_KEY);
+            if (position != null) {
+                // we resume from the last known position
+                eventStream = new EventStream(boxAPIConnection, Long.parseLong(position));
+            } else {
+                eventStream = new EventStream(boxAPIConnection);
+            }
+        } catch (Exception e) {
+            throw new ProcessException("Could not retrieve processor state", e);

Review Comment:
   ```suggestion
               throw new ProcessException("Could not retrieve last event position", e);
   ```



##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,270 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.Stateful;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.state.Scope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("""
+        Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc.
+        The content of the events is sent to the 'success' relationship as a JSON array. Events can be dropped in case of NiFi restart
+        or if the queue capacity is exceeded. The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """)
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+@Stateful(description = """
+        The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """, scopes = { Scope.CLUSTER })
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    private final static String POSITION_KEY = "position";
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Queue Capacity")
+            .description("""
+                    The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor.
+                    Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total
+                    memory used by the processor during these surges.
+                    """)
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+    private volatile AtomicLong position = new AtomicLong(0);
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+
+        try {
+            final String position = context.getStateManager().getState(Scope.CLUSTER).get(POSITION_KEY);
+            if (position != null) {
+                // we resume from the last known position
+                eventStream = new EventStream(boxAPIConnection, Long.parseLong(position));
+            } else {
+                eventStream = new EventStream(boxAPIConnection);
+            }
+        } catch (Exception e) {
+            throw new ProcessException("Could not retrieve processor state", e);
+        }
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();

Review Comment:
   ```suggestion
           final int queueCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
   ```



##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,270 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.Stateful;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.state.Scope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("""
+        Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc.
+        The content of the events is sent to the 'success' relationship as a JSON array. Events can be dropped in case of NiFi restart
+        or if the queue capacity is exceeded. The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """)
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+@Stateful(description = """
+        The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """, scopes = { Scope.CLUSTER })
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    private final static String POSITION_KEY = "position";
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Queue Capacity")
+            .description("""
+                    The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor.
+                    Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total
+                    memory used by the processor during these surges.
+                    """)
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+    private volatile AtomicLong position = new AtomicLong(0);
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+
+        try {
+            final String position = context.getStateManager().getState(Scope.CLUSTER).get(POSITION_KEY);
+            if (position != null) {
+                // we resume from the last known position
+                eventStream = new EventStream(boxAPIConnection, Long.parseLong(position));
+            } else {
+                eventStream = new EventStream(boxAPIConnection);
+            }
+        } catch (Exception e) {
+            throw new ProcessException("Could not retrieve processor state", e);
+        }
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {
+            events = new LinkedBlockingQueue<>(eventsCapacity);
+        } else {
+            // create new one with events from the old queue in case capacity has changed
+            final LinkedBlockingQueue<BoxEvent> newQueue = new LinkedBlockingQueue<>(eventsCapacity);
+            newQueue.addAll(events);
+            events = newQueue;
+        }
+
+        eventStream.addListener(new EventListener() {
+
+            @Override
+            public void onEvent(BoxEvent event) {
+                if (!events.offer(event)) {

Review Comment:
   Reviewing the `EventStream` behavior, it seems like a better option would be to use `put()`, which would block the EventStream Thread until the events queue has capacity. This would avoid errors, and dropping data.



##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,270 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.Stateful;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.state.Scope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("""
+        Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc.
+        The content of the events is sent to the 'success' relationship as a JSON array. Events can be dropped in case of NiFi restart
+        or if the queue capacity is exceeded. The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """)
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+@Stateful(description = """
+        The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """, scopes = { Scope.CLUSTER })
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {

Review Comment:
   Reviewing this once more, it seems like the more appropriate name would be `ConsumeBoxEvents`, following the convention of other event processors.
   ```suggestion
   public class ConsumeBoxEvents extends AbstractProcessor implements VerifiableProcessor {
   ```



##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,270 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.Stateful;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.state.Scope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("""
+        Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc.
+        The content of the events is sent to the 'success' relationship as a JSON array. Events can be dropped in case of NiFi restart
+        or if the queue capacity is exceeded. The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """)
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+@Stateful(description = """
+        The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """, scopes = { Scope.CLUSTER })
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    private final static String POSITION_KEY = "position";
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Queue Capacity")
+            .description("""
+                    The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor.
+                    Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total
+                    memory used by the processor during these surges.
+                    """)
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+    private volatile AtomicLong position = new AtomicLong(0);
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+
+        try {
+            final String position = context.getStateManager().getState(Scope.CLUSTER).get(POSITION_KEY);
+            if (position != null) {
+                // we resume from the last known position
+                eventStream = new EventStream(boxAPIConnection, Long.parseLong(position));
+            } else {
+                eventStream = new EventStream(boxAPIConnection);
+            }
+        } catch (Exception e) {
+            throw new ProcessException("Could not retrieve processor state", e);
+        }
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {
+            events = new LinkedBlockingQueue<>(eventsCapacity);
+        } else {
+            // create new one with events from the old queue in case capacity has changed
+            final LinkedBlockingQueue<BoxEvent> newQueue = new LinkedBlockingQueue<>(eventsCapacity);
+            newQueue.addAll(events);
+            events = newQueue;
+        }
+
+        eventStream.addListener(new EventListener() {
+
+            @Override
+            public void onEvent(BoxEvent event) {
+                if (!events.offer(event)) {
+                    getLogger().warn("Failed to add event (ID = {}) to queue. Queue is full.", event.getID());
+                }
+            }
+
+            @Override
+            public void onNextPosition(long pos) {
+                try {
+                    context.getStateManager().setState(Map.of(POSITION_KEY, String.valueOf(pos)), Scope.CLUSTER);
+                    position.set(pos);
+                } catch (IOException e) {
+                    getLogger().warn("Failed to save position in processor state", e);
+                }
+                getLogger().debug("Next position: {}", position);
+            }
+
+            @Override
+            public boolean onException(Throwable e) {
+                getLogger().warn("An error has been received from the stream. Last tracked position {}", position.get(), e);
+                return true;
+            }
+
+        });
+
+        eventStream.start();
+    }
+
+    @OnStopped
+    public void stopped() {
+        if (eventStream != null && eventStream.isStarted()) {
+            eventStream.stop();
+        }
+    }
+
+    @Override
+    public List<ConfigVerificationResult> verify(ProcessContext context, ComponentLog verificationLogger, Map<String, String> attributes) {
+
+        final List<ConfigVerificationResult> results = new ArrayList<>();
+        BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+
+        try {
+            boxAPIConnection.refresh();
+            results.add(new ConfigVerificationResult.Builder()
+                    .verificationStepName("Box API Connection")
+                    .outcome(Outcome.SUCCESSFUL)
+                    .explanation("Successfully validated Box connection")
+                    .build());
+        } catch (Exception e) {
+            getLogger().warn("Failed to verify configuration", e);
+            results.add(new ConfigVerificationResult.Builder()
+                    .verificationStepName("Box API Connection")
+                    .outcome(Outcome.FAILED)
+                    .explanation(String.format("Failed to validate Box connection: %s", e.getMessage()))
+                    .build());
+        }
+
+        return results;
+    }
+
+    @Override
+    public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException {
+        if (events.isEmpty()) {
+            context.yield();
+            return;
+        }
+
+        final FlowFile flowFile = session.create();
+        final List<BoxEvent> boxEvents = new ArrayList<>();
+        final int recordCount = events.drainTo(boxEvents);
+
+        try (final OutputStream out = session.write(flowFile)) {
+            final Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
+            writer.write("[");
+            final Iterator<BoxEvent> iterator = boxEvents.iterator();
+            while (iterator.hasNext()) {
+                BoxEvent event = iterator.next();
+                JsonObject jsonEvent = toRecord(event);
+                jsonEvent.writeTo(writer);
+                if (iterator.hasNext()) {
+                    writer.write(",");
+                }
+            }
+            writer.write("]");
+            writer.flush();
+        } catch (Exception e) {
+            getLogger().error("Failed to write events to FlowFile; will re-queue events and try again", e);
+            boxEvents.forEach(events::offer);
+            session.remove(flowFile);
+            context.yield();
+            return;
+        }
+
+        session.putAttribute(flowFile, "record.count", String.valueOf(recordCount));
+        session.putAttribute(flowFile, CoreAttributes.MIME_TYPE.key(), "application/json");
+        session.transfer(flowFile, REL_SUCCESS);
+    }
+
+    private JsonObject toRecord(BoxEvent event) {

Review Comment:
   Is there a reason for creating the new `JsonObject` as opposed to using the `getSourceJson()` method on `BoxEvent`?



##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,270 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.Stateful;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.state.Scope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("""
+        Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc.
+        The content of the events is sent to the 'success' relationship as a JSON array. Events can be dropped in case of NiFi restart
+        or if the queue capacity is exceeded. The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """)
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+@Stateful(description = """
+        The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """, scopes = { Scope.CLUSTER })
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    private final static String POSITION_KEY = "position";
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Queue Capacity")
+            .description("""
+                    The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor.
+                    Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total
+                    memory used by the processor during these surges.
+                    """)
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+    private volatile AtomicLong position = new AtomicLong(0);
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+
+        try {
+            final String position = context.getStateManager().getState(Scope.CLUSTER).get(POSITION_KEY);
+            if (position != null) {
+                // we resume from the last known position
+                eventStream = new EventStream(boxAPIConnection, Long.parseLong(position));
+            } else {
+                eventStream = new EventStream(boxAPIConnection);
+            }

Review Comment:
   Recommend reversing the condition for readability:
   ```suggestion
               if (position == null) {
                   eventStream = new EventStream(boxAPIConnection);
               } else {
                   eventStream = new EventStream(boxAPIConnection, Long.parseLong(position));
               }
   ```



##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,270 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.Stateful;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.state.Scope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("""
+        Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc.
+        The content of the events is sent to the 'success' relationship as a JSON array. Events can be dropped in case of NiFi restart
+        or if the queue capacity is exceeded. The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """)
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+@Stateful(description = """
+        The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """, scopes = { Scope.CLUSTER })
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    private final static String POSITION_KEY = "position";
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Queue Capacity")
+            .description("""
+                    The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor.
+                    Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total
+                    memory used by the processor during these surges.
+                    """)
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+    private volatile AtomicLong position = new AtomicLong(0);
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+
+        try {
+            final String position = context.getStateManager().getState(Scope.CLUSTER).get(POSITION_KEY);
+            if (position != null) {
+                // we resume from the last known position
+                eventStream = new EventStream(boxAPIConnection, Long.parseLong(position));
+            } else {
+                eventStream = new EventStream(boxAPIConnection);
+            }
+        } catch (Exception e) {
+            throw new ProcessException("Could not retrieve processor state", e);
+        }
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {
+            events = new LinkedBlockingQueue<>(eventsCapacity);
+        } else {
+            // create new one with events from the old queue in case capacity has changed
+            final LinkedBlockingQueue<BoxEvent> newQueue = new LinkedBlockingQueue<>(eventsCapacity);
+            newQueue.addAll(events);
+            events = newQueue;
+        }
+
+        eventStream.addListener(new EventListener() {
+
+            @Override
+            public void onEvent(BoxEvent event) {
+                if (!events.offer(event)) {
+                    getLogger().warn("Failed to add event (ID = {}) to queue. Queue is full.", event.getID());
+                }
+            }
+
+            @Override
+            public void onNextPosition(long pos) {
+                try {
+                    context.getStateManager().setState(Map.of(POSITION_KEY, String.valueOf(pos)), Scope.CLUSTER);
+                    position.set(pos);
+                } catch (IOException e) {
+                    getLogger().warn("Failed to save position in processor state", e);
+                }
+                getLogger().debug("Next position: {}", position);

Review Comment:
   This seems unnecessary now that the position is tracked in state.



##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,270 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.Stateful;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.state.Scope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("""
+        Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc.
+        The content of the events is sent to the 'success' relationship as a JSON array. Events can be dropped in case of NiFi restart
+        or if the queue capacity is exceeded. The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """)
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+@Stateful(description = """
+        The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """, scopes = { Scope.CLUSTER })
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    private final static String POSITION_KEY = "position";
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Queue Capacity")
+            .description("""
+                    The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor.
+                    Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total
+                    memory used by the processor during these surges.
+                    """)
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+    private volatile AtomicLong position = new AtomicLong(0);
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+
+        try {
+            final String position = context.getStateManager().getState(Scope.CLUSTER).get(POSITION_KEY);
+            if (position != null) {
+                // we resume from the last known position
+                eventStream = new EventStream(boxAPIConnection, Long.parseLong(position));
+            } else {
+                eventStream = new EventStream(boxAPIConnection);
+            }
+        } catch (Exception e) {
+            throw new ProcessException("Could not retrieve processor state", e);
+        }
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {
+            events = new LinkedBlockingQueue<>(eventsCapacity);
+        } else {
+            // create new one with events from the old queue in case capacity has changed
+            final LinkedBlockingQueue<BoxEvent> newQueue = new LinkedBlockingQueue<>(eventsCapacity);
+            newQueue.addAll(events);
+            events = newQueue;
+        }
+
+        eventStream.addListener(new EventListener() {
+
+            @Override
+            public void onEvent(BoxEvent event) {
+                if (!events.offer(event)) {
+                    getLogger().warn("Failed to add event (ID = {}) to queue. Queue is full.", event.getID());
+                }
+            }
+
+            @Override
+            public void onNextPosition(long pos) {
+                try {
+                    context.getStateManager().setState(Map.of(POSITION_KEY, String.valueOf(pos)), Scope.CLUSTER);
+                    position.set(pos);
+                } catch (IOException e) {
+                    getLogger().warn("Failed to save position in processor state", e);

Review Comment:
   ```suggestion
                       getLogger().warn("Failed to save position [{}] in processor state", pos, e);
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-14238 - Add CaptureBoxEvents processor [nifi]

Posted by "pvillard31 (via GitHub)" <gi...@apache.org>.
pvillard31 commented on code in PR #9695:
URL: https://github.com/apache/nifi/pull/9695#discussion_r1952493451


##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,270 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.Stateful;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.state.Scope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("""
+        Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc.
+        The content of the events is sent to the 'success' relationship as a JSON array. Events can be dropped in case of NiFi restart
+        or if the queue capacity is exceeded. The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """)
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+@Stateful(description = """
+        The last known position of the Box stream is stored in the processor state and is used to
+        resume the stream from the last known position when the processor is restarted.
+        """, scopes = { Scope.CLUSTER })
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    private final static String POSITION_KEY = "position";
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Queue Capacity")
+            .description("""
+                    The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor.
+                    Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total
+                    memory used by the processor during these surges.
+                    """)
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+    private volatile AtomicLong position = new AtomicLong(0);
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+
+        try {
+            final String position = context.getStateManager().getState(Scope.CLUSTER).get(POSITION_KEY);
+            if (position != null) {
+                // we resume from the last known position
+                eventStream = new EventStream(boxAPIConnection, Long.parseLong(position));
+            } else {
+                eventStream = new EventStream(boxAPIConnection);
+            }
+        } catch (Exception e) {
+            throw new ProcessException("Could not retrieve processor state", e);
+        }
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {
+            events = new LinkedBlockingQueue<>(eventsCapacity);
+        } else {
+            // create new one with events from the old queue in case capacity has changed
+            final LinkedBlockingQueue<BoxEvent> newQueue = new LinkedBlockingQueue<>(eventsCapacity);
+            newQueue.addAll(events);
+            events = newQueue;
+        }
+
+        eventStream.addListener(new EventListener() {
+
+            @Override
+            public void onEvent(BoxEvent event) {
+                if (!events.offer(event)) {
+                    getLogger().warn("Failed to add event (ID = {}) to queue. Queue is full.", event.getID());
+                }
+            }
+
+            @Override
+            public void onNextPosition(long pos) {
+                try {
+                    context.getStateManager().setState(Map.of(POSITION_KEY, String.valueOf(pos)), Scope.CLUSTER);
+                    position.set(pos);
+                } catch (IOException e) {
+                    getLogger().warn("Failed to save position in processor state", e);
+                }
+                getLogger().debug("Next position: {}", position);
+            }
+
+            @Override
+            public boolean onException(Throwable e) {
+                getLogger().warn("An error has been received from the stream. Last tracked position {}", position.get(), e);
+                return true;
+            }
+
+        });
+
+        eventStream.start();
+    }
+
+    @OnStopped
+    public void stopped() {
+        if (eventStream != null && eventStream.isStarted()) {
+            eventStream.stop();
+        }
+    }
+
+    @Override
+    public List<ConfigVerificationResult> verify(ProcessContext context, ComponentLog verificationLogger, Map<String, String> attributes) {
+
+        final List<ConfigVerificationResult> results = new ArrayList<>();
+        BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+
+        try {
+            boxAPIConnection.refresh();
+            results.add(new ConfigVerificationResult.Builder()
+                    .verificationStepName("Box API Connection")
+                    .outcome(Outcome.SUCCESSFUL)
+                    .explanation("Successfully validated Box connection")
+                    .build());
+        } catch (Exception e) {
+            getLogger().warn("Failed to verify configuration", e);
+            results.add(new ConfigVerificationResult.Builder()
+                    .verificationStepName("Box API Connection")
+                    .outcome(Outcome.FAILED)
+                    .explanation(String.format("Failed to validate Box connection: %s", e.getMessage()))
+                    .build());
+        }
+
+        return results;
+    }
+
+    @Override
+    public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException {
+        if (events.isEmpty()) {
+            context.yield();
+            return;
+        }
+
+        final FlowFile flowFile = session.create();
+        final List<BoxEvent> boxEvents = new ArrayList<>();
+        final int recordCount = events.drainTo(boxEvents);
+
+        try (final OutputStream out = session.write(flowFile)) {
+            final Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
+            writer.write("[");
+            final Iterator<BoxEvent> iterator = boxEvents.iterator();
+            while (iterator.hasNext()) {
+                BoxEvent event = iterator.next();
+                JsonObject jsonEvent = toRecord(event);
+                jsonEvent.writeTo(writer);
+                if (iterator.hasNext()) {
+                    writer.write(",");
+                }
+            }
+            writer.write("]");
+            writer.flush();
+        } catch (Exception e) {
+            getLogger().error("Failed to write events to FlowFile; will re-queue events and try again", e);
+            boxEvents.forEach(events::offer);
+            session.remove(flowFile);
+            context.yield();
+            return;
+        }
+
+        session.putAttribute(flowFile, "record.count", String.valueOf(recordCount));
+        session.putAttribute(flowFile, CoreAttributes.MIME_TYPE.key(), "application/json");
+        session.transfer(flowFile, REL_SUCCESS);
+    }
+
+    private JsonObject toRecord(BoxEvent event) {

Review Comment:
   The `getSourceJson()` method only returns a subset of the data contained in the BoxEvent object. We actually use it in this `toRecord` method:
   ````java
   json.add("source", Objects.requireNonNullElse(event.getSourceJSON(), Json.NULL));
   ````



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-14238 - Add CaptureBoxEvents processor [nifi]

Posted by "exceptionfactory (via GitHub)" <gi...@apache.org>.
exceptionfactory commented on code in PR #9695:
URL: https://github.com/apache/nifi/pull/9695#discussion_r1945545746


##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Max Size of Message Queue")
+            .description("The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor. " +
+                    "Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total " +
+                    "memory used by the processor during these surges.")
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+        eventStream = new EventStream(boxAPIConnection);
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {
+            events = new LinkedBlockingQueue<>(eventsCapacity);
+        }
+
+        eventStream.addListener(new EventListener() {
+            @Override
+            public void onEvent(BoxEvent event) {
+                if (!events.offer(event)) {
+                    getLogger().warn("Failed to add event to queue. Queue is full.");
+                }
+            }
+
+            @Override
+            public void onNextPosition(long position) {
+                getLogger().debug("Next position: {}", position);
+            }
+
+            @Override
+            public boolean onException(Throwable e) {
+                getLogger().warn("An error has been received from the stream.", e);

Review Comment:
   Right, the change would involve tracking the last position from `onNextPosition` in an AtomicLong, so it could be pulled and referenced in this warning. That could be helpful when debugging is not enabled.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-14238 - Add CaptureBoxEvents processor [nifi]

Posted by "exceptionfactory (via GitHub)" <gi...@apache.org>.
exceptionfactory commented on code in PR #9695:
URL: https://github.com/apache/nifi/pull/9695#discussion_r1945544592


##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc. The content "
+        + "of the events is sent to the 'success' relationship as a JSON array.")
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Max Size of Message Queue")
+            .description("The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor. " +
+                    "Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total " +
+                    "memory used by the processor during these surges.")
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+        eventStream = new EventStream(boxAPIConnection);
+
+        final int eventsCapacity = context.getProperty(MAX_MESSAGE_QUEUE_SIZE).asInteger();
+        if (events == null) {

Review Comment:
   Yes, clearing the queue might be clearer in terms of the actual behavior.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-14238 - Add CaptureBoxEvents processor [nifi]

Posted by "exceptionfactory (via GitHub)" <gi...@apache.org>.
exceptionfactory commented on code in PR #9695:
URL: https://github.com/apache/nifi/pull/9695#discussion_r1947942576


##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,237 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("""
+        Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc.
+        The content of the events is sent to the 'success' relationship as a JSON array.
+        """)
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Queue Capacity")
+            .description("""
+                    The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor.
+                    Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total
+                    memory used by the processor during these surges.
+                    """)
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+        eventStream = new EventStream(boxAPIConnection);

Review Comment:
   This constructor starts processing events at the current time according to the [EventStream](https://github.com/box/box-java-sdk/blob/f6c37ce61ed02eb21ce6eb6ec5d23ffcabbb76d2/src/main/java/com/box/sdk/EventStream.java#L46) class constructor.
   
   This seems like a reasonable initial implementation, but are there any concerns around picking up from the last position on start or stop using state information?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-14238 - Add CaptureBoxEvents processor [nifi]

Posted by "pvillard31 (via GitHub)" <gi...@apache.org>.
pvillard31 commented on code in PR #9695:
URL: https://github.com/apache/nifi/pull/9695#discussion_r1947954119


##########
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CaptureBoxEvents.java:
##########
@@ -0,0 +1,237 @@
+/*
+ * 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.nifi.processors.box;
+
+import com.box.sdk.BoxAPIConnection;
+import com.box.sdk.BoxEvent;
+import com.box.sdk.EventListener;
+import com.box.sdk.EventStream;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"box", "storage"})
+@CapabilityDescription("""
+        Captures all events from Box. This processor can be used to capture events such as uploads, modifications, deletions, etc.
+        The content of the events is sent to the 'success' relationship as a JSON array.
+        """)
+@SeeAlso({ FetchBoxFile.class, PutBoxFile.class, ListBoxFile.class })
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class CaptureBoxEvents extends AbstractProcessor implements VerifiableProcessor {
+
+    public static final PropertyDescriptor MAX_MESSAGE_QUEUE_SIZE = new PropertyDescriptor.Builder()
+            .name("Queue Capacity")
+            .description("""
+                    The maximum size of the internal queue used to buffer events being transferred from the underlying stream to the processor.
+                    Setting this value higher allows more messages to be buffered in memory during surges of incoming messages, but increases the total
+                    memory used by the processor during these surges.
+                    """)
+            .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
+            .defaultValue("10000")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            MAX_MESSAGE_QUEUE_SIZE
+    );
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("Events received successfully will be sent out this relationship.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    private volatile BoxAPIConnection boxAPIConnection;
+    private volatile EventStream eventStream;
+    protected volatile LinkedBlockingQueue<BoxEvent> events;
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public final Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+        eventStream = new EventStream(boxAPIConnection);

Review Comment:
   I didn't even notice that it was possible to resume at a specific position, things make much more sense now. I've changed the code to not clear the queue if the processor is stopped and reuse the queue if restarted, this way stopping/starting the processor would not cause missing events. It could still happen in case of a restart though.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org