com.savoirtech.json.processor.JsonComparisonProcessor.java Source code

Java tutorial

Introduction

Here is the source code for com.savoirtech.json.processor.JsonComparisonProcessor.java

Source

/*
 *  Copyright (c) 2016 Savoir Technologies
 *
 *  Licensed 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 com.savoirtech.json.processor;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import com.jayway.jsonpath.Configuration;
import com.savoirtech.json.model.JsonComparatorRuleSpecification;
import com.savoirtech.json.rules.JsonComparatorCompiledRule;
import com.savoirtech.json.JsonComparatorResult;
import com.savoirtech.json.rules.RuleChildComparator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Stateful processor of a single comparison.
 *
 * Created by art on 5/9/16.
 */
public class JsonComparisonProcessor {

    private static final Logger DEFAULT_LOGGER = LoggerFactory.getLogger(JsonComparisonProcessor.class);

    private Logger log = DEFAULT_LOGGER;

    /**
     * Template, or expected, JSON for the comparison.
     */
    private final JsonElement templateJson;

    /**
     * Actual JSON being compared.
     */
    private final JsonElement actualJson;

    /**
     * Processor of rules for this comparison, responsible for compiling the rules and determining
     * which rule applies, if any, for each JSON path.
     */
    private RuleProcessor ruleProcessor;

    /**
     * Child comparator for use by rules when performing their own deep comparisons.
     */
    private final MyChildRuleComparator childRuleComparator = new MyChildRuleComparator();

    //========================================
    // Constructor
    //----------------------------------------

    /**
     * Construct a comparison processor using the given json path configuration and rules in order to
     * compare the template json given to the actual json given.
     *
     * @param jsonPathConfiguration configuration to use with JsonPath.
     * @param templateJson          template of the expected JSON.
     * @param rules                 rules to apply to the actual JSON while comparing to the template
     *                              JSON.
     * @param actualJson            actual JSON to compare.
     */
    public JsonComparisonProcessor(Configuration jsonPathConfiguration, JsonElement templateJson,
            JsonComparatorRuleSpecification[] rules, JsonElement actualJson) {

        this.templateJson = templateJson;
        this.actualJson = actualJson;

        this.ruleProcessor = new RuleProcessor(jsonPathConfiguration, rules, actualJson);
    }

    //========================================
    // Getters and Setters
    //----------------------------------------

    public Logger getLog() {
        return log;
    }

    public void setLog(Logger log) {
        this.log = log;
    }

    public RuleProcessor getRuleProcessor() {
        return ruleProcessor;
    }

    public void setRuleProcessor(RuleProcessor ruleProcessor) {
        this.ruleProcessor = ruleProcessor;
    }

    //========================================
    // Public API
    //----------------------------------------

    /**
     * Execute the comparison of the JSON and return the results.
     *
     * @return result indicating whether the JSON was a match and providing an error description for
     * failures.
     */
    public JsonComparatorResult executeComparison() {
        this.ruleProcessor.init();

        return this.walkAndCompare("$", this.templateJson, this.actualJson);
    }

    //========================================
    // Internal Methods
    //----------------------------------------

    /**
     * Walk the JSON and compare the actual JSON to the template JSON, applying rules as-needed.
     *
     * @param path        current path to the JSON elements given.
     * @param templateEle the template, or expected, JSON at this path.
     * @param actualEle   the actual JSON at this path.
     * @return result indicating whether there is a match, and providing a description when there is a
     * mismatch.
     */
    private JsonComparatorResult walkAndCompare(String path, JsonElement templateEle, JsonElement actualEle) {

        JsonComparatorResult result;

        // Find the rule that applies, if any
        JsonComparatorCompiledRule rule = this.ruleProcessor.findMatchingRule(path);

        if (rule != null) {
            result = rule.compare(path, templateEle, actualEle, this.childRuleComparator);
        } else {
            result = this.shallowCompareJsonElements(path, templateEle, actualEle);
        }

        // Make sure contents of objects and arrays are walked, as needed
        if ((result.isMatch()) && (!result.isDeep())) {
            if (actualEle.isJsonObject()) {
                result = this.walkJsonObjectFields(path, templateEle.getAsJsonObject(),
                        actualEle.getAsJsonObject());
            } else if (actualEle.isJsonArray()) {
                result = this.walkJsonArray(path, templateEle.getAsJsonArray(), actualEle.getAsJsonArray());
            }
        }

        return result;
    }

