Package org.freezedry.difference

Source Code of org.freezedry.difference.ObjectDifferenceCalculator$Difference

/*
* Copyright 2013 Robert Philipp
*
*  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 org.freezedry.difference;

import org.freezedry.persistence.PersistenceEngine;
import org.freezedry.persistence.containers.Pair;
import org.freezedry.persistence.keyvalue.KeyValueBuilder;
import org.freezedry.persistence.keyvalue.renderers.PersistenceRenderer;
import org.freezedry.persistence.tree.InfoNode;
import org.freezedry.persistence.writers.KeyValueMapWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.regex.Pattern;

/**
* Calculates the difference between two object of the same type, and lists the flattened properties that differ. Also
* provides access to flatten objects into key-value pairs.
*
* By calling {@link #listOrderIgnored()} any differences in collections or arrays that are purely caused by the ordering
* are ignored. For example, if we have a list, numbers=(1,2,3,4,5), in the reference object, and then we compare it to
* the modified object where the list, numbers=(2,4,3,1,5), the difference calculator would report no difference. This
* works for multi-dimensional collections or arrays, and for list containing objects that contains list, etc.
*
* @author Robert Philpp
*         10/7/13, 1:27 PM
*/
public class ObjectDifferenceCalculator {

  private static Logger LOGGER = LoggerFactory.getLogger( ObjectDifferenceCalculator.class );

  private static Pattern LIST_PATTERN = Pattern.compile( "(.)*(\\[[\\d]+\\])+(.)*" );

  private final KeyValueMapWriter mapWriter;
  private final PersistenceEngine persistenceEngine = new PersistenceEngine().withPersistNullValues();
  private boolean isListOrderIgnored = false;

  /**
   * Constructs a basic key-value writer that uses the specified renderers and separator.
   * @param renderers The mapping between the {@link Class} represented by an {@link org.freezedry.persistence.tree.InfoNode} and
   * the {@link org.freezedry.persistence.keyvalue.renderers.PersistenceRenderer} used to create the key-value pair.
   * @param arrayRenderer The {@link org.freezedry.persistence.keyvalue.renderers.PersistenceRenderer} used to create key-value pairs for
   * {@link org.freezedry.persistence.tree.InfoNode}s that represent an array.
   * @param keySeparator The separator between the flattened elements of the key
   * @see org.freezedry.persistence.keyvalue.AbstractKeyValueBuilder#getRenderer(Class)
   */
  public ObjectDifferenceCalculator( final Map<Class<?>, PersistenceRenderer> renderers,
                     final PersistenceRenderer arrayRenderer,
                     final String keySeparator )
  {
    mapWriter = new KeyValueMapWriter( renderers, arrayRenderer, keySeparator );
  }

  /**
   * Constructs a basic key-value writer that uses the default renderers and specified separator.
   * @param keySeparator The separator between the flattened elements of the key
   */
  public ObjectDifferenceCalculator( final String keySeparator )
  {
    mapWriter = new KeyValueMapWriter( keySeparator );
  }

  /**
   * Constructs a basic key-value writer that uses the default renderers and separator.
   */
  public ObjectDifferenceCalculator()
  {
    mapWriter = new KeyValueMapWriter();
  }

  /**
   * Constructs a key-value writer using the specified key-value list builder
   * @param builder The {@link org.freezedry.persistence.keyvalue.KeyValueBuilder} used to flatten the semantic model
   */
  public ObjectDifferenceCalculator( final KeyValueBuilder builder )
  {
    mapWriter = new KeyValueMapWriter( builder );
  }

  /**
   * Sets this difference calculator to ignore the ordering of lists when comparing objects. When ignoring order,
   * the difference calculator checks only that all the elements of a list match. This works for multidimensional lists
   * or objects contained in, and containing, lists.
   * @return This {@link org.freezedry.difference.ObjectDifferenceCalculator} for chaining
   */
  public ObjectDifferenceCalculator listOrderIgnored()
  {
    this.isListOrderIgnored = true;
    return this;
  }

  /**
   * Sets this difference calculator to care the ordering of lists when comparing objects.
   * @return This {@link org.freezedry.difference.ObjectDifferenceCalculator} for chaining
   */
  public ObjectDifferenceCalculator listOrderMatters()
  {
    this.isListOrderIgnored = false;
    return this;
  }

  /**
   * If set to {@code true} then tells the {@link org.freezedry.persistence.PersistenceEngine} to persist null values.
   * By default the {@link org.freezedry.persistence.PersistenceEngine} does not persist null values.
   * @param isPersistNullValues Whether or not to persist fields that have null values
   */
  public void setPersistNullValues( final boolean isPersistNullValues )
  {
    persistenceEngine.setPersistNullValues( isPersistNullValues );
  }

