This is an automated email from the ASF dual-hosted git repository.

pvillard pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 783e3e6f34 NIFI-14333 Create GetBoxGroupMembers processor
783e3e6f34 is described below

commit 783e3e6f3496fb292f4a295b6dedc6a2f464c4db
Author: Alaksiej Ščarbaty <[email protected]>
AuthorDate: Thu Mar 6 15:24:47 2025 +0100

    NIFI-14333 Create GetBoxGroupMembers processor
    
    Co-authored-by: Pierre Villard <[email protected]>
    Signed-off-by: Pierre Villard <[email protected]>
    
    This closes #9780.
---
 .../nifi/processors/box/BoxGroupAttributes.java    |  30 ++++
 .../nifi/processors/box/GetBoxGroupMembers.java    | 172 +++++++++++++++++++++
 .../services/org.apache.nifi.processor.Processor   |   3 +-
 .../processors/box/GetBoxGroupMembersTest.java     | 150 ++++++++++++++++++
 4 files changed, 354 insertions(+), 1 deletion(-)

diff --git 
a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxGroupAttributes.java
 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxGroupAttributes.java
new file mode 100644
index 0000000000..ece230b3d2
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/BoxGroupAttributes.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+final class BoxGroupAttributes {
+
+    static final String GROUP_ID = "box.group.id";
+    static final String GROUP_USER_IDS = "box.group.user.ids";
+    static final String GROUP_USER_LOGINS = "box.group.user.logins";
+
+    static final String ERROR_MESSAGE = "error.message";
+    static final String ERROR_CODE = "error.code";
+
+    private BoxGroupAttributes() {
+    }
+}
diff --git 
a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/GetBoxGroupMembers.java
 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/GetBoxGroupMembers.java
