Thu. May 16th, 2024

In today’s digital era, data comes in various formats, with JSON (JavaScript Object Notation) being one of the most popular for representing structured data. Manipulating and processing JSON data efficiently is crucial for many software applications, from web development to data analysis. In this article, we’ll delve into the workings of the JsonFieldProcessor class, a Java component designed to simplify JSON data processing tasks.

Introduction to JsonFieldProcessor:

The JsonFieldProcessor class serves as a versatile tool for working with JSON data structures in Java applications. Developed as part of a redesign project, this class provides methods for traversing JSON trees, processing specific fields, and extracting valuable insights from JSON datasets.

Key Features and Functionality:

  1. Method Overriding for Field Processing:
    • One of the core features of the JsonFieldProcessor class is the processField() method, which acts as a template for processing individual fields within a JSON structure. Users can extend this class and override the processField() method to implement custom processing logic tailored to their application’s requirements.
  2. Traversal and Stream Generation:
    • The class offers methods for traversing JSON trees and generating streams of key-value pairs representing paths and corresponding nodes within the JSON structure. This functionality enables efficient data extraction and manipulation operations on JSON data.
  3. Internal Field Search and Processing:
    • Internally, the JsonFieldProcessor class employs recursive algorithms to search for specific fields within the JSON tree. Once a target field is found, the class invokes the overridden processField() method, allowing users to perform custom processing on the identified fields.
  4. Node Retrieval and Path Generation:
    • Additionally, the class provides utility methods for retrieving parent and current nodes based on a given list of parent nodes. It also facilitates the generation of standardized paths for nodes within the JSON tree, simplifying data referencing and manipulation tasks.

Practical Applications:

The JsonFieldProcessor class can be applied in various scenarios, including:

  • Data Extraction: Efficiently extract relevant information from large JSON datasets by specifying target fields for processing.
  • Data Transformation: Perform data transformation tasks such as filtering, mapping, or aggregating JSON data based on specific criteria.
  • Data Analysis: Analyze JSON data structures to identify patterns, trends, or anomalies within the dataset.

The JsonFieldProcessor class provides a powerful toolkit for Java developers to streamline JSON data processing tasks in their applications. By leveraging its features for traversal, field processing, and stream generation, developers can build robust and efficient solutions for working with JSON data. Whether it’s extracting insights from complex JSON datasets or performing data transformations, the JsonFieldProcessor class serves as a valuable asset for modern software development projects.

Here is the code:

package com.nextrials.designtool.redesign.changelog.processors;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.nextrials.designtool.redesign.publish.dto.utils.ListUtils;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;


/**
 * Basic processor for working with Json Database manipulation
 */
@Slf4j
public class JsonFieldProcessor {

    /**
     * Method to override for processing indicated fields
     * @param prefix List of previous nodes in the tree
     * @param rootNode Root of the tree
     */
    protected void processField(List<String> prefix, JsonNode rootNode ) {
        throw new UnsupportedOperationException();
    }

    /**
     * Method to determine if String is an integer
     * @param strNum String contain text to test.
     * @return boolean indicating if text is an integer.
     */
    protected boolean isInteger(String strNum) {
        if (strNum == null) {
            return false;
        }
        try {
            Integer.parseInt(strNum);
        } catch (NumberFormatException nfe) {
            return false;
        }
        return true;
    }

    /**
     * Method to generate a standard display for the tree path
     * @param prefix List of parent tree nodes
     * @return String with the standardized path
     */
    protected String generatePath(List<String> prefix) {
        return "/" + String.join("/", ListUtils.safe(prefix));
    }

    /**
     * Must override processField to use this.  Will call process field for all nodes with key
     * found in findFields.
     * @param rootNode Root of tree
     * @param findFields List of field names to search for to call processing on.
     */
    public void process( JsonNode rootNode, List<String> findFields) {
        this.findFields(null,rootNode,findFields,rootNode);
    }

    /**
     * Generate a stream that contains key that is path, and value that is the node for that path
     * @param rootNode Root of tree
     * @return stream of Map.Entry<String, JsonNode>
     */
    public Stream<Map.Entry<String, JsonNode>> stream(JsonNode rootNode) {
        Map<String,JsonNode> map = new HashMap<>();
        this.stream(null,rootNode,map);

        return map.entrySet().stream();
    }

