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 3693d8af72 NIFI-14445: Creating processor to fetch specific metadata 
instance for box file
3693d8af72 is described below

commit 3693d8af72e669ae2b97ee75630c0550ade64c8c
Author: Noah Cover <[email protected]>
AuthorDate: Mon Apr 7 17:49:35 2025 -0700

    NIFI-14445: Creating processor to fetch specific metadata instance for box 
file
    
    Signed-off-by: Pierre Villard <[email protected]>
    
    This closes #9856.
---
 ...nces.java => FetchBoxFileMetadataInstance.java} | 169 ++++++++-------------
 .../box/ListBoxFileMetadataInstances.java          |  56 +------
 .../processors/box/utils/BoxMetadataUtils.java     |  90 +++++++++++
 .../services/org.apache.nifi.processor.Processor   |   1 +
 ...cesParseJsonTest.java => BoxParseJsonTest.java} |  37 ++---
 .../box/FetchBoxFileMetadataInstanceTest.java      | 147 ++++++++++++++++++
 6 files changed, 322 insertions(+), 178 deletions(-)

diff --git 
a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/ListBoxFileMetadataInstances.java
 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/FetchBoxFileMetadataInstance.java
similarity index 56%
copy from 
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/ListBoxFileMetadataInstances.java
copy to 
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/FetchBoxFileMetadataInstance.java
index 3c05d841a1..aff6e82182 100644
--- 
a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/ListBoxFileMetadataInstances.java
+++ 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/FetchBoxFileMetadataInstance.java
@@ -20,7 +20,6 @@ import com.box.sdk.BoxAPIConnection;
 import com.box.sdk.BoxAPIResponseException;
 import com.box.sdk.BoxFile;
 import com.box.sdk.Metadata;
-import com.eclipsesource.json.JsonValue;
 import org.apache.nifi.annotation.behavior.InputRequirement;
 import org.apache.nifi.annotation.behavior.WritesAttribute;
 import org.apache.nifi.annotation.behavior.WritesAttributes;
@@ -42,11 +41,7 @@ import org.apache.nifi.processor.util.StandardValidators;
 
 import java.io.IOException;
 import java.io.OutputStream;
-import java.math.BigDecimal;
-import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -56,21 +51,21 @@ import static 
org.apache.nifi.processors.box.BoxFileAttributes.ERROR_CODE;
 import static org.apache.nifi.processors.box.BoxFileAttributes.ERROR_CODE_DESC;
 import static org.apache.nifi.processors.box.BoxFileAttributes.ERROR_MESSAGE;
 import static 
org.apache.nifi.processors.box.BoxFileAttributes.ERROR_MESSAGE_DESC;
+import static 
org.apache.nifi.processors.box.utils.BoxMetadataUtils.processBoxMetadataInstance;
 
 @InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED)
