All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.redhat.lightblue.migrator.facade.ConsistencyChecker Maven / Gradle / Ivy

There is a newer version: 2.56.1
Show newest version
package com.redhat.lightblue.migrator.facade;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.reflections.Reflections;
import org.skyscreamer.jsonassert.JSONCompare;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.skyscreamer.jsonassert.JSONCompareResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.redhat.lightblue.migrator.facade.methodcallstringifier.LazyMethodCallStringifier;
import com.redhat.lightblue.migrator.facade.methodcallstringifier.MethodCallStringifier;

import jiff.JsonDiff;

/**
 * This class checks for data inconsistencies and handles logging.
 *
 * @author ykoer
 *
 */
public class ConsistencyChecker {

    // using non-static slf4j loggers for easy unit testing
    Logger inconsistencyLog = LoggerFactory.getLogger(this.getClass());
    Logger hugeInconsistencyLog = LoggerFactory.getLogger(this.getClass().getName()+"Huge");

    private final String implementationName;
    protected int maxInconsistencyLogLength = 65536; // 64KB
    protected boolean logResponseDataEnabled = true;
    private Map,ModelMixIn> modelMixIns;
    private ObjectMapper objectMapper = null;

    private JsonDiff jiff = new JsonDiff();

    public ConsistencyChecker(String implementationName) {
        this.implementationName = implementationName;
        jiff.setOption(JsonDiff.Option.ARRAY_ORDER_INSIGNIFICANT);

        this.objectMapper = createObjectMapper();
    }

    private Map,ModelMixIn> findModelMixInMappings() {
        if  (modelMixIns==null) {
            Reflections reflections = new Reflections("");
            Set> classes = reflections.getTypesAnnotatedWith(ModelMixIn.class);
            modelMixIns = new HashMap<>();
            for (Class clazz : classes) {
                modelMixIns.put(clazz, clazz.getAnnotation(ModelMixIn.class));
            }
        }
        return modelMixIns;
    }

