/* * Copyright 2014 the original author or authors. * * 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.springframework.sync; import java.io.IOException; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import org.springframework.util.ObjectUtils; import difflib.Delta; import difflib.Delta.TYPE; import difflib.DiffUtils; /** * Provides support for producing a {@link Patch} from the comparison of two objects. * @author Craig Walls */ public class Diff { /** * Performs a difference operation between two objects, resulting in a {@link Patch} describing the differences. * * @param original the original, unmodified object. * @param modified the modified object. * @return a {@link Patch} describing the differences between the two objects. * @throws PatchException if an error occurs while performing the difference. */ public static Patch diff(Object original, Object modified) throws PatchException { try { List<PatchOperation> operations = new ArrayList<PatchOperation>(); if (original instanceof List && modified instanceof List) { diffList(operations, "", (List<?>) original, (List<?>) modified); } else { diffNonList(operations, "", original, modified); } return new Patch(operations); } catch (Exception e) { throw new PatchException("Error performing diff:", e); } } // private helpers private static void diffList(List<PatchOperation> operations, String path, List<?> original, List<?> modified) throws IOException, IllegalAccessException { difflib.Patch diff = DiffUtils.diff(original, modified); List<Delta> deltas = diff.getDeltas(); for (Delta delta : deltas) { TYPE type = delta.getType(); int revisedPosition = delta.getRevised().getPosition(); if (type == TYPE.CHANGE) { List<?> lines = delta.getRevised().getLines(); for(int offset = 0; offset < lines.size(); offset++) { Object originalObject = original.get(revisedPosition + offset); Object revisedObject = modified.get(revisedPosition + offset); diffNonList(operations, path + "/" + (revisedPosition + offset), originalObject, revisedObject); } } else if (type == TYPE.INSERT) { List<?> lines = delta.getRevised().getLines(); for(int offset = 0; offset < lines.size(); offset++) { operations.add(new AddOperation(path + "/" + (revisedPosition + offset), lines.get(offset))); } } else if (type == TYPE.DELETE) { List<?> lines = delta.getOriginal().getLines(); for(int offset = 0; offset < lines.size(); offset++) { Object originalObject = original.get(revisedPosition + offset); operations.add(new TestOperation(path + "/" + revisedPosition, originalObject)); operations.add(new RemoveOperation(path + "/" + revisedPosition)); } } } } private static void diffNonList(List<PatchOperation> operations, String path, Object original, Object modified) throws IOException, IllegalAccessException { if (!ObjectUtils.nullSafeEquals(original, modified)) { if (modified == null) { operations.add(new RemoveOperation(path)); return; } if (isPrimitive(modified)) { operations.add(new TestOperation(path, original)); if (original == null) { operations.add(new AddOperation(path, modified)); } else { operations.add(new ReplaceOperation(path, modified)); } return; } Class<? extends Object> originalType = original.getClass(); Field[] fields = originalType.getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); Class<?> fieldType = field.getType(); Object origValue = field.get(original); Object modValue = field.get(modified); if ((fieldType.isArray() || Collection.class.isAssignableFrom(fieldType)) && origValue != null && modValue != null) { if (Collection.class.isAssignableFrom(fieldType)) { diffList(operations, path + "/" + field.getName(), (List<?>) origValue, (List<?>) modValue); } else if (fieldType.isArray()) { diffList(operations, path + "/" + field.getName(), Arrays.asList((Object[]) origValue), Arrays.asList((Object[]) modValue)); } } else { diffNonList(operations, path+"/"+field.getName(), origValue, modValue); } } } } private static boolean isPrimitive(Object o) { return o instanceof String || o instanceof Number || o instanceof Boolean; } }