-@Tags({"box", "storage", "metadata", "instances", "templates"})
-@CapabilityDescription("Retrieves all metadata instances associated with a Box 
file.")
-@SeeAlso({ListBoxFile.class, FetchBoxFile.class, FetchBoxFileInfo.class})
+@Tags({"box", "storage", "metadata", "instance", "template"})
+@CapabilityDescription("Retrieves specific metadata instance associated with a 
Box file using template key and scope.")
+@SeeAlso({ListBoxFileMetadataInstances.class, ListBoxFile.class, 
FetchBoxFile.class, FetchBoxFileInfo.class})
 @WritesAttributes({
         @WritesAttribute(attribute = "box.id", description = "The ID of the 
file from which metadata was fetched"),
-        @WritesAttribute(attribute = "record.count", description = "The number 
of records in the FlowFile"),
-        @WritesAttribute(attribute = "mime.type", description = "The MIME Type 
specified by the Record Writer"),
-        @WritesAttribute(attribute = "box.metadata.instances.names", 
description = "Comma-separated list of instances names"),
-        @WritesAttribute(attribute = "box.metadata.instances.count", 
description = "Number of metadata instances found"),
+        @WritesAttribute(attribute = "box.metadata.template.key", description 
= "The metadata template key"),
+        @WritesAttribute(attribute = "box.metadata.template.scope", 
description = "The metadata template scope"),
+        @WritesAttribute(attribute = "mime.type", description = "The MIME Type 
of the FlowFile content"),
         @WritesAttribute(attribute = ERROR_CODE, description = 
ERROR_CODE_DESC),
         @WritesAttribute(attribute = ERROR_MESSAGE, description = 
ERROR_MESSAGE_DESC)
 })
-public class ListBoxFileMetadataInstances extends AbstractProcessor {
+public class FetchBoxFileMetadataInstance extends AbstractProcessor {
 
     public static final PropertyDescriptor FILE_ID = new 
PropertyDescriptor.Builder()
             .name("File ID")
@@ -81,30 +76,55 @@ public class ListBoxFileMetadataInstances extends 
AbstractProcessor {
             .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
             .build();
 
+    public static final PropertyDescriptor TEMPLATE_KEY = new 
PropertyDescriptor.Builder()
+            .name("Template Key")
+            .description("The metadata template key to retrieve.")
+            .required(true)
+            
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .build();
+
+    public static final PropertyDescriptor TEMPLATE_SCOPE = new 
PropertyDescriptor.Builder()
+            .name("Template Scope")
+            .description("The metadata template scope (e.g., 'enterprise', 
'global').")
+            .required(true)
+            .defaultValue("enterprise")
+            
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .build();
+
     public static final Relationship REL_SUCCESS = new Relationship.Builder()
             .name("success")
-            .description("A FlowFile containing the metadata instances records 
will be routed to this relationship upon successful processing.")
+            .description("A FlowFile containing the metadata instance will be 
routed to this relationship upon successful processing.")
             .build();
 
     public static final Relationship REL_FAILURE = new Relationship.Builder()
             .name("failure")
-            .description("A FlowFile will be routed here if there is an error 
fetching metadata instances from the file.")
+            .description("A FlowFile will be routed here if there is an error 
fetching metadata instance from the file.")
             .build();
 
-    public static final Relationship REL_NOT_FOUND = new Relationship.Builder()
-            .name("not found")
+    public static final Relationship REL_FILE_NOT_FOUND = new 
Relationship.Builder()
+            .name("file not found")
             .description("FlowFiles for which the specified Box file was not 
found will be routed to this relationship.")
             .build();
 
+    public static final Relationship REL_TEMPLATE_NOT_FOUND = new 
Relationship.Builder()
+            .name("template not found")
+            .description("FlowFiles for which the specified metadata template 
was not found will be routed to this relationship.")
+            .build();
+
     public static final Set<Relationship> RELATIONSHIPS = Set.of(
             REL_SUCCESS,
             REL_FAILURE,
-            REL_NOT_FOUND
+            REL_FILE_NOT_FOUND,
+            REL_TEMPLATE_NOT_FOUND
     );
 
     private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = 
List.of(
             BoxClientService.BOX_CLIENT_SERVICE,
-            FILE_ID
+            FILE_ID,
+            TEMPLATE_KEY,
+            TEMPLATE_SCOPE
     );
 
     private volatile BoxAPIConnection boxAPIConnection;
@@ -134,68 +154,35 @@ public class ListBoxFileMetadataInstances extends 
AbstractProcessor {
         }
 
         final String fileId = 
context.getProperty(FILE_ID).evaluateAttributeExpressions(flowFile).getValue();
+        final String templateKey = 
context.getProperty(TEMPLATE_KEY).evaluateAttributeExpressions(flowFile).getValue();
+        final String templateScope = 
context.getProperty(TEMPLATE_SCOPE).evaluateAttributeExpressions(flowFile).getValue();
 
         try {
             final BoxFile boxFile = getBoxFile(fileId);
+            final Metadata metadata = boxFile.getMetadata(templateKey, 
templateScope);
 
-            final List<Map<String, Object>> instanceList = new ArrayList<>();
-            final Iterable<Metadata> metadataList = boxFile.getAllMetadata();
-            final Iterator<Metadata> iterator = metadataList.iterator();
-            final Set<String> templateNames = new LinkedHashSet<>();
-
-            if (!iterator.hasNext()) {
-                flowFile = session.putAttribute(flowFile, "box.id", fileId);
-                flowFile = session.putAttribute(flowFile, 
"box.metadata.instances.count", "0");
-                session.transfer(flowFile, REL_SUCCESS);
-                return;
-            }
-
-            while (iterator.hasNext()) {
-                final Metadata metadata = iterator.next();
-                final Map<String, Object> instanceFields = new HashMap<>();
-
-                templateNames.add(metadata.getTemplateName());
-
-                // Add standard metadata fields
-                instanceFields.put("$id", metadata.getID());
-                instanceFields.put("$type", metadata.getTypeName());
-                instanceFields.put("$parent", "file_" + fileId); // match the 
Box API format
-                instanceFields.put("$template", metadata.getTemplateName());
-                instanceFields.put("$scope", metadata.getScope());
-
-                for (final String fieldName : metadata.getPropertyPaths()) {
-                    final JsonValue jsonValue = metadata.getValue(fieldName);
-                    if (jsonValue != null) {
-                        final String cleanFieldName = 
fieldName.startsWith("/") ? fieldName.substring(1) : fieldName;
-                        final Object fieldValue = parseJsonValue(jsonValue);
-                        instanceFields.put(cleanFieldName, fieldValue);
-                    }
-                }
-                instanceList.add(instanceFields);
-            }
+            final Map<String, Object> instanceFields = new HashMap<>();
+            processBoxMetadataInstance(fileId, metadata, instanceFields);
 
             try {
                 try (final OutputStream out = session.write(flowFile);
                      final BoxMetadataJsonArrayWriter writer = 
BoxMetadataJsonArrayWriter.create(out)) {
-
-                    // Write each metadata template as a separate JSON object 
in the array
-                    for (Map<String, Object> templateFields : instanceList) {
-                        writer.write(templateFields);
-                    }
+                    writer.write(instanceFields);
                 }
 
                 final Map<String, String> recordAttributes = new HashMap<>();
-                recordAttributes.put("record.count", 
String.valueOf(instanceList.size()));
                 recordAttributes.put(CoreAttributes.MIME_TYPE.key(), 
"application/json");
                 recordAttributes.put("box.id", fileId);
-                recordAttributes.put("box.metadata.instances.names", 
String.join(",", templateNames));
-                recordAttributes.put("box.metadata.instances.count", 
String.valueOf(instanceList.size()));
+                recordAttributes.put("box.metadata.template.key", templateKey);
+                recordAttributes.put("box.metadata.template.scope", 
templateScope);
                 flowFile = session.putAllAttributes(flowFile, 
recordAttributes);
 
-                session.getProvenanceReporter().receive(flowFile, 
BoxFileUtils.BOX_URL + fileId + "/metadata");
+                session.getProvenanceReporter().receive(flowFile,
+                        String.format("%s/%s/metadata/%s", 
BoxFileUtils.BOX_URL, fileId, templateKey));
                 session.transfer(flowFile, REL_SUCCESS);
             } catch (final IOException e) {
-                getLogger().error("Failed writing metadata instances from file 
[{}]", fileId, e);
+                getLogger().error("Failed writing metadata instance from file 
[{}] with template key [{}] and scope [{}]",
+                        fileId, templateKey, templateScope, e);
                 flowFile = session.putAttribute(flowFile, ERROR_MESSAGE, 
e.getMessage());
                 session.transfer(flowFile, REL_FAILURE);
             }
@@ -204,14 +191,22 @@ public class ListBoxFileMetadataInstances extends 
AbstractProcessor {
             flowFile = session.putAttribute(flowFile, ERROR_CODE, 
valueOf(e.getResponseCode()));
             flowFile = session.putAttribute(flowFile, ERROR_MESSAGE, 
e.getMessage());
             if (e.getResponseCode() == 404) {
-                getLogger().warn("Box file with ID {} was not found.", fileId);
-                session.transfer(flowFile, REL_NOT_FOUND);
+                final String errorBody = e.getResponse();
+                if (errorBody != null && 
errorBody.toLowerCase().contains("instance_not_found")) {
+                    getLogger().warn("Box metadata template with key {} and 
scope {} was not found.", templateKey, templateScope);
+                    session.transfer(flowFile, REL_TEMPLATE_NOT_FOUND);
+                } else {
+                    getLogger().warn("Box file with ID {} was not found.", 
fileId);
+                    session.transfer(flowFile, REL_FILE_NOT_FOUND);
+                }
             } else {
-                getLogger().error("Couldn't fetch metadata instances from file 
with id [{}]", fileId, e);
+                getLogger().error("Couldn't fetch metadata instance from file 
with id [{}] with template key [{}] and scope [{}]",
+                        fileId, templateKey, templateScope, e);
                 session.transfer(flowFile, REL_FAILURE);
             }
         } catch (final Exception e) {
-            getLogger().error("Failed to process metadata instances for file 
[{}]", fileId, e);
+            getLogger().error("Failed to process metadata instance for file 
[{}] with template key [{}] and scope [{}]",
+                    fileId, templateKey, templateScope, e);
             flowFile = session.putAttribute(flowFile, ERROR_MESSAGE, 
e.getMessage());
             session.transfer(flowFile, REL_FAILURE);
         }
@@ -226,42 +221,4 @@ public class ListBoxFileMetadataInstances extends 
AbstractProcessor {
     BoxFile getBoxFile(final String fileId) {
         return new BoxFile(boxAPIConnection, fileId);
     }
-
-    /**
-     * Parses a JsonValue and returns the appropriate Java object.
-     * Box does not allow exponential notation in metadata values, so we need 
to handle
-     * special number formats. For numbers containing decimal points or 
exponents, we try to
-     * convert them to BigDecimal first for precise representation. If that 
fails, we
-     * fall back to double, which might lose precision but allows the 
processing to continue.
-     *
-     * @param jsonValue The JsonValue to parse.
-     * @return The parsed Java object.
-     */
-    protected static Object parseJsonValue(final JsonValue jsonValue) {
-        if (jsonValue == null) {
-            return null;
-        }
-        if (jsonValue.isString()) {
-            return jsonValue.asString();
-        } else if (jsonValue.isNumber()) {
-            final String numberString = jsonValue.toString();
-            if (numberString.contains(".") || 
numberString.toLowerCase().contains("e")) {
-                try {
-                    return (new BigDecimal(numberString)).toPlainString();
-                } catch (final NumberFormatException e) {
-                    return jsonValue.asDouble();
-                }
-            } else {
-                try {
-                    return jsonValue.asLong();
-                } catch (final NumberFormatException e) {
-                    return (new BigDecimal(numberString)).toPlainString();
-                }
-            }
-        } else if (jsonValue.isBoolean()) {
-            return jsonValue.asBoolean();
-        }
-        // Fallback: return the string representation.
-        return jsonValue.toString();
-    }
 }
diff --git 
a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/ListBoxFileMetadataInstances.java
 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/ListBoxFileMetadataInstances.java
index 3c05d841a1..ea95e2af78 100644
--- 
a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/ListBoxFileMetadataInstances.java
+++ 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/ListBoxFileMetadataInstances.java
@@ -20,7 +20,6 @@ import com.box.sdk.BoxAPIConnection;
 import com.box.sdk.BoxAPIResponseException;
 import com.box.sdk.BoxFile;
 import com.box.sdk.Metadata;
-import com.eclipsesource.json.JsonValue;
 import org.apache.nifi.annotation.behavior.InputRequirement;
 import org.apache.nifi.annotation.behavior.WritesAttribute;
 import org.apache.nifi.annotation.behavior.WritesAttributes;
@@ -42,7 +41,6 @@ import org.apache.nifi.processor.util.StandardValidators;
 
 import java.io.IOException;
 import java.io.OutputStream;
-import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -56,6 +54,7 @@ import static 
org.apache.nifi.processors.box.BoxFileAttributes.ERROR_CODE;
 import static org.apache.nifi.processors.box.BoxFileAttributes.ERROR_CODE_DESC;
 import static org.apache.nifi.processors.box.BoxFileAttributes.ERROR_MESSAGE;
 import static 
org.apache.nifi.processors.box.BoxFileAttributes.ERROR_MESSAGE_DESC;
+import static 
org.apache.nifi.processors.box.utils.BoxMetadataUtils.processBoxMetadataInstance;
 
 @InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED)
 @Tags({"box", "storage", "metadata", "instances", "templates"})
@@ -157,20 +156,7 @@ public class ListBoxFileMetadataInstances extends 
AbstractProcessor {
                 templateNames.add(metadata.getTemplateName());
 
                 // Add standard metadata fields
-                instanceFields.put("$id", metadata.getID());
-                instanceFields.put("$type", metadata.getTypeName());
-                instanceFields.put("$parent", "file_" + fileId); // match the 
Box API format
-                instanceFields.put("$template", metadata.getTemplateName());
-                instanceFields.put("$scope", metadata.getScope());
-
-                for (final String fieldName : metadata.getPropertyPaths()) {
-                    final JsonValue jsonValue = metadata.getValue(fieldName);
-                    if (jsonValue != null) {
-                        final String cleanFieldName = 
fieldName.startsWith("/") ? fieldName.substring(1) : fieldName;
-                        final Object fieldValue = parseJsonValue(jsonValue);
-                        instanceFields.put(cleanFieldName, fieldValue);
-                    }
-                }
+                processBoxMetadataInstance(fileId, metadata, instanceFields);
                 instanceList.add(instanceFields);
             }
 
@@ -226,42 +212,4 @@ public class ListBoxFileMetadataInstances extends 
AbstractProcessor {
     BoxFile getBoxFile(final String fileId) {
         return new BoxFile(boxAPIConnection, fileId);
     }
-
-    /**
-     * Parses a JsonValue and returns the appropriate Java object.
-     * Box does not allow exponential notation in metadata values, so we need 
to handle
-     * special number formats. For numbers containing decimal points or 
exponents, we try to
-     * convert them to BigDecimal first for precise representation. If that 
fails, we
-     * fall back to double, which might lose precision but allows the 
processing to continue.
-     *
-     * @param jsonValue The JsonValue to parse.
-     * @return The parsed Java object.
-     */
-    protected static Object parseJsonValue(final JsonValue jsonValue) {
-        if (jsonValue == null) {
-            return null;
-        }
-        if (jsonValue.isString()) {
-            return jsonValue.asString();
-        } else if (jsonValue.isNumber()) {
-            final String numberString = jsonValue.toString();
-            if (numberString.contains(".") || 
numberString.toLowerCase().contains("e")) {
-                try {
-                    return (new BigDecimal(numberString)).toPlainString();
-                } catch (final NumberFormatException e) {
-                    return jsonValue.asDouble();
-                }
-            } else {
-                try {
-                    return jsonValue.asLong();
-                } catch (final NumberFormatException e) {
-                    return (new BigDecimal(numberString)).toPlainString();
-                }
-            }
-        } else if (jsonValue.isBoolean()) {
-            return jsonValue.asBoolean();
-        }
-        // Fallback: return the string representation.
-        return jsonValue.toString();
-    }
 }
diff --git 
a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/utils/BoxMetadataUtils.java
 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/utils/BoxMetadataUtils.java
new file mode 100644
index 0000000000..596e018d9a
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/utils/BoxMetadataUtils.java
@@ -0,0 +1,90 @@
+/*
+ * 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.utils;
+
+import com.box.sdk.Metadata;
+import com.eclipsesource.json.JsonValue;
+
+import java.math.BigDecimal;
+import java.util.Map;
+
+public class BoxMetadataUtils {
+
+    /**
+     * Parses a JsonValue and returns the appropriate Java object.
+     * Box does not allow exponential notation in metadata values, so we need 
to handle
+     * special number formats. For numbers containing decimal points or 
exponents, we try to
+     * convert them to BigDecimal first for precise representation. If that 
fails, we
+     * fall back to double, which might lose precision but allows the 
processing to continue.
+     *
+     * @param jsonValue The JsonValue to parse.
+     * @return The parsed Java object.
+     */
+    public static Object parseJsonValue(final JsonValue jsonValue) {
+        if (jsonValue == null) {
+            return null;
+        }
+        if (jsonValue.isString()) {
+            return jsonValue.asString();
+        } else if (jsonValue.isNumber()) {
+            final String numberString = jsonValue.toString();
+            if (numberString.contains(".") || 
numberString.toLowerCase().contains("e")) {
+                try {
+                    return (new BigDecimal(numberString)).toPlainString();
+                } catch (final NumberFormatException e) {
+                    return jsonValue.asDouble();
+                }
+            } else {
+                try {
+                    return jsonValue.asLong();
+                } catch (final NumberFormatException e) {
+                    return (new BigDecimal(numberString)).toPlainString();
+                }
+            }
+        } else if (jsonValue.isBoolean()) {
+            return jsonValue.asBoolean();
+        }
+        // Fallback: return the string representation.
+        return jsonValue.toString();
+    }
+
+    /**
+     * Processes Box metadata instance and populates the provided map with the 
default fields.
+     *
+     * @param fileId         The ID of the file.
+     * @param metadata       The Box metadata instance.
+     * @param instanceFields The map to populate with metadata fields.
+     */
+    public static void processBoxMetadataInstance(final String fileId,
+                                                  final Metadata metadata,
+                                                  final Map<String, Object> 
instanceFields) {
+        instanceFields.put("$id", metadata.getID());
+        instanceFields.put("$type", metadata.getTypeName());
+        instanceFields.put("$parent", "file_" + fileId); // match the Box API 
format
+        instanceFields.put("$template", metadata.getTemplateName());
+        instanceFields.put("$scope", metadata.getScope());
+
+        for (final String fieldName : metadata.getPropertyPaths()) {
+            final JsonValue jsonValue = metadata.getValue(fieldName);
+            if (jsonValue != null) {
+                final String cleanFieldName = fieldName.startsWith("/") ? 
fieldName.substring(1) : fieldName;
+                final Object fieldValue = parseJsonValue(jsonValue);
+                instanceFields.put(cleanFieldName, fieldValue);
+            }
+        }
+    }
+}
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 74ed0b7e9c..90461c91e2 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
@@ -20,6 +20,7 @@ org.apache.nifi.processors.box.CreateBoxFileMetadataInstance
 org.apache.nifi.processors.box.ExtractStructuredBoxFileMetadata
 org.apache.nifi.processors.box.FetchBoxFile
 org.apache.nifi.processors.box.FetchBoxFileInfo
+org.apache.nifi.processors.box.FetchBoxFileMetadataInstance
 org.apache.nifi.processors.box.FetchBoxFileRepresentation
 org.apache.nifi.processors.box.GetBoxFileCollaborators
 org.apache.nifi.processors.box.GetBoxGroupMembers
diff --git 
a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileMetadataInstancesParseJsonTest.java
 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/BoxParseJsonTest.java
similarity index 72%
rename from 
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileMetadataInstancesParseJsonTest.java
rename to 
nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/BoxParseJsonTest.java
index 521d664984..c6279f8c95 100644
--- 
a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/ListBoxFileMetadataInstancesParseJsonTest.java
+++ 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/BoxParseJsonTest.java
@@ -20,21 +20,22 @@ import com.eclipsesource.json.Json;
 import com.eclipsesource.json.JsonArray;
 import com.eclipsesource.json.JsonObject;
 import com.eclipsesource.json.JsonValue;
+import org.apache.nifi.processors.box.utils.BoxMetadataUtils;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
-public class ListBoxFileMetadataInstancesParseJsonTest {
+public class BoxParseJsonTest {
 
     @Test
     void testParseString() {
         String expected = "test string";
-        Object result = 
ListBoxFileMetadataInstances.parseJsonValue(Json.value(expected));
+        Object result = BoxMetadataUtils.parseJsonValue(Json.value(expected));
         assertEquals(expected, result);
 
         // Empty string
         expected = "";
-        result = 
ListBoxFileMetadataInstances.parseJsonValue(Json.value(expected));
+        result = BoxMetadataUtils.parseJsonValue(Json.value(expected));
         assertEquals(expected, result);
     }
 
@@ -42,12 +43,12 @@ public class ListBoxFileMetadataInstancesParseJsonTest {
     void testParseBoolean() {
         // Test true
         boolean expected = true;
-        Object result = 
ListBoxFileMetadataInstances.parseJsonValue(Json.value(expected));
+        Object result = BoxMetadataUtils.parseJsonValue(Json.value(expected));
         assertEquals(expected, result);
 
         // Test false
         expected = false;
-        result = 
ListBoxFileMetadataInstances.parseJsonValue(Json.value(expected));
+        result = BoxMetadataUtils.parseJsonValue(Json.value(expected));
         assertEquals(expected, result);
     }
 
@@ -55,17 +56,17 @@ public class ListBoxFileMetadataInstancesParseJsonTest {
     void testParseIntegerNumber() {
         // Integer value
         long expected = 42;
-        Object result = 
ListBoxFileMetadataInstances.parseJsonValue(Json.value(expected));
+        Object result = BoxMetadataUtils.parseJsonValue(Json.value(expected));
         assertEquals(expected, result);
 
         // Max long value
         expected = Long.MAX_VALUE;
-        result = 
ListBoxFileMetadataInstances.parseJsonValue(Json.value(expected));
+        result = BoxMetadataUtils.parseJsonValue(Json.value(expected));
         assertEquals(expected, result);
 
         // Min long value
         expected = Long.MIN_VALUE;
-        result = 
ListBoxFileMetadataInstances.parseJsonValue(Json.value(expected));
+        result = BoxMetadataUtils.parseJsonValue(Json.value(expected));
         assertEquals(expected, result);
     }
 
@@ -74,19 +75,19 @@ public class ListBoxFileMetadataInstancesParseJsonTest {
         // Double without exponent
         String input = "3.14159";
         JsonValue jsonValue = Json.parse(input);
-        Object result = ListBoxFileMetadataInstances.parseJsonValue(jsonValue);
+        Object result = BoxMetadataUtils.parseJsonValue(jsonValue);
         assertEquals(input, result);
 
         // Very small number that should be preserved
         input = "0.0000000001";
         jsonValue = Json.parse(input);
-        result = ListBoxFileMetadataInstances.parseJsonValue(jsonValue);
+        result = BoxMetadataUtils.parseJsonValue(jsonValue);
         assertEquals(input, result);
 
         // Very large number that should be preserved
         input = "9999999999999999.9999";
         jsonValue = Json.parse(input);
-        result = ListBoxFileMetadataInstances.parseJsonValue(jsonValue);
+        result = BoxMetadataUtils.parseJsonValue(jsonValue);
         assertEquals(input, result);
     }
 
@@ -95,19 +96,19 @@ public class ListBoxFileMetadataInstancesParseJsonTest {
         // Scientific notation is converted to plain string format
         String input = "1.234e5";
         JsonValue jsonValue = Json.parse(input);
-        Object result = ListBoxFileMetadataInstances.parseJsonValue(jsonValue);
+        Object result = BoxMetadataUtils.parseJsonValue(jsonValue);
         assertEquals("123400", result);
 
         // large exponent
         input = "1.234e20";
         jsonValue = Json.parse(input);
-        result = ListBoxFileMetadataInstances.parseJsonValue(jsonValue);
+        result = BoxMetadataUtils.parseJsonValue(jsonValue);
         assertEquals("123400000000000000000", result);
 
         // Negative exponent
         input = "1.234e-5";
         jsonValue = Json.parse(input);
-        result = ListBoxFileMetadataInstances.parseJsonValue(jsonValue);
+        result = BoxMetadataUtils.parseJsonValue(jsonValue);
         assertEquals("0.00001234", result);
     }
 
@@ -115,12 +116,12 @@ public class ListBoxFileMetadataInstancesParseJsonTest {
     void testParseObjectAndArray() {
         // JSON objects return their string representation
         JsonObject jsonObject = Json.object().add("key", "value");
-        Object result = 
ListBoxFileMetadataInstances.parseJsonValue(jsonObject);
+        Object result = BoxMetadataUtils.parseJsonValue(jsonObject);
         assertEquals(jsonObject.toString(), result);
 
         // JSON arrays return their string representation
         JsonArray jsonArray = Json.array().add("item1").add("item2");
-        result = ListBoxFileMetadataInstances.parseJsonValue(jsonArray);
+        result = BoxMetadataUtils.parseJsonValue(jsonArray);
         assertEquals(jsonArray.toString(), result);
     }
 
@@ -128,11 +129,11 @@ public class ListBoxFileMetadataInstancesParseJsonTest {
     void testParseNumberFormatException() {
         String largeIntegerString = "9999999999999999999"; // Beyond 
Long.MAX_VALUE
         JsonValue jsonValue = Json.parse(largeIntegerString);
-        Object result = ListBoxFileMetadataInstances.parseJsonValue(jsonValue);
+        Object result = BoxMetadataUtils.parseJsonValue(jsonValue);
         assertEquals(largeIntegerString, result);
 
         double doubleValue = 123.456;
-        result = 
ListBoxFileMetadataInstances.parseJsonValue(Json.value(doubleValue));
+        result = BoxMetadataUtils.parseJsonValue(Json.value(doubleValue));
         assertEquals(String.valueOf(doubleValue), result);
     }
 }
diff --git 
a/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/FetchBoxFileMetadataInstanceTest.java
 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/FetchBoxFileMetadataInstanceTest.java
new file mode 100644
index 0000000000..ec12f25385
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/FetchBoxFileMetadataInstanceTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.BoxAPIException;
+import com.box.sdk.BoxAPIResponseException;
+import com.box.sdk.BoxFile;
+import com.box.sdk.Metadata;
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonObject;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.util.MockFlowFile;
+import org.apache.nifi.util.TestRunners;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class FetchBoxFileMetadataInstanceTest extends AbstractBoxFileTest {
+
+    private static final String TEMPLATE_KEY = "fileMetadata";
+    private static final String TEMPLATE_SCOPE = "enterprise_123";
+    private static final String TEMPLATE_ID = "12345";
+
+    @Mock
+    private BoxFile mockBoxFile;
+
+    @Override
+    @BeforeEach
+    void setUp() throws Exception {
+        final FetchBoxFileMetadataInstance testSubject = new 
FetchBoxFileMetadataInstance() {
+            @Override
+            BoxFile getBoxFile(String fileId) {
+                return mockBoxFile;
+            }
+        };
+
+        testRunner = TestRunners.newTestRunner(testSubject);
+        super.setUp();
+    }
+
+    @Test
+    void testSuccessfulMetadataRetrieval() {
+        final JsonObject metadataJson = Json.object()
+                .add("$id", TEMPLATE_ID)
+                .add("$type", "fileMetadata-123")
+                .add("$parent", "file_" + TEST_FILE_ID)
+                .add("$template", TEMPLATE_KEY)
+                .add("$scope", TEMPLATE_SCOPE)
+                .add("fileName", "document.pdf")
+                .add("fileExtension", "pdf");
+        final Metadata metadata = new Metadata(metadataJson);
+
+        when(mockBoxFile.getMetadata(TEMPLATE_KEY, 
TEMPLATE_SCOPE)).thenReturn(metadata);
+
+        testRunner.setProperty(FetchBoxFileMetadataInstance.FILE_ID, 
TEST_FILE_ID);
+        testRunner.setProperty(FetchBoxFileMetadataInstance.TEMPLATE_KEY, 
TEMPLATE_KEY);
+        testRunner.setProperty(FetchBoxFileMetadataInstance.TEMPLATE_SCOPE, 
TEMPLATE_SCOPE);
+        testRunner.enqueue(new byte[0]);
+        testRunner.run();
+        
testRunner.assertAllFlowFilesTransferred(FetchBoxFileMetadataInstance.REL_SUCCESS,
 1);
+
+        final MockFlowFile flowFile = 
testRunner.getFlowFilesForRelationship(FetchBoxFileMetadataInstance.REL_SUCCESS).getFirst();
+        flowFile.assertAttributeEquals("box.id", TEST_FILE_ID);
+        flowFile.assertAttributeEquals(CoreAttributes.MIME_TYPE.key(), 
"application/json");
+        flowFile.assertAttributeEquals("box.metadata.template.key", 
TEMPLATE_KEY);
+        flowFile.assertAttributeEquals("box.metadata.template.scope", 
TEMPLATE_SCOPE);
+
+        final String content = new String(flowFile.toByteArray());
+        assertTrue(content.contains("\"$id\":\"" + TEMPLATE_ID + "\""));
+        assertTrue(content.contains("\"$template\":\"" + TEMPLATE_KEY + "\""));
+        assertTrue(content.contains("\"$scope\":\"" + TEMPLATE_SCOPE + "\""));
+        assertTrue(content.contains("\"$parent\":\"file_" + TEST_FILE_ID + 
"\""));
+        assertTrue(content.contains("\"fileName\":\"document.pdf\""));
+        assertTrue(content.contains("\"fileExtension\":\"pdf\""));
+    }
+
+    @Test
+    void testMetadataNotFound() {
+        when(mockBoxFile.getMetadata(anyString(), anyString())).thenThrow(
+                new BoxAPIResponseException("instance_not_found - Template not 
found", 404, "instance_not_found", null));
+
+        testRunner.setProperty(FetchBoxFileMetadataInstance.FILE_ID, 
TEST_FILE_ID);
+        testRunner.setProperty(FetchBoxFileMetadataInstance.TEMPLATE_KEY, 
TEMPLATE_KEY);
+        testRunner.setProperty(FetchBoxFileMetadataInstance.TEMPLATE_SCOPE, 
TEMPLATE_SCOPE);
+        testRunner.enqueue(new byte[0]);
+        testRunner.run();
+
+        
testRunner.assertAllFlowFilesTransferred(FetchBoxFileMetadataInstance.REL_TEMPLATE_NOT_FOUND,
 1);
+        final MockFlowFile flowFile = 
testRunner.getFlowFilesForRelationship(FetchBoxFileMetadataInstance.REL_TEMPLATE_NOT_FOUND).getFirst();
+        flowFile.assertAttributeExists(BoxFileAttributes.ERROR_MESSAGE);
+    }
+
+    @Test
+    void testFileNotFound() {
+        final BoxAPIResponseException mockException = new 
BoxAPIResponseException("API Error", 404, "Box File Not Found", null);
+        doThrow(mockException).when(mockBoxFile).getMetadata(anyString(), 
anyString());
+
+        testRunner.setProperty(FetchBoxFileMetadataInstance.FILE_ID, 
TEST_FILE_ID);
+        testRunner.setProperty(FetchBoxFileMetadataInstance.TEMPLATE_KEY, 
TEMPLATE_KEY);
+        testRunner.setProperty(FetchBoxFileMetadataInstance.TEMPLATE_SCOPE, 
TEMPLATE_SCOPE);
+        testRunner.enqueue(new byte[0]);
+        testRunner.run();
+
+        
testRunner.assertAllFlowFilesTransferred(FetchBoxFileMetadataInstance.REL_FILE_NOT_FOUND,
 1);
+        final MockFlowFile flowFile = 
testRunner.getFlowFilesForRelationship(FetchBoxFileMetadataInstance.REL_FILE_NOT_FOUND).getFirst();
+        flowFile.assertAttributeEquals(BoxFileAttributes.ERROR_CODE, "404");
+        flowFile.assertAttributeEquals(BoxFileAttributes.ERROR_MESSAGE, "API 
Error [404]");
+    }
+
+    @Test
+    void testBoxApiException() {
+        final BoxAPIException mockException = new BoxAPIException("General API 
Error", 500, "Unexpected Error");
+        doThrow(mockException).when(mockBoxFile).getMetadata(anyString(), 
anyString());
+
+        testRunner.setProperty(FetchBoxFileMetadataInstance.FILE_ID, 
TEST_FILE_ID);
+        testRunner.setProperty(FetchBoxFileMetadataInstance.TEMPLATE_KEY, 
TEMPLATE_KEY);
+        testRunner.setProperty(FetchBoxFileMetadataInstance.TEMPLATE_SCOPE, 
TEMPLATE_SCOPE);
+        testRunner.enqueue(new byte[0]);
+        testRunner.run();
+
+        
testRunner.assertAllFlowFilesTransferred(FetchBoxFileMetadataInstance.REL_FAILURE,
 1);
+        final MockFlowFile flowFile = 
testRunner.getFlowFilesForRelationship(FetchBoxFileMetadataInstance.REL_FAILURE).getFirst();
+        flowFile.assertAttributeEquals(BoxFileAttributes.ERROR_MESSAGE, 
"General API Error\nUnexpected Error");
+    }
+}


Reply via email to