    /**
     * Method to generate the Map of all nodes with paths
     * @param prefix List of parent nodes
     * @param currentNode Node for processing
     * @param map Map containing the list of paths/nodes.
     */
    protected void stream(List<String> prefix, JsonNode currentNode, Map<String,JsonNode> map) {
        map.put(this.generatePath(prefix),currentNode);

        if (currentNode.isArray()) {
            ArrayNode arrayNode = (ArrayNode) currentNode;
            Iterator<JsonNode> node = arrayNode.elements();
            int index = 0;
            while (node.hasNext()) {
                List<String> nextList;
                if (prefix == null) {
                    nextList = new ArrayList<>();
                } else {
                    nextList = new ArrayList<>(prefix);
                }
                nextList.add(String.valueOf(index));
                this.stream(nextList, node.next(),map);
                index += 1;
            }
        } else if (currentNode.isObject()) {
            List<Map.Entry<String, JsonNode>> actualList = getFields(currentNode);

            actualList.forEach(entry -> {
                List<String> nextList;
                if (prefix == null) {
                    nextList = new ArrayList<>();
                } else {
                    nextList = new ArrayList<>(prefix);
                }
                nextList.add(entry.getKey());
                this.stream(nextList, entry.getValue(),map);
            });
        }
    }

    /**
     * Method to convert fields to list
     * @param currentNode Node to extract fields
     * @return List of fields
     */
    @NotNull
    private List<Map.Entry<String, JsonNode>> getFields(JsonNode currentNode) {
        Iterator<Map.Entry<String, JsonNode>> fields = currentNode.fields();
        Iterable<Map.Entry<String, JsonNode>> iterable = () -> fields;

        return StreamSupport
                .stream(iterable.spliterator(), false)
                .collect(Collectors.toList());
    }

    /**
     * Method used internally to find fields and recursively search the tree for fields to process.
     * @param prefix List of parent nodes
     * @param rootNode Root of tree
     * @param findFields List of fields to find and call processField on.
     * @param currentNode Current node
     */
    protected void findFields(List<String> prefix, JsonNode rootNode, List<String> findFields, JsonNode currentNode) {
        if (currentNode.isArray()) {
            ArrayNode arrayNode = (ArrayNode) currentNode;
            Iterator<JsonNode> node = arrayNode.elements();
            int index = 0;
            while (node.hasNext()) {
                List<String> nextList;
                if (prefix == null) {
                    nextList = new ArrayList<>();
                } else {
                    nextList = new ArrayList<>(prefix);
                }
                nextList.add(String.valueOf(index));
                this.findFields(nextList, rootNode, findFields, node.next());
                index += 1;
            }
        } else if (currentNode.isObject()) {
            List<Map.Entry<String, JsonNode>> actualList = getFields(currentNode);

            actualList.forEach(entry -> {
                List<String> nextList;
                if (prefix == null) {
                    nextList = new ArrayList<>();
                } else {
                    nextList = new ArrayList<>(prefix);
                }
                nextList.add(entry.getKey());
                this.findFields(nextList, rootNode, findFields, entry.getValue());
            });
        } else {
            if (findFields.contains(prefix.get(prefix.size() - 1))) {
                if(log.isDebugEnabled()) {
                    log.debug("Processing: {} with value: {}",this.generatePath(prefix),  currentNode);
                }
                this.processField(prefix, rootNode);
            }
        }
    }

    /**
     * Return the parent node with the given parent list
     * @param prefix List of parent nodes
     * @param rootNode Root of Tree
     * @return ObjectNode that is the parent
     */
    protected ObjectNode findParentNode(List<String> prefix, JsonNode rootNode ) {
        JsonNode locatedNode = rootNode;
        if(prefix != null) {
            for(int i = 0; i < prefix.size() -1 ; i++) {
                String value = prefix.get(i);
                if(isInteger(value)) {
                    locatedNode = locatedNode.path(Integer.parseInt(value));
                } else {
                    locatedNode = locatedNode.path(value);
                }
            }
        }
        return (ObjectNode) locatedNode;
    }

    /**
     * Return the current node give the prefix list
     * @param prefix List of parent nodes
     * @param rootNode Root of Tree
     * @return JsonNode indicating the current node
     */
    protected JsonNode findCurrentNode(List<String> prefix, JsonNode rootNode ) {
        JsonNode locatedNode = rootNode;
        if (prefix != null) {
            for (String value : prefix) {
                if (isInteger(value)) {
                    locatedNode = locatedNode.path(Integer.parseInt(value));
                } else {
                    locatedNode = locatedNode.path(value);
                }
            }
        }
        return locatedNode;
    }
}

By Jeffery Miller

I am known for being able to quickly decipher difficult problems to assist development teams in producing a solution. I have been called upon to be the Team Lead for multiple large-scale projects. I have a keen interest in learning new technologies, always ready for a new challenge.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.