new file mode 100644
index 0000000000..ce16f7116a
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/GetBoxGroupMembers.java
@@ -0,0 +1,172 @@
+/*
+ * 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.BoxAPIException;
+import com.box.sdk.BoxGroup;
+import com.box.sdk.BoxGroupMembership;
+import com.box.sdk.BoxUser;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.ReadsAttribute;
+import org.apache.nifi.annotation.behavior.ReadsAttributes;
+import org.apache.nifi.annotation.behavior.SideEffectFree;
+import org.apache.nifi.annotation.behavior.WritesAttribute;
+import org.apache.nifi.annotation.behavior.WritesAttributes;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.box.controllerservices.BoxClientService;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+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.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+
+import static java.util.stream.Collectors.joining;
+import static 
org.apache.nifi.annotation.behavior.InputRequirement.Requirement.INPUT_REQUIRED;
+import static org.apache.nifi.processors.box.BoxGroupAttributes.ERROR_CODE;
+import static org.apache.nifi.processors.box.BoxGroupAttributes.ERROR_MESSAGE;
+import static org.apache.nifi.processors.box.BoxGroupAttributes.GROUP_USER_IDS;
+import static 
org.apache.nifi.processors.box.BoxGroupAttributes.GROUP_USER_LOGINS;
+
+@SideEffectFree
+@InputRequirement(INPUT_REQUIRED)
+@Tags({"box", "storage", "metadata"})
+@CapabilityDescription("Retrieves members for a Box Group and writes their 
details in FlowFile attributes.")
+@ReadsAttributes({
+        @ReadsAttribute(attribute = BoxGroupAttributes.GROUP_ID, description = 
"The ID of the Group to retrieve members for."),
+})
+@WritesAttributes({
+        @WritesAttribute(attribute = GROUP_USER_IDS, description = "A 
comma-separated list of user IDs in the group."),
+        @WritesAttribute(attribute = GROUP_USER_LOGINS, description = "A 
comma-separated list of user Logins (emails) in the group."),
+        @WritesAttribute(attribute = ERROR_CODE, description = "An http error 
code returned by Box."),
+        @WritesAttribute(attribute = ERROR_MESSAGE, description = "An error 
message returned by Box."),
+})
+public class GetBoxGroupMembers extends AbstractProcessor {
+
+    static final PropertyDescriptor GROUP_ID = new PropertyDescriptor.Builder()
+            .name("Group ID")
+            .description("The ID of the Group to retrieve members for")
+            .required(true)
+            .defaultValue("${%s}".formatted(BoxGroupAttributes.GROUP_ID))
+            
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = 
List.of(
+            BoxClientService.BOX_CLIENT_SERVICE,
+            GROUP_ID
+    );
+
+    static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("The FlowFile will be routed here after successfully 
retrieving Group members.")
+            .build();
+
+    static final Relationship REL_FAILURE = new Relationship.Builder()
+            .name("failure")
+            .description("The FlowFile will be routed here when Group 
memberships retrieval was attempted but failed.")
+            .build();
+
+    static final Relationship REL_NOT_FOUND = new Relationship.Builder()
+            .name("not.found")
+            .description("The FlowFile will be routed here when the Group was 
not found.")
+            .build();
+
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(
+            REL_SUCCESS,
+            REL_FAILURE,
+            REL_NOT_FOUND
+    );
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    private volatile BoxAPIConnection boxAPIConnection;
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        final BoxClientService boxClientService = 
context.getProperty(BoxClientService.BOX_CLIENT_SERVICE).asControllerService(BoxClientService.class);
+        boxAPIConnection = boxClientService.getBoxApiConnection();
+    }
+
+    @Override
+    public void onTrigger(final ProcessContext context, final ProcessSession 
session) throws ProcessException {
+        final FlowFile flowFile = session.get();
+        if (flowFile == null) {
+            return;
+        }
+
+        final String groupId = 
context.getProperty(GROUP_ID).evaluateAttributeExpressions(flowFile).getValue();
+        try {
+            final Collection<BoxGroupMembership.Info> memberships = 
retrieveGroupMemberships(groupId);
+
+            final String userIDs = extractUserProperty(memberships, 
BoxUser.Info::getID);
+            final String userLogins = extractUserProperty(memberships, 
BoxUser.Info::getLogin);
+
+            session.putAttribute(flowFile, GROUP_USER_IDS, userIDs);
+            session.putAttribute(flowFile, GROUP_USER_LOGINS, userLogins);
+            session.transfer(flowFile, REL_SUCCESS);
+
+        } catch (final BoxAPIException e) {
+            session.putAttribute(flowFile, ERROR_MESSAGE, e.getMessage());
+            session.putAttribute(flowFile, ERROR_CODE, 
String.valueOf(e.getResponseCode()));
+
+            if (e.getResponseCode() == 404) {
+                getLogger().warn("Box Group with ID {} was not found.", 
groupId);
+                session.transfer(flowFile, REL_NOT_FOUND);
+            } else {
+                getLogger().error("Failed to retrieve Box Group for ID: {}, 
file [{}]", groupId, flowFile, e);
+                session.transfer(flowFile, REL_FAILURE);
+            }
+        } catch (final ProcessException e) {
+            throw e;
+        } catch (final RuntimeException e) {
+            getLogger().error("Failed to retrieve Box Group for ID: {}, file 
[{}]", groupId, flowFile, e);
+            session.transfer(flowFile, REL_FAILURE);
+        }
+    }
+
+    private String extractUserProperty(final 
Collection<BoxGroupMembership.Info> memberships, final Function<BoxUser.Info, 
String> userPropertyExtractor) {
+        return memberships.stream()
+                .map(BoxGroupMembership.Info::getUser)
+                .map(userPropertyExtractor)
+                .collect(joining(","));
+    }
+
+    // Package-private for testing.
+    Collection<BoxGroupMembership.Info> retrieveGroupMemberships(final String 
groupId) {
+        return new BoxGroup(boxAPIConnection, groupId).getMemberships();
+    }
+}
diff --git 
a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
index b4dc7216ad..4c62ac0aa2 100644
--- 
a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
+++ 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
@@ -15,9 +15,10 @@
 
 org.apache.nifi.processors.box.ConsumeBoxEnterpriseEvents
 org.apache.nifi.processors.box.ConsumeBoxEvents
-org.apache.nifi.processors.box.GetBoxFileCollaborators
 org.apache.nifi.processors.box.FetchBoxFile
 org.apache.nifi.processors.box.FetchBoxFileInfo
 org.apache.nifi.processors.box.FetchBoxFileRepresentation
+org.apache.nifi.processors.box.GetBoxFileCollaborators
+org.apache.nifi.processors.box.GetBoxGroupMembers
 org.apache.nifi.processors.box.ListBoxFile
 org.apache.nifi.processors.box.PutBoxFile
diff --git 
a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/GetBoxGroupMembersTest.java
 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/GetBoxGroupMembersTest.java
new file mode 100644
index 0000000000..fd27e0df05
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/GetBoxGroupMembersTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.BoxAPIResponseException;
+import com.box.sdk.BoxGroupMembership;
+import com.eclipsesource.json.Json;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.util.MockFlowFile;
+import org.apache.nifi.util.TestRunners;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.emptyMap;
+import static java.util.stream.Collectors.joining;
+import static org.apache.nifi.processors.box.BoxGroupAttributes.GROUP_USER_IDS;
+import static 
org.apache.nifi.processors.box.BoxGroupAttributes.GROUP_USER_LOGINS;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class GetBoxGroupMembersTest extends AbstractBoxFileTest {
+
+    private static final String VALID_GROUP_ID = "valid-group";
+    private static final String NOT_FOUND_GROUP_ID = "not-found-group";
+    private static final String ERROR_GROUP_ID = "error-group";
+
+    private final AtomicReference<Collection<BoxGroupMembership.Info>> 
membershipsHolder = new AtomicReference<>();
+
+    @BeforeEach
+    void setUp() throws Exception {
+        final GetBoxGroupMembers processor = new GetBoxGroupMembers() {
+            @Override
+            Collection<BoxGroupMembership.Info> retrieveGroupMemberships(final 
String groupId) {
+                return switch (groupId) {
+                    case VALID_GROUP_ID -> membershipsHolder.get();
+                    case NOT_FOUND_GROUP_ID -> throw new 
BoxAPIResponseException("Group not found", 404, "Not found", emptyMap());
+                    case ERROR_GROUP_ID -> throw new 
BoxAPIResponseException("Client error", 400, "Client error", emptyMap());
+                    default -> throw new IllegalArgumentException("Unexpected 
group ID: " + groupId);
+                };
+            }
+        };
+
+        testRunner = TestRunners.newTestRunner(processor);
+        super.setUp();
+    }
+
+    @AfterEach
+    void tearDown() {
+        membershipsHolder.set(null);
+    }
+
+    @Test
+    void getMembers_forEmptyGroup() {
+        membershipsHolder.set(emptyList());
+
+        testRunner.enqueue(createFlowFile(VALID_GROUP_ID));
+        testRunner.run();
+        
testRunner.assertAllFlowFilesTransferred(GetBoxGroupMembers.REL_SUCCESS, 1);
+
+        final MockFlowFile flowFile = 
testRunner.getFlowFilesForRelationship(GetBoxGroupMembers.REL_SUCCESS).getFirst();
+        assertTrue(flowFile.getAttribute(GROUP_USER_IDS).isEmpty());
+        assertTrue(flowFile.getAttribute(GROUP_USER_LOGINS).isEmpty());
+    }
+
+    @Test
+    void getMembers_forGroupWithSingleMember() {
+        final BoxGroupMembership.Info member = createGroupMember("1", 
"[email protected]");
+        membershipsHolder.set(List.of(member));
+
+        testRunner.enqueue(createFlowFile(VALID_GROUP_ID));
+        testRunner.run();
+        
testRunner.assertAllFlowFilesTransferred(GetBoxGroupMembers.REL_SUCCESS, 1);
+
+        final MockFlowFile flowFile = 
testRunner.getFlowFilesForRelationship(GetBoxGroupMembers.REL_SUCCESS).getFirst();
+        assertEquals(flowFile.getAttribute(GROUP_USER_IDS), 
member.getUser().getID());
+        assertEquals(flowFile.getAttribute(GROUP_USER_LOGINS), 
member.getUser().getLogin());
+    }
+
+    @Test
+    void getMembers_forGroupWithMultipleMembers() {
+        final BoxGroupMembership.Info member1 = createGroupMember("1", 
"[email protected]");
+        final BoxGroupMembership.Info member2 = createGroupMember("2", 
"[email protected]");
+        final BoxGroupMembership.Info member3 = createGroupMember("3", 
"[email protected]");
+        final List<BoxGroupMembership.Info> members = List.of(member1, 
member2, member3);
+        membershipsHolder.set(members);
+
+        testRunner.enqueue(createFlowFile(VALID_GROUP_ID));
+        testRunner.run();
+        
testRunner.assertAllFlowFilesTransferred(GetBoxGroupMembers.REL_SUCCESS, 1);
+
+        final MockFlowFile flowFile = 
testRunner.getFlowFilesForRelationship(GetBoxGroupMembers.REL_SUCCESS).getFirst();
+
+        final String expectedUserIds = members.stream().map(m -> 
m.getUser().getID()).collect(joining(","));
+        final String expectedUserLogins = members.stream().map(m -> 
m.getUser().getLogin()).collect(joining(","));
+
+        assertEquals(flowFile.getAttribute(GROUP_USER_IDS), expectedUserIds);
+        assertEquals(flowFile.getAttribute(GROUP_USER_LOGINS), 
expectedUserLogins);
+    }
+
+    @Test
+    void getMembers_forNotFoundGroup_routesToNotFound() {
+        testRunner.enqueue(createFlowFile(NOT_FOUND_GROUP_ID));
+        testRunner.run();
+        
testRunner.assertAllFlowFilesTransferred(GetBoxGroupMembers.REL_NOT_FOUND, 1);
+    }
+
+    @Test
+    void getMembers_forBoxApiFailure_routesToFailure() {
+        testRunner.enqueue(createFlowFile(ERROR_GROUP_ID));
+        testRunner.run();
+        
testRunner.assertAllFlowFilesTransferred(GetBoxGroupMembers.REL_FAILURE, 1);
+    }
+
+    private BoxGroupMembership.Info createGroupMember(final String userId, 
final String userLogin) {
+        final String infoJson = Json.object()
+                .add("user", Json.object()
+                        .add("id", userId)
+                        .add("login", userLogin))
+                .toString();
+
+        return new BoxGroupMembership(null, "id").new Info(infoJson);
+    }
+
+    private FlowFile createFlowFile(final String groupId) {
+        final MockFlowFile flowFile = new MockFlowFile(0);
+        flowFile.putAttributes(Map.of(BoxGroupAttributes.GROUP_ID, groupId));
+        return flowFile;
+    }
+}

Reply via email to