PlaceComparisonHelper.java

package emissary.util;

import emissary.config.Configurator;
import emissary.core.DiffCheckConfiguration;
import emissary.core.IBaseDataObject;
import emissary.core.IBaseDataObjectDiffHelper;
import emissary.core.IBaseDataObjectHelper;
import emissary.place.ServiceProviderPlace;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;

/**
 * Helper class to use during development of a major refactoring or replacement of a Place.
 */
public class PlaceComparisonHelper {

    private PlaceComparisonHelper() {}

    // Config key to use in Place configuration to define another place to compare
    public static final String CFG_PLACE_TO_COMPARE = "PLACE_TO_COMPARE";

    /**
     * Given a config which contains a 'PLACE_TO_COMPARE' directive, instantiate the referenced place with its usual
     * configuration
     * 
     * @param configG from an existing place which is scanned for PLACE_TO_COMPARE, which should reference a place that the
     *        developer wants to compare against
     * @return the instantiated place if configured
     * @throws ReflectiveOperationException if there is a problem.
     */
    @Nullable
    public static ServiceProviderPlace getPlaceToCompare(final Configurator configG) throws ReflectiveOperationException {
        if (configG == null) {
            return null;
        }

        // Find the config line, which could be null if not defined
        final String compareToPlaceCfg = StringUtils.trimToNull(configG.findStringEntry(CFG_PLACE_TO_COMPARE));
        if (StringUtils.isNotBlank(compareToPlaceCfg)) {
            final Class<? extends ServiceProviderPlace> clazz = Class
                    .forName(compareToPlaceCfg).asSubclass(ServiceProviderPlace.class);
            return clazz.getDeclaredConstructor(String.class)
                    .newInstance(compareToPlaceCfg + ".cfg");
        }

        return null;
    }

    /**
     * Used to compare a 'new' place with another, usually during development aimed at replacing the 'old' place.
     * 
     * @param newResults an empty list to add results into from running the newPlace
     * @param ibdoForNewPlace the actual BDO being passed in for comparison
     * @param newPlace the new place we're comparing 'from'
     * @param newMethodName to use when comparing (e.g. processHeavyDuty)
     * @param oldPlace to compare against
     * @param oldMethodName to use when comparing (e.g. processHeavyDuty)
     * @param options {@link DiffCheckConfiguration} to configure diffing options
     * @return a readable string of differences to log or use elsewhere.
     * @throws ReflectiveOperationException if there is a problem.
     */
    @SuppressWarnings("unchecked")
    public static String compareToPlace(final List<IBaseDataObject> newResults, final IBaseDataObject ibdoForNewPlace,
            final ServiceProviderPlace newPlace, final String newMethodName, final ServiceProviderPlace oldPlace, final String oldMethodName,
            final DiffCheckConfiguration options) throws ReflectiveOperationException {

        Validate.notNull(newResults, "Required: newResults not null");
        Validate.notNull(ibdoForNewPlace, "Required: ibdoForNewPlace not null");
        Validate.notNull(newPlace, "Required: newPlace not null");
        Validate.notNull(newMethodName, "newMethodName: newResults not null");
        Validate.notNull(oldPlace, "Required: oldPlace not null");
        Validate.notNull(oldMethodName, "Required: oldMethodName not null");
        Validate.notNull(options, "Required: options not null");

        // Generate an identifier from the simple class names, e.g. Comparison[ColorPlace==ColourPlace]
        final String oldPlaceName = oldPlace.getClass().getSimpleName();
        final String newPlaceName = newPlace.getClass().getSimpleName();
        final String identifier = String.format("Comparison[%s==%s]", oldPlaceName, newPlaceName);

        // Get the method to run (e.g. processHeavyDuty)
        final Method oldProcess = oldPlace.getClass().getDeclaredMethod(oldMethodName, IBaseDataObject.class);
        final Method newProcess = newPlace.getClass().getDeclaredMethod(newMethodName, IBaseDataObject.class);

        // Clone the data before running old or new methods
        final IBaseDataObject ibdoForOldPlace = IBaseDataObjectHelper.clone(ibdoForNewPlace, true);

        // Actually run the places to get results
        final List<IBaseDataObject> oldResults = (List<IBaseDataObject>) oldProcess.invoke(oldPlace, ibdoForOldPlace);
        newResults.addAll((List<IBaseDataObject>) newProcess.invoke(newPlace, ibdoForNewPlace));

        // Now generate the 'diff' for the results
        return checkDifferences(ibdoForOldPlace, ibdoForNewPlace, oldResults, newResults, identifier, options);
    }

    /**
     * Given two BDOs and results from two processing place runs, compare them and log any differences.
     * 
     * @param ibdoForOldPlace likely a cloned object of the 'new' place object
     * @param ibdoForNewPlace the 'main' BDO that was originally passed in from upstream
     * @param oldResults from the 'old' run with the 'ibdoForOldPlace' object
     * @param newResults from the 'new' run with the 'ibdoForNewPlace' object
     * @param identifier to highlight any differences in logs
     * @param options {@link DiffCheckConfiguration} to configure diffing options
     * @return the string of differences, or null if there aren't any
     */
    @Nullable
    public static String checkDifferences(final IBaseDataObject ibdoForOldPlace, final IBaseDataObject ibdoForNewPlace,
            final List<IBaseDataObject> oldResults, final List<IBaseDataObject> newResults, final String identifier,
            final DiffCheckConfiguration options) {
        Validate.notNull(ibdoForOldPlace, "Required: ibdoForOldPlace not null");
        Validate.notNull(ibdoForNewPlace, "Required: ibdoForNewPlace not null");
        Validate.notNull(oldResults, "Required: oldResults not null");
        Validate.notNull(newResults, "Required: newResults not null");
        Validate.notNull(identifier, "Required: identifier not null");
        Validate.notNull(options, "Required: options not null");

        final List<String> parentDifferences = new ArrayList<>();
        final List<String> childDifferences = new ArrayList<>();

        // Runnables aren't compared for the diff
        final List<Runnable> oldRunnables = DisposeHelper.get(ibdoForOldPlace);
        final List<Runnable> newRunnables = DisposeHelper.get(ibdoForNewPlace);

        try {
            ibdoForOldPlace.deleteParameter(DisposeHelper.KEY);
            ibdoForNewPlace.deleteParameter(DisposeHelper.KEY);
            IBaseDataObjectDiffHelper.diff(ibdoForNewPlace, ibdoForOldPlace, parentDifferences,
                    options);
            IBaseDataObjectDiffHelper.diff(newResults, oldResults, identifier, childDifferences,
                    options);
        } finally {
            // Make sure we put the runnables back on if an error occurs during the diff
            ibdoForOldPlace.setParameter(DisposeHelper.KEY, oldRunnables);
            ibdoForNewPlace.setParameter(DisposeHelper.KEY, newRunnables);
        }

        if (!parentDifferences.isEmpty() || !childDifferences.isEmpty()) {
            final StringBuilder sb = new StringBuilder();
            for (int i = 0; i < parentDifferences.size(); i++) {
                if (i != 0) {
                    sb.append(StringUtils.LF);
                }
                sb.append(identifier).append(": PDiff: ");
                sb.append(parentDifferences.get(i));
            }
            if (!parentDifferences.isEmpty() && !childDifferences.isEmpty()) {
                sb.append(StringUtils.LF);
            }
            for (int i = 0; i < childDifferences.size(); i++) {
                if (i != 0) {
                    sb.append(StringUtils.LF);
                }
                sb.append(identifier).append(": CDiff: ");
                sb.append(childDifferences.get(i));
            }

            return sb.toString();
        }

        return null;
    }
}