    private ObjectMapper createObjectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        for (Map.Entry, ModelMixIn> entry : findModelMixInMappings().entrySet()) {
            mapper.addMixIn(entry.getValue().clazz(), entry.getKey());
        }
        return mapper;
    }

    /* Log inconsistencies based on following rules:
     *
     * If logResponseDataEnabled=true:
     *     - Log message < MAX_INCONSISTENCY_LOG_LENGTH to server.log.
     *     - Log message > MAX_INCONSISTENCY_LOG_LENGTH and diff <= MAX_INCONSISTENCY_LOG_LENGTH, log diff to server.log.
     *     - Otherwise log method name and parameters to server.log and full message to inconsistency.log.
     * If logResponseDataEnabled=false:
     *     - diff <= MAX_INCONSISTENCY_LOG_LENGTH, log diff to server.log.
     *     - Otherwise log method name and parameters to server.log
     *     - Always log full message to inconsistency.log
     *
     *  Logging inconsistencies at debug level since everything >= info would also appear in server.log
     */
    private void logInconsistency(String parentThreadName, String callToLogInCaseOfInconsistency, String legacyJson, String lightblueJson, String diff) {
        String logMessage = String.format("[%s] Inconsistency found in %s.%s - diff: %s - legacyJson: %s, lightblueJson: %s", parentThreadName, implementationName, callToLogInCaseOfInconsistency, diff, legacyJson, lightblueJson);
        if (logResponseDataEnabled) {
            if (logMessage.length()<=maxInconsistencyLogLength) {
                inconsistencyLog.warn(logMessage);
            } else if (diff!=null&&diff.length()<=maxInconsistencyLogLength) {
                inconsistencyLog.warn(String.format("[%s] Inconsistency found in %s.%s - diff: %s", parentThreadName, implementationName, callToLogInCaseOfInconsistency, diff));
                hugeInconsistencyLog.debug(logMessage);
            } else {
                inconsistencyLog.warn(String.format("[%s] Inconsistency found in %s.%s - payload and diff is greater than %d bytes!", parentThreadName, implementationName, callToLogInCaseOfInconsistency, maxInconsistencyLogLength));
                hugeInconsistencyLog.debug(logMessage);
            }
        } else {
            if (diff!=null&&diff.length()<=maxInconsistencyLogLength) {
                inconsistencyLog.warn(String.format("[%s] Inconsistency found in %s.%s - diff: %s", parentThreadName, implementationName, callToLogInCaseOfInconsistency, diff));
            } else {
                inconsistencyLog.warn(String.format("[%s] Inconsistency found in %s.%s - diff is greater than %d bytes!", parentThreadName, implementationName, callToLogInCaseOfInconsistency, maxInconsistencyLogLength));
            }
            // logData is turned off, log in inconsistency.log for debugging
            hugeInconsistencyLog.debug(logMessage);
        }
    }


    private void logInconsistencyUsingJSONCompare(final String parentThreadName, final String legacyJson, final String lightblueJson, final MethodCallStringifier callToLogInCaseOfInconsistency) {
        try {
            Timer t = new Timer("ConsistencyCheck (JSONCompare)");

            JSONCompareResult result = JSONCompare.compareJSON(legacyJson, lightblueJson, JSONCompareMode.NON_EXTENSIBLE);

            long jiffConsistencyCheckTook = t.complete();

            if (inconsistencyLog.isDebugEnabled()) {
                inconsistencyLog.debug(String.format("[%s] JSONCompare consistency check took: %dms", parentThreadName, jiffConsistencyCheckTook));
                inconsistencyLog.debug(String.format("[%s] JSONCompare consistency check passed: true", parentThreadName));
            }

            if (result.passed()) {
                inconsistencyLog.error(String.format("[%s] Jiff consistency check found an inconsistency but JSONCompare didn't! Happened in %s", parentThreadName, callToLogInCaseOfInconsistency.toString()));
                return;
            }

            // log nice diff
            logInconsistency(parentThreadName, callToLogInCaseOfInconsistency.toString(), legacyJson, lightblueJson, result.getMessage().replaceAll("\n", ","));
        } catch (Exception e) {
            inconsistencyLog.error("JSONCompare consistency check failed for "+callToLogInCaseOfInconsistency, e);
        }
    }

    private void logInconsistencyUsingJSONCompare(final String parentThreadName, final String legacyJson, final String lightblueJson, final MethodCallStringifier callToLogInCaseOfInconsistency, boolean blocking) {
        if (blocking) {
            inconsistencyLog.debug("Running logInconsistencyUsingJSONCompare in a blocking manner");
            logInconsistencyUsingJSONCompare(parentThreadName, legacyJson, lightblueJson, callToLogInCaseOfInconsistency);
        } else {
            inconsistencyLog.debug("Running logInconsistencyUsingJSONCompare in a NON blocking manner");
            new Thread(new Runnable() {

                @Override
                public void run() {
                    logInconsistencyUsingJSONCompare(parentThreadName, legacyJson, lightblueJson, callToLogInCaseOfInconsistency);
                }
            }).start();
        }
    }

    // convenience method for unit testing
    protected boolean checkConsistency(Object o1, Object o2) {
        return checkConsistency(o1, o2, null, null);
    }

    /**
     * Check that objects are equal using org.skyscreamer.jsonassert library.
     *
     * @param legacyEntity                    object returned from legacy call
     * @param lightblueEntity                 object returned from lightblue call
     * @param methodName                      the method name
     * @param callToLogInCaseOfInconsistency  the call including parameters
     * @return
     */
    public boolean checkConsistency(final Object legacyEntity, final Object lightblueEntity, final String methodName, MethodCallStringifier callToLogInCaseOfInconsistency) {

        if (legacyEntity==null&&lightblueEntity==null) {
            return true;
        }

        if (callToLogInCaseOfInconsistency == null) {
            callToLogInCaseOfInconsistency = new LazyMethodCallStringifier();
        }

        Timer p2j = new Timer("ConsistencyChecker's pojo2json conversion");
        final JsonNode legacyJson = objectMapper.valueToTree(legacyEntity);
        final JsonNode lightblueJson = objectMapper.valueToTree(lightblueEntity);
        p2j.complete();

        try {
            Timer t = new Timer("checkConsistency (jiff)");

            boolean consistent = jiff.computeDiff(legacyJson, lightblueJson).isEmpty();

            long jiffConsistencyCheckTook = t.complete();

            if (inconsistencyLog.isDebugEnabled()) {
                inconsistencyLog.debug("Jiff consistency check took: " + jiffConsistencyCheckTook+" ms");
                inconsistencyLog.debug("Jiff consistency check passed: true");
            }

            if (consistent) {
                return true;
            }

            // objects are inconsistent, use org.skyscreamer.jsonassert.JSONCompare to produce inconsistency warning
            // it's slow, but produces nice diffs
            String legacyJsonStr =  objectMapper.writeValueAsString(legacyEntity);
            String lightblueJsonStr =  objectMapper.writeValueAsString(lightblueEntity);

            // JSONCompare fails when comparing booleans, convert them to strings
            if ("true".equals(legacyJsonStr) || "false".equals(legacyJsonStr))
                legacyJsonStr = "\""+legacyJsonStr+"\"";
            if ("true".equals(lightblueJsonStr) || "false".equals(lightblueJsonStr))
                lightblueJsonStr = "\""+lightblueJsonStr+"\"";

            if ("null".equals(legacyJsonStr) || "null".equals(lightblueJsonStr)) {
                logInconsistency(Thread.currentThread().getName(), callToLogInCaseOfInconsistency.toString(), legacyJsonStr, lightblueJsonStr, "One object is null and the other isn't");
            } else {
                logInconsistencyUsingJSONCompare(Thread.currentThread().getName(), legacyJsonStr, lightblueJsonStr, callToLogInCaseOfInconsistency, Boolean.valueOf(System.getProperty("lightblue.facade.consistencyChecker.blocking", "false")));
            }

            // inconsistent
            return false;

        } catch (IOException e) {
            inconsistencyLog.error("Consistency check failed in "+implementationName+"."+callToLogInCaseOfInconsistency+"! Invalid JSON: legacyJson="+legacyJson+", lightblueJson="+lightblueJson, e);
        }
        return false;
    }

    public void setMaxInconsistencyLogLength(int length) {
        this.maxInconsistencyLogLength = length;
    }

    public void setLogResponseDataEnabled(boolean logResponseDataEnabled) {
        this.logResponseDataEnabled = logResponseDataEnabled;
    }

    void setInconsistencyLog(Logger inconsistencyLog) {
        this.inconsistencyLog = inconsistencyLog;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy