com.cedarsoftware.util.GraphComparator Maven / Gradle / Ivy
package com.cedarsoftware.util;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import static com.cedarsoftware.util.GraphComparator.Delta.Command.ARRAY_RESIZE;
import static com.cedarsoftware.util.GraphComparator.Delta.Command.ARRAY_SET_ELEMENT;
import static com.cedarsoftware.util.GraphComparator.Delta.Command.LIST_RESIZE;
import static com.cedarsoftware.util.GraphComparator.Delta.Command.LIST_SET_ELEMENT;
import static com.cedarsoftware.util.GraphComparator.Delta.Command.MAP_PUT;
import static com.cedarsoftware.util.GraphComparator.Delta.Command.MAP_REMOVE;
import static com.cedarsoftware.util.GraphComparator.Delta.Command.OBJECT_ASSIGN_FIELD;
import static com.cedarsoftware.util.GraphComparator.Delta.Command.OBJECT_FIELD_TYPE_CHANGED;
import static com.cedarsoftware.util.GraphComparator.Delta.Command.OBJECT_ORPHAN;
import static com.cedarsoftware.util.GraphComparator.Delta.Command.SET_ADD;
import static com.cedarsoftware.util.GraphComparator.Delta.Command.SET_REMOVE;
/**
* Graph Utility algorithms, such as Asymmetric Graph Difference.
*
* @author John DeRegnaucourt ([email protected])
*
* Copyright [2010] John DeRegnaucourt
*
* 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
*
* License
*
* 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.
*/
@SuppressWarnings("unchecked")
public class GraphComparator
{
public static final String ROOT = "-root-";
public interface ID
{
Object getId(Object objectToId);
}
public static class Delta implements Serializable
{
private static final long serialVersionUID = -4388236892818050806L;
private String srcPtr;
private Object id;
private String fieldName;
private Object srcValue;
private Object targetValue;
private Object optionalKey;
private Command cmd;
public Delta(Object id, String fieldName, String srcPtr, Object srcValue, Object targetValue, Object optKey)
{
this.id = id;
this.fieldName = fieldName;
this.srcPtr = srcPtr;
this.srcValue = srcValue;
this.targetValue = targetValue;
optionalKey = optKey;
}
public Object getId()
{
return id;
}
public void setId(Object id)
{
this.id = id;
}
public String getFieldName()
{
return fieldName;
}
public void setFieldName(String fieldName)
{
this.fieldName = fieldName;
}
public Object getSourceValue()
{
return srcValue;
}
public void setSourceValue(Object srcValue)
{
this.srcValue = srcValue;
}
public Object getTargetValue()
{
return targetValue;
}
public void setTargetValue(Object targetValue)
{
this.targetValue = targetValue;
}
public Object getOptionalKey()
{
return optionalKey;
}
public void setOptionalKey(Object optionalKey)
{
this.optionalKey = optionalKey;
}
public Command getCmd()
{
return cmd;
}
public void setCmd(Command cmd)
{
this.cmd = cmd;
}
public String toString()
{
return "Delta {" +
"id=" + id +
", fieldName='" + fieldName + '\'' +
", srcPtr=" + srcPtr +
", srcValue=" + srcValue +
", targetValue=" + targetValue +
", optionalKey=" + optionalKey +
", cmd='" + cmd + '\'' +
'}';
}
public boolean equals(Object other)
{
if (this == other)
{
return true;
}
if (other == null || getClass() != other.getClass())
{
return false;
}
Delta delta = (Delta) other;
return srcPtr.equals(delta.srcPtr);
}
public int hashCode()
{
return srcPtr.hashCode();
}
/**
* These are all possible Delta.Commands that are generated when performing
* the graph comparison.
*/
public enum Command
{
ARRAY_SET_ELEMENT("array.setElement"),
ARRAY_RESIZE("array.resize"),
OBJECT_ASSIGN_FIELD("object.assignField"),
OBJECT_ORPHAN("object.orphan"),
OBJECT_FIELD_TYPE_CHANGED("object.fieldTypeChanged"),
SET_ADD("set.add"),
SET_REMOVE("set.remove"),
MAP_PUT("map.put"),
MAP_REMOVE("map.remove"),
LIST_RESIZE("list.resize"),
LIST_SET_ELEMENT("list.setElement");
private final String name;
Command(final String name)
{
this.name = name.intern();
}
public String getName()
{
return name;
}
public static Command fromName(String name)
{
if (name == null || "".equals(name.trim()))
{
throw new IllegalArgumentException("Name is required for Command.forName()");
}
name = name.toLowerCase();
for (Command t : Command.values())
{
if (t.getName().equals(name))
{
return t;
}
}
throw new IllegalArgumentException("Unknown Command enum: " + name);
}
}
}
public static class DeltaError extends Delta
{
private static final long serialVersionUID = 6248596026486571238L;
public String error;
public DeltaError(String error, Delta delta)
{
super(delta.getId(), delta.fieldName, delta.srcPtr, delta.srcValue, delta.targetValue, delta.optionalKey);
this.error = error;
}
public String getError()
{
return error;
}
public String toString(){
return String.format("%s (%s)", getError(), super.toString());
}
}
public interface DeltaProcessor
{
void processArraySetElement(Object srcValue, Field field, Delta delta);
void processArrayResize(Object srcValue, Field field, Delta delta);
void processObjectAssignField(Object srcValue, Field field, Delta delta);
void processObjectOrphan(Object srcValue, Field field, Delta delta);
void processObjectTypeChanged(Object srcValue, Field field, Delta delta);
void processSetAdd(Object srcValue, Field field, Delta delta);
void processSetRemove(Object srcValue, Field field, Delta delta);
void processMapPut(Object srcValue, Field field, Delta delta);
void processMapRemove(Object srcValue, Field field, Delta delta);
void processListResize(Object srcValue, Field field, Delta delta);
void processListSetElement(Object srcValue, Field field, Delta delta);
class Helper
{
private static Object getFieldValueAs(Object source, Field field, Class> type, Delta delta)
{
Object fieldValue;
try
{
fieldValue = field.get(source);
}
catch (Exception e)
{
throw new RuntimeException(delta.cmd + " failed, unable to access field: " + field.getName() +
", obj id: " + delta.id + ", optionalKey: " + getStringValue(delta.optionalKey), e);
}
if (fieldValue == null)
{
throw new RuntimeException(delta.cmd + " failed, null value at field: " + field.getName() + ", obj id: " +
delta.id + ", optionalKey: " + getStringValue(delta.optionalKey));
}
if (!type.isAssignableFrom(fieldValue.getClass()))
{
throw new ClassCastException(delta.cmd + " failed, field: " + field.getName() + " is not of type: " +
type.getName() + ", obj id: " + delta.id + ", optionalKey: " + getStringValue(delta.optionalKey));
}
return fieldValue;
}
private static int getResizeValue(Delta delta)
{
boolean rightType = delta.optionalKey instanceof Integer ||
delta.optionalKey instanceof Long ||
delta.optionalKey instanceof Short ||
delta.optionalKey instanceof Byte ||
delta.optionalKey instanceof BigInteger;
if (rightType && ((Number)delta.optionalKey).intValue() >= 0)
{
return ((Number)delta.optionalKey).intValue();
}
else
{
throw new IllegalArgumentException(delta.cmd + " failed, the optionalKey must be a integer value 0 or greater, field: " + delta.fieldName +
", obj id: " + delta.id + ", optionalKey: " + getStringValue(delta.optionalKey));
}
}
private static String getStringValue(Object foo)
{
if (foo == null)
{
return "null";
}
else if (foo.getClass().isArray())
{
StringBuilder s = new StringBuilder();
s.append('[');
final int len = Array.getLength(foo);
for (int i=0; i < len; i++)
{
Object element = Array.get(foo, i);
s.append(element == null ? "null" : element.toString());
if (i < len - 1)
{
s.append(',');
}
}
s.append(']');
return s.toString();
}
return foo.toString();
}
}
}
/**
* Perform the asymmetric graph delta. This will compare two disparate graphs
* and generate the necessary 'commands' to convert the source graph into the
* target graph. All nodes (cities) in the graph must be uniquely identifiable.
* An ID interface must be passed in, where the supplied implementation of this, usually
* done as an anonymous inner function, implements the ID.getId() method. The
* compare() function uses this interface to get the unique ID from the graph nodes.
*
* @return Collection of Delta records. Each delta record records a difference
* between graph A - B (asymmetric difference). It contains the information required to
* morph B into A. For example, if Graph B represents a stored object model in the
* database, and Graph A is an inbound change of that graph, the deltas can be applied
* to B such that the persistent storage will now be A.
*/
public static List compare(Object source, Object target, final ID idFetcher)
{
Set deltas = new LinkedHashSet<>();
Set visited = new HashSet<>();
LinkedList stack = new LinkedList<>();
stack.push(new Delta(0L, ROOT, ROOT, source, target, null));
while (!stack.isEmpty())
{
Delta delta = stack.pop();
String path = delta.srcPtr;
if (!stack.isEmpty())
{
path += "." + System.identityHashCode(stack.peek().srcValue);
}
// for debugging
// System.out.println("path = " + path);
if (visited.contains(path))
{ // handle cyclic graphs correctly.
// srcPtr is taken into account (see Delta.equals()), which means
// that an instance alone is not enough to skip, the pointer to it
// must also be identical (before skipping it).
continue;
}
final Object srcValue = delta.srcValue;
final Object targetValue = delta.targetValue;
visited.add(path);
if (srcValue == targetValue)
{ // Same instance is always equal to itself.
continue;
}
if (srcValue == null || targetValue == null)
{ // If either one is null, they are not equal (both can't be null, due to above comparison).
delta.setCmd(OBJECT_ASSIGN_FIELD);
deltas.add(delta);
continue;
}
if (!srcValue.getClass().equals(targetValue.getClass()))
{ // Must be same class when not a Map, Set, List. This allows comparison to
// ignore an ArrayList versus a LinkedList (only the contents will be checked).
if (!((srcValue instanceof Map && targetValue instanceof Map) ||
(srcValue instanceof Set && targetValue instanceof Set) ||
(srcValue instanceof List && targetValue instanceof List)))
{
delta.setCmd(OBJECT_FIELD_TYPE_CHANGED);
deltas.add(delta);
continue;
}
}
if (isLogicalPrimitive(srcValue.getClass()))
{
if (!srcValue.equals(targetValue))
{
delta.setCmd(OBJECT_ASSIGN_FIELD);
deltas.add(delta);
}
continue;
}
// Special handle [] types because they require CopyElement / Resize commands unique to Arrays.
if (srcValue.getClass().isArray())
{
compareArrays(delta, deltas, stack, idFetcher);
continue;
}
// Special handle Sets because they require Add/Remove commands unique to Sets
if (srcValue instanceof Set)
{
compareSets(delta, deltas, stack, idFetcher);
continue;
}
// Special handle Maps because they required Put/Remove commands unique to Maps
if (srcValue instanceof Map)
{
compareMaps(delta, deltas, stack, idFetcher);
continue;
}
// Special handle List because they require CopyElement / Resize commands unique to List
if (srcValue instanceof List)
{
compareLists(delta, deltas, stack, idFetcher);
continue;
}
if (srcValue instanceof Collection)
{
throw new RuntimeException("Detected custom Collection that does not extend List or Set: " +
srcValue.getClass().getName() + ". GraphUtils.compare() needs to be updated to support it, obj id: " + delta.id + ", field: " + delta.fieldName);
}
if (isIdObject(srcValue, idFetcher) && isIdObject(targetValue, idFetcher))
{
final Object srcId = idFetcher.getId(srcValue);
final Object targetId = idFetcher.getId(targetValue);
if (!srcId.equals(targetId))
{ // Field references different object, need to create a command that assigns the new object to the field.
// This maintains 'Graph Shape'
delta.setCmd(OBJECT_ASSIGN_FIELD);
deltas.add(delta);
continue;
}
final Collection fields = ReflectionUtils.getDeepDeclaredFields(srcValue.getClass());
String sysId = "(" + System.identityHashCode(srcValue) + ").";
for (Field field : fields)
{
try
{
String srcPtr = sysId + field.getName();
stack.push(new Delta(srcId, field.getName(), srcPtr, field.get(srcValue), field.get(targetValue), null));
}
catch (Exception ignored) { }
}
}
else
{ // Non-ID object, need to check for 'deep' equivalency (best we can do). This works, but the change could
// be at a lower level in the graph (overly safe). However, without an ID, there is no way to point to the
// lower level difference object.
if (!DeepEquals.deepEquals(srcValue, targetValue))
{
delta.setCmd(OBJECT_ASSIGN_FIELD);
deltas.add(delta);
}
}
}
// source objects by ID
final Set
© 2015 - 2024 Weber Informatics LLC | Privacy Policy