  /**
   * Calculates the difference between the two specified objects and returns the difference as key-value pairs. The
   * {@link Difference} object holds the value of the {@code object}'s property and the {@code referenceObjects}'s property
   * that differ.
   * @param modifiedObject The new object to compare against the reference object
   * @param referenceObject The reference object against which to compare the object
   * @param <T> The object's and the reference object's type.
   * @return A {@link Map} that holds the flattened property names of all the properties that are different between the
   * object and the reference object. For each entry in the map, the {@link Difference} holds the object's property
   * value and the reference object's property value
   */
  public final < T > Map< String, Difference > calculateDifference( final T modifiedObject, final T referenceObject )
  {
    final Map< String, Object > objectMap = flattenObject( modifiedObject );
    final Map< String, Object > referenceObjectMap = flattenObject( referenceObject );

    final Set< String > keys = new LinkedHashSet<>();
    keys.addAll( objectMap.keySet() );
    keys.addAll( referenceObjectMap.keySet() );

    final Map< String, Difference > difference = new LinkedHashMap<>();
    for( String key : keys )
    {
      final Object modifiedValue = objectMap.get( key );
      final Object referenceValue = referenceObjectMap.get( key );
      if( ( modifiedValue != null && referenceValue != null && !modifiedValue.toString().equals( referenceValue.toString() ) ) ||
        ( modifiedValue != null && referenceValue == null ) ||
        ( modifiedValue == null && referenceValue != null ) )
      {
        difference.put( key, new Difference( modifiedValue, referenceValue ) );
      }
    }

    if( isListOrderIgnored )
    {
      ignoreListOrder( difference );
    }

    return difference;
  }

  /**
   * Flattens the object into key-value pairs.
   * @param object The object to flatten
   * @return The flattened version of the object
   * @see KeyValueBuilder
   */
  public final Map< String, Object > flattenObject( final Object object )
  {
    final InfoNode rootNode = persistenceEngine.createSemanticModel( object );
    return mapWriter.createMap( rootNode );
  }

  /**
   * Removes differences where the difference is only due to the place within a list the difference occurs.
   * @param differences A map holding differences between two objects and the keys representing the flattened field names
   * @return The map with differences removed that are only due to their placement in a list
   */
  private Map< String, Difference > ignoreListOrder( final Map< String, Difference > differences )
  {
    LOGGER.debug( "Ignoring the order of lists" );
    final Map< String, Map< String, Difference > > categories = classifyLists( differences );
    for( Map.Entry< String, Map< String, Difference > > entry : categories.entrySet() )
    {
      // differences, even though they're lists, that have only one element are different regardless of
      // whether order matters, so, in that case, we don't check that difference for list ordering
      if( entry.getValue().size() > 1 )
      {
        LOGGER.trace( "Comparing lists: " + entry.getKey() );
        // todo if one-dimensional list, then just use the map< value, count > method to compare lists

        // create the value tree (pair.first) and reference tree (pair.second)
        final Pair< Group, Group > trees = createTree( entry.getKey(), entry.getValue() );

        // compare the trees, returning the names of lists that are the same
        final List< String > groupNames = getGroupNames( entry.getKey(), entry.getValue().keySet() );
        removeEquivalentLists( trees.getFirst(), trees.getSecond(), groupNames, differences );
      }
    }
    return differences;
  }

  /**
   * Removes lists from the difference map that are equivalent when order is not taken into account.
   * @param values The root group (node) holding the values, or modified values
   * @param referenceValues The root group (node) holding the reference values
   * @param groupNames The names of the groups in the value (and reference value) trees
   * @param differences The raw differences as calculated by the {@link #calculateDifference(Object, Object)} method
   *                    before order is taken into account.
   */
  private void removeEquivalentLists( final Group values,
                    final Group referenceValues,
                    final List<String> groupNames,
                    final Map<String, Difference> differences )
  {
    // todo make more efficient by remove group names for which the lists are equivalent
    for( String groupName : groupNames )
    {
      final Group value = values.findGroup( groupName );
      if( value != null )
      {
        for( String groupName2 : groupNames )
        {
          final Group referenceValue = referenceValues.findGroup( groupName2 );
          if( referenceValue != null && value.equivalentValues( referenceValue ) )
          {
            // remove the list from the differences map
            for( Map.Entry< String, Set< String > > entry : value.getValues().entrySet() )
            {
              for( String key : entry.getValue() )
              {
                differences.remove( key );
                LOGGER.debug( "Removed (only difference in order): " + key );
              }
            }

            // remove the nodes, since they are the same
            value.getParent().removeChild( value );
            referenceValue.getParent().removeChild( referenceValue );

            break;
          }
        }
      }
    }
  }