    /**
     * Performs a minimal, shallow comparison of the two given JSON elements.
     */
    private JsonComparatorResult shallowCompareJsonElements(String path, JsonElement expected, JsonElement actual) {
        JsonComparatorResult result;

        if (expected.isJsonObject()) {
            if (actual.isJsonObject()) {
                return new JsonComparatorResult(false, true, null, null);
            } else {
                return new JsonComparatorResult(false, false,
                        "actual json at path " + path + " is not an object, but an object is expected", path);
            }
        } else if (expected.isJsonArray()) {
            if (actual.isJsonArray()) {
                return new JsonComparatorResult(false, true, null, null);
            } else {
                return new JsonComparatorResult(false, false,
                        "actual json at path " + path + " is not an array, but an array is expected", path);
            }
        } else {
            if (expected.equals(actual)) {
                return new JsonComparatorResult(false, true, null, null);
            } else {
                return new JsonComparatorResult(false, false,
                        "primitive mismatch at path " + path + ": actual=" + actual + "; expected=" + expected,
                        path);
            }
        }
    }

    /**
     * Walk all of the fields within the JSON objects given, comparing each.
     */
    private JsonComparatorResult walkJsonObjectFields(String pathToObject, JsonObject templateObj,
            JsonObject actualObj) {

        boolean match = true;
        String errorMessage = null;
        String errorPath = null;

        //
        // Make sure the set of fields in both objects match.  If not, there's no need to continue to
        //  perform a deep comparison.
        //

        if (this.jsonObjectFieldSetsMatch(templateObj, actualObj)) {
            //
            // Iterate over all of the fields in the objects and compare each.
            //
            Iterator<Map.Entry<String, JsonElement>> entryIterator = actualObj.entrySet().iterator();

            while ((match) && (entryIterator.hasNext())) {
                Map.Entry<String, JsonElement> entry = entryIterator.next();

                String fieldPath = pathToObject + "['" + entry.getKey() + "']";

                JsonElement templateFieldEle = templateObj.get(entry.getKey());

                // Perform a deep comparison of the field values.
                JsonComparatorResult fieldResult = this.walkAndCompare(fieldPath, templateFieldEle,
                        entry.getValue());

                match = fieldResult.isMatch();
                errorMessage = fieldResult.getErrorMessage();
                errorPath = fieldResult.getErrorPath();
            }
        } else {
            match = false;
            errorMessage = "object field sets do not match: path='" + pathToObject + "'";
            errorPath = pathToObject;
        }

        return new JsonComparatorResult(true, match, errorMessage, errorPath);
    }

    /**
     * Walk all of the fields within the JSON arrays given, comparing each.
     *
     * @param pathToArray path to the array elements being compared.
     * @param templateArr template, or expected, array.
     * @param actualArr   actual array.
     * @return result of the comparison indicating whether the JSON matches, and providing a cause
     * description when they do no match.
     */
    private JsonComparatorResult walkJsonArray(String pathToArray, JsonArray templateArr, JsonArray actualArr) {

        boolean match = true;
        String errorMessage = null;
        String errorPath = null;

        //
        // Make sure the arrays are the same size; otherwise, there's no need to check the contents.
        //
        if (templateArr.size() == actualArr.size()) {
            //
            // Loop over the array elements and compare each.
            //
            Iterator<JsonElement> actualArrayIterator = actualArr.iterator();
            Iterator<JsonElement> templateArrayIterator = templateArr.iterator();
            int position = 0;

            while ((match) && (actualArrayIterator.hasNext())) {
                JsonElement templateArrayEle = templateArrayIterator.next();
                JsonElement actualArrayEle = actualArrayIterator.next();

                String valuePath = pathToArray + "[" + position + "]";

                // Perform a deep comparison of the array entries.
                JsonComparatorResult childResult = this.walkAndCompare(valuePath, templateArrayEle, actualArrayEle);

                match = childResult.isMatch();
                errorMessage = childResult.getErrorMessage();
                errorPath = childResult.getErrorPath();

                position++;
            }
        } else {
            match = false;
            errorMessage = "array size mismatch: path='" + pathToArray + "'; actualSize=" + actualArr.size()
                    + "; expectedSize=" + templateArr.size();
            errorPath = pathToArray;
        }

        return new JsonComparatorResult(true, match, errorMessage, errorPath);
    }

    /**
     * Determine whether the set of field names in the two given JSON objects are the same.
     */
    private boolean jsonObjectFieldSetsMatch(JsonObject first, JsonObject second) {
        Set<String> firstFieldNames = first.entrySet().stream().map(Map.Entry::getKey).collect(Collectors.toSet());

        Set<String> secondFieldNames = second.entrySet().stream().map(Map.Entry::getKey)
                .collect(Collectors.toSet());

        return (firstFieldNames.equals(secondFieldNames));
    }

    //========================================
    // Internal Classes
    //----------------------------------------

    /**
     * Comparator for use by rules when they need to perform deep comparisons.  This comparator allows
     * rules to continue to be applied without forcing every rule implementation to handle rules
     * themselves.
     */
    private class MyChildRuleComparator implements RuleChildComparator {

        @Override
        public JsonComparatorResult compare(String path, JsonElement templateEle, JsonElement actualEle) {

            return walkAndCompare(path, templateEle, actualEle);
        }
    }
}