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;
+ }
+}