  /**
   * Classifies the lists into groups representing the same field. The categories are represented by the key in
   * the outer return map, which is the group name with the indexes changed to wild cards (i.e. [1] or [2] -> [*])
   * @param differences The object differences
   * @return a map whose keys are the group name patterns, and whose values are the differences.
   */
  private Map< String, Map< String, Difference > > classifyLists( final Map< String, Difference > differences )
  {
    final Map< Pattern, Map< String, Difference > > mainGroups = new LinkedHashMap<>();

    // finds the list-patterns and classifies each of the field names accordingly
    for( Map.Entry< String, Difference > entry : differences.entrySet() )
    {
      // for keys that fit the list pattern, we classify them in groups that represent the same fields
      if( LIST_PATTERN.matcher( entry.getKey() ).matches() )
      {
        // see if any of the patterns match the current key, if not, then we add the new pattern to the map
        // along with the key and its value
        boolean isClassified = false;
        for( Map.Entry< Pattern, Map< String, Difference > > classEntry : mainGroups.entrySet() )
        {
          if( classEntry.getKey().matcher( entry.getKey() ).matches() )
          {
            classEntry.getValue().put( entry.getKey(), entry.getValue() );
            isClassified = true;
            break;
          }
        }
        if( !isClassified )
        {
          final Map< String, Difference > difference = new HashMap<>();
          difference.put( entry.getKey(), entry.getValue() );
          final Pattern pattern = Pattern.compile( entry.getKey().replaceAll( "\\[[\\d]+\\]", "\\\\[[\\\\d]+\\\\]" ) );
          mainGroups.put( pattern, difference );
        }
      }
    }

    // convert the indexes for the regex patterns (the group names) to "[*]"
    final Map< String, Map< String, Difference > > groups = new HashMap<>( mainGroups.size() );
    for( Map.Entry< Pattern, Map< String, Difference > > entry : mainGroups.entrySet() )
    {
      final String key = entry.getKey().pattern().replaceAll( Pattern.quote( "\\[[\\d]+\\]" ), "[*]" );
      groups.put( key, entry.getValue() );
    }
    return groups;
  }

  /**
   * Creates the value and reference trees representing the lists. The first elements in the pairs contain the value
   * tree, and the second elements contain the reference-value tree.
   * @param category The list category
   * @param differences The object differences
   * @return the value and reference trees representing the lists. The first elements in the pairs contain the value
   * tree, and the second elements contain the reference-value tree.
   */
  private Pair< Group, Group > createTree( final String category, final Map< String, Difference > differences )
  {
    // split the difference objects into values and reference values
    final Map< String, Object > values = new HashMap<>();
    final Map< String, Object > referenceValues = new HashMap<>();
    for( Map.Entry< String, Difference > entry : differences.entrySet() )
    {
      values.put( entry.getKey(), entry.getValue().getObject() );
      referenceValues.put( entry.getKey(), entry.getValue().getReferenceObject() );
    }

    // the number of groups give the dimensions of the lists, and is used as the max
    // levels of the tree as a stopping condition
    final int numGroups = calcListDimensions( category );
    if( numGroups < 1 )
    {
      return null;
    }

    // create the tree for the values
    final Group root = new Group( category );
    createGroup( root, 1, numGroups, category, values );

    // create the tree for the reference values
    final Group referenceRoot = new Group( category );
    createGroup( referenceRoot, 1, numGroups, category, referenceValues );

    return new Pair<>( root, referenceRoot );
  }

  /**
   * Calculates the number of unique indexes for the next dimension of the multidimensional array starting from the
   * dimension represented by the specified name.
   * @param name The name representing the current dimension of the multi-dimensional array
   * @param keys The flattened field names representing the entire multi-dimensional array
   * @return the number of unique indexes for the next dimension of the multidimensional array starting from the
   * dimension represented by the specified name.
   */
  private int getNumGroups( final String name, final Set< String > keys )
  {
    final int openBracket = name.lastIndexOf( "[" );

    // root node, no dimensions
    if( openBracket == -1 )
    {
      return -1;
    }
    final Set< Integer > indexes = new HashSet<>();
    for( String key: keys )
    {
      final int closeBracket = key.indexOf( "]", openBracket + 1 );
      indexes.add( Integer.valueOf( key.substring( openBracket + 1, closeBracket ) ) );
    }
    return Collections.max( indexes ) + 1;
  }

  /**
   * Returns the group names with the indexes filled in (i.e. the wild card is replaced). For example, if the name
   * is {@code name[0][*]} and there are 3 groups within that name found in the keys, then the following list of names
   * will be returned {@code name[0][0], name[0][1], name[0][2]}.
   * @param name The name that serves as a template (i.e name[0][*])
   * @param keys The keys to use to determine the number of groups at this dimension
   * @return the group names with the indexes filled in (i.e. the wild card is replaced)
   */
  private List< String > getGroupNames( final String name, final Set< String > keys )
  {
    final List< String > indexes = new ArrayList<>();
    getGroupNames( name, keys, indexes );
    return indexes;

//    final List< String > indexes = new ArrayList<>();
//
//    final int index = name.indexOf( "[*]" );
//    final String truncated = index > 0 ? name.substring( 0, name.indexOf( "[*]" )+3 ) : name;
//
//    final int numGroups = getNumGroups( truncated, keys );
//    for( int i = 0; i < numGroups; ++i )
//    {
//      indexes.add( name.replaceFirst( "\\*", Integer.toString( i ) ) );
//    }
//    return indexes;
  }

  private void getGroupNames( final String name, final Set< String > keys, final List< String > indexes )
  {
    final int index = name.indexOf( "[*]" );
    final int lastIndex = name.lastIndexOf( "[*]" );
    final String truncated = index > 0 ? name.substring( 0, name.indexOf( "[*]" )+3 ) : name;
    final int numGroups = getNumGroups( truncated, keys );
    for( int i = 0; i < numGroups; ++i )
    {
      final String indexedName = name.replaceFirst( "\\*", Integer.toString( i ) );
      indexes.add( indexedName );
      if( index < lastIndex )
      {
        getGroupNames( indexedName, keys, indexes );
      }
    }
  }

  /**
   * Creates the tree (recursively) of groups, each representing an element of a list (which itself may contain lists).
   * The leaf groups are the actual values of the lists.
   * @param parent The parent group to which the newly created groups are added.
   * @param currentLevel The current level of within the tree
   * @param maxLevel The max number of levels the tree will have (the dimension of the lists, i.e. name[i][j] has two dimensions)
   * @param groupName The name of the group (which will be something like name[*], or name[1][*], etc)
   * @param values The key-values representing the lists
   */
  private void createGroup( final Group parent, final int currentLevel, final int maxLevel, final String groupName, final Map< String, Object > values )
  {
    final List< String > groupNames = getGroupNames( groupName, values.keySet() );
    if( currentLevel < maxLevel )
    {
      for( String name : groupNames )
      {
        final Group group = new Group( name );
        parent.addChild( group );
        createGroup( group, currentLevel+1, maxLevel, name, values );
      }
    }
    else
    {
      // leaf node, add the lists
      for( Map.Entry< String, Object > entry : values.entrySet() )
      {
        if( groupNames.contains( entry.getKey() ) )
        {
          final String value = ( entry.getValue() == null ? null : entry.getValue().toString() );
          parent.addValue( entry.getKey(), value );
        }
      }
    }
  }

  /**
   * Calculates the dimensions of the list (i.e. name[*][*] has dimensions of 2)
   * @param category The category name
   * @return The number of dimensions in the list
   */
  private int calcListDimensions( final String category )
  {
    int count = 0;
    int index = 0;
    while( ( index = category.indexOf( "[*]", index+3 ) ) > 0 )
    {
      ++count;
    }
    return count;
  }

  /**
   * Class representing the difference between the object's and the reference object's value.
   */
  public final static class Difference {

    private final Object object;
    private final Object referenceObject;

    /**
     * The holds the value of a property of the object and the reference object
     * @param modifiedObject The value of the object's property
     * @param referenceObject The value of the reference object's property
     */
    public Difference( final Object modifiedObject, final Object referenceObject )
    {
      this.object = modifiedObject;
      this.referenceObject = referenceObject;
    }

    /**
     * @return The object's value
     */
    public Object getObject()
    {
      return object;
    }

    /**
     * @return The reference object's value
     */
    public Object getReferenceObject()
    {
      return referenceObject;
    }

    /**
     * @return a string representation of the difference
     */
    @Override
    public String toString()
    {
      return "Modified Object: " + (object == null ? "[null]" : object.toString()) + "; " +
          "Reference Object: " + (referenceObject == null ? "[null]" : referenceObject.toString());
    }
  }
}
TOP

Related Classes of org.freezedry.difference.ObjectDifferenceCalculator$Difference

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.