diff --git a/pom.xml b/pom.xml index c415e57..95ae150 100644 --- a/pom.xml +++ b/pom.xml @@ -144,11 +144,6 @@ jackson-core ${jackson.version} - - org.apache.commons - commons-collections4 - 4.2 - test commons-io diff --git a/src/main/java/com/flipkart/zjsonpatch/EditScript.java b/src/main/java/com/flipkart/zjsonpatch/EditScript.java new file mode 100644 index 0000000..bfdef32 --- /dev/null +++ b/src/main/java/com/flipkart/zjsonpatch/EditScript.java @@ -0,0 +1,147 @@ +/* + * Copyright 2021 flipkart.com zjsonpatch. + * + * 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 com.flipkart.zjsonpatch; + +import java.util.ArrayList; +import java.util.List; + +import com.flipkart.zjsonpatch.InternalUtils.CommandVisitor; +import com.flipkart.zjsonpatch.InternalUtils.DeleteCommand; +import com.flipkart.zjsonpatch.InternalUtils.EditCommand; +import com.flipkart.zjsonpatch.InternalUtils.InsertCommand; +import com.flipkart.zjsonpatch.InternalUtils.KeepCommand; + +/** + * This class gathers all the {@link EditCommand commands} needed to transform + * one objects sequence into another objects sequence. + *

+ * An edit script is the most general view of the differences between two + * sequences. It is built as the result of the comparison between two sequences + * by the {@link SequencesComparator SequencesComparator} class. The user can + * walk through it using the visitor design pattern. + *

+ *

+ * It is guaranteed that the objects embedded in the {@link InsertCommand insert + * commands} come from the second sequence and that the objects embedded in + * either the {@link DeleteCommand delete commands} or {@link KeepCommand keep + * commands} come from the first sequence. This can be important if subclassing + * is used for some elements in the first sequence and the {@code equals} + * method is specialized. + *

+ *

+ * ATTRIBUTION NOTICE:
+ * This class contains source code copied from + * Apache commons-collection4 + * + *

+ * + * @see SequencesComparator + * @see EditCommand + * @see CommandVisitor + * @see ReplacementsHandler + * + * @since 4.0 + */ +public class EditScript { + + /** Container for the commands. */ + private final List> commands; + + /** Length of the longest common subsequence. */ + private int lcsLength; + + /** Number of modifications. */ + private int modifications; + + /** + * Simple constructor. Creates a new empty script. + */ + public EditScript() { + commands = new ArrayList<>(); + lcsLength = 0; + modifications = 0; + } + + /** + * Add a keep command to the script. + * + * @param command command to add + */ + public void append(final KeepCommand command) { + commands.add(command); + ++lcsLength; + } + + /** + * Add an insert command to the script. + * + * @param command command to add + */ + public void append(final InsertCommand command) { + commands.add(command); + ++modifications; + } + + /** + * Add a delete command to the script. + * + * @param command command to add + */ + public void append(final DeleteCommand command) { + commands.add(command); + ++modifications; + } + + /** + * Visit the script. The script implements the visitor design + * pattern, this method is the entry point to which the user supplies its + * own visitor, the script will be responsible to drive it through the + * commands in order and call the appropriate method as each command is + * encountered. + * + * @param visitor the visitor that will visit all commands in turn + */ + public void visit(final CommandVisitor visitor) { + for (final EditCommand command : commands) { + command.accept(visitor); + } + } + + /** + * Get the length of the Longest Common Subsequence (LCS). The length of the + * longest common subsequence is the number of {@link KeepCommand keep + * commands} in the script. + * + * @return length of the Longest Common Subsequence + */ + public int getLCSLength() { + return lcsLength; + } + + /** + * Get the number of effective modifications. The number of effective + * modification is the number of {@link DeleteCommand delete} and + * {@link InsertCommand insert} commands in the script. + * + * @return number of effective modifications + */ + public int getModifications() { + return modifications; + } + + +} \ No newline at end of file diff --git a/src/main/java/com/flipkart/zjsonpatch/InternalUtils.java b/src/main/java/com/flipkart/zjsonpatch/InternalUtils.java index 7ac78b3..1ae70c7 100644 --- a/src/main/java/com/flipkart/zjsonpatch/InternalUtils.java +++ b/src/main/java/com/flipkart/zjsonpatch/InternalUtils.java @@ -1,13 +1,37 @@ +/* + * Copyright 2021 flipkart.com zjsonpatch. + * + * 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 com.flipkart.zjsonpatch; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; +import java.io.Serializable; import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; import java.util.List; +import java.util.Objects; +/** + *

+ * ATTRIBUTION NOTICE:
+ * This class contains source code copied from
+ * Apache commons-collection4 + * + *

+ */ class InternalUtils { static List toList(ArrayNode input) { @@ -19,40 +43,480 @@ static List toList(ArrayNode input) { return toReturn; } - static List longestCommonSubsequence(final List a, final List b) { - if (a == null || b == null) { - throw new NullPointerException("List must not be null for longestCommonSubsequence"); - } - - List toReturn = new LinkedList(); - - int aSize = a.size(); - int bSize = b.size(); - int temp[][] = new int[aSize + 1][bSize + 1]; - - for (int i = 1; i <= aSize; i++) { - for (int j = 1; j <= bSize; j++) { - if (i == 0 || j == 0) { - temp[i][j] = 0; - } else if (a.get(i - 1).equals(b.get(j - 1))) { - temp[i][j] = temp[i - 1][j - 1] + 1; - } else { - temp[i][j] = Math.max(temp[i][j - 1], temp[i - 1][j]); - } - } - } - int i = aSize, j = bSize; - while (i > 0 && j > 0) { - if (a.get(i - 1).equals(b.get(j - 1))) { - toReturn.add(a.get(i - 1)); - i--; - j--; - } else if (temp[i - 1][j] > temp[i][j - 1]) - i--; - else - j--; - } - Collections.reverse(toReturn); - return toReturn; + + //----------------------------------------------------------------------- + /** + * Returns the longest common subsequence (LCS) of two sequences (lists). + * + * @param the element type + * @param a the first list + * @param b the second list + * @return the longest common subsequence + * @throws NullPointerException if either list is {@code null} + * @since 4.0 + */ + public static List longestCommonSubsequence(final List a, final List b) { + return longestCommonSubsequence( a, b, DefaultEquator.defaultEquator() ); + } + + /** + * Returns the longest common subsequence (LCS) of two sequences (lists). + * + * @param the element type + * @param listA the first list + * @param listB the second list + * @param equator the equator used to test object equality + * @return the longest common subsequence + * @throws NullPointerException if either list or the equator is {@code null} + * @since 4.0 + */ + public static List longestCommonSubsequence(final List listA, final List listB, + final Equator equator) { + Objects.requireNonNull(listA, "listA"); + Objects.requireNonNull(listB, "listB"); + Objects.requireNonNull(equator, "equator"); + + final SequencesComparator comparator = new SequencesComparator<>(listA, listB, equator); + final EditScript script = comparator.getScript(); + final LcsVisitor visitor = new LcsVisitor<>(); + script.visit(visitor); + return visitor.getSubSequence(); + } + + /** + * A helper class used to construct the longest common subsequence. + */ + private static final class LcsVisitor implements CommandVisitor { + private final ArrayList sequence; + + LcsVisitor() { + sequence = new ArrayList<>(); + } + + @Override + public void visitInsertCommand(final E object) { + // noop + } + + @Override + public void visitDeleteCommand(final E object) { + // noop + } + + @Override + public void visitKeepCommand(final E object) { + sequence.add(object); + } + + public List getSubSequence() { + return sequence; + } + } + + /** + * An equation function, which determines equality between objects of type T. + *

+ * It is the functional sibling of {@link java.util.Comparator}; {@link Equator} is to + * {@link Object} as {@link java.util.Comparator} is to {@link java.lang.Comparable}. + *

+ * + * @param the types of object this {@link Equator} can evaluate. + * @since 4.0 + */ + public static interface Equator { + /** + * Evaluates the two arguments for their equality. + * + * @param o1 the first object to be equated. + * @param o2 the second object to be equated. + * @return whether the two objects are equal. + */ + boolean equate(T o1, T o2); + + /** + * Calculates the hash for the object, based on the method of equality used in the equate + * method. This is used for classes that delegate their {@link Object#equals(Object) equals(Object)} method to an + * Equator (and so must also delegate their {@link Object#hashCode() hashCode()} method), or for implementations + * of {@link org.apache.commons.collections4.map.HashedMap} that use an Equator for the key objects. + * + * @param o the object to calculate the hash for. + * @return the hash of the object. + */ + int hash(T o); + } + + /** + * Default {@link Equator} implementation. + *

+ * Copied from Apache commons-collections + * + * @param the types of object this {@link Equator} can evaluate. + * @since 4.0 + */ + public static class DefaultEquator implements Equator, Serializable { + + /** Serial version UID */ + private static final long serialVersionUID = 825802648423525485L; + + /** Static instance */ + @SuppressWarnings("rawtypes") // the static instance works for all types + public static final DefaultEquator INSTANCE = new DefaultEquator<>(); + + /** + * Hashcode used for {@code null} objects. + */ + public static final int HASHCODE_NULL = -1; + + /** + * Factory returning the typed singleton instance. + * + * @param the object type + * @return the singleton instance + */ + @SuppressWarnings("unchecked") + public static DefaultEquator defaultEquator() { + return DefaultEquator.INSTANCE; + } + + /** + * Restricted constructor. + */ + private DefaultEquator() { + } + + /** + * {@inheritDoc} Delegates to {@link Object#equals(Object)}. + */ + @Override + public boolean equate(final T o1, final T o2) { + return o1 == o2 || o1 != null && o1.equals(o2); + } + + /** + * {@inheritDoc} + * + * @return {@code o.hashCode()} if {@code o} is non- + * {@code null}, else {@link #HASHCODE_NULL}. + */ + @Override + public int hash(final T o) { + return o == null ? HASHCODE_NULL : o.hashCode(); + } + + private Object readResolve() { + return INSTANCE; + } + } + + /** + * Abstract base class for all commands used to transform an objects sequence + * into another one. + *

+ * When two objects sequences are compared through the + * {@link SequencesComparator#getScript SequencesComparator.getScript} method, + * the result is provided has a {@link EditScript script} containing the commands + * that progressively transform the first sequence into the second one. + *

+ *

+ * There are only three types of commands, all of which are subclasses of this + * abstract class. Each command is associated with one object belonging to at + * least one of the sequences. These commands are {@link InsertCommand + * InsertCommand} which correspond to an object of the second sequence being + * inserted into the first sequence, {@link DeleteCommand DeleteCommand} which + * correspond to an object of the first sequence being removed and + * {@link KeepCommand KeepCommand} which correspond to an object of the first + * sequence which {@code equals} an object in the second sequence. It is + * guaranteed that comparison is always performed this way (i.e. the + * {@code equals} method of the object from the first sequence is used and + * the object passed as an argument comes from the second sequence) ; this can + * be important if subclassing is used for some elements in the first sequence + * and the {@code equals} method is specialized. + *

+ * + * @see SequencesComparator + * @see EditScript + * + * @since 4.0 + */ + public static abstract class EditCommand { + + /** Object on which the command should be applied. */ + private final T object; + + /** + * Simple constructor. Creates a new instance of EditCommand + * + * @param object reference to the object associated with this command, this + * refers to an element of one of the sequences being compared + */ + protected EditCommand(final T object) { + this.object = object; + } + + /** + * Returns the object associated with this command. + * + * @return the object on which the command is applied + */ + protected T getObject() { + return object; + } + + /** + * Accept a visitor. + *

+ * This method is invoked for each commands belonging to + * an {@link EditScript EditScript}, in order to implement the visitor design pattern + * + * @param visitor the visitor to be accepted + */ + public abstract void accept(CommandVisitor visitor); + + } + + /** + * Command representing the keeping of one object present in both sequences. + *

+ * When one object of the first sequence {@code equals} another objects in + * the second sequence at the right place, the {@link EditScript edit script} + * transforming the first sequence into the second sequence uses an instance of + * this class to represent the keeping of this object. The objects embedded in + * these type of commands always come from the first sequence. + *

+ * + * @see SequencesComparator + * @see EditScript + * + * @since 4.0 + */ + public static class KeepCommand extends EditCommand { + + /** + * Simple constructor. Creates a new instance of KeepCommand + * + * @param object the object belonging to both sequences (the object is a + * reference to the instance in the first sequence which is known + * to be equal to an instance in the second sequence) + */ + public KeepCommand(final T object) { + super(object); + } + + /** + * Accept a visitor. When a {@code KeepCommand} accepts a visitor, it + * calls its {@link CommandVisitor#visitKeepCommand visitKeepCommand} method. + * + * @param visitor the visitor to be accepted + */ + @Override + public void accept(final CommandVisitor visitor) { + visitor.visitKeepCommand(getObject()); + } + } + + /** + * Command representing the insertion of one object of the second sequence. + *

+ * When one object of the second sequence has no corresponding object in the + * first sequence at the right place, the {@link EditScript edit script} + * transforming the first sequence into the second sequence uses an instance of + * this class to represent the insertion of this object. The objects embedded in + * these type of commands always come from the second sequence. + *

+ * + * @see SequencesComparator + * @see EditScript + * + * @since 4.0 + */ + public static class InsertCommand extends EditCommand { + + /** + * Simple constructor. Creates a new instance of InsertCommand + * + * @param object the object of the second sequence that should be inserted + */ + public InsertCommand(final T object) { + super(object); + } + + /** + * Accept a visitor. When an {@code InsertCommand} accepts a visitor, + * it calls its {@link CommandVisitor#visitInsertCommand visitInsertCommand} + * method. + * + * @param visitor the visitor to be accepted + */ + @Override + public void accept(final CommandVisitor visitor) { + visitor.visitInsertCommand(getObject()); + } + + } + /** + * Command representing the deletion of one object of the first sequence. + *

+ * When one object of the first sequence has no corresponding object in the + * second sequence at the right place, the {@link EditScript edit script} + * transforming the first sequence into the second sequence uses an instance of + * this class to represent the deletion of this object. The objects embedded in + * these type of commands always come from the first sequence. + *

+ * + * @see SequencesComparator + * @see EditScript + * + * @since 4.0 + */ + public static class DeleteCommand extends EditCommand { + + /** + * Simple constructor. Creates a new instance of {@link DeleteCommand}. + * + * @param object the object of the first sequence that should be deleted + */ + public DeleteCommand(final T object) { + super(object); + } + + /** + * Accept a visitor. When a {@code DeleteCommand} accepts a visitor, it calls + * its {@link CommandVisitor#visitDeleteCommand visitDeleteCommand} method. + * + * @param visitor the visitor to be accepted + */ + @Override + public void accept(final CommandVisitor visitor) { + visitor.visitDeleteCommand(getObject()); + } + } + + /** + * This interface should be implemented by user object to walk + * through {@link EditScript EditScript} objects. + *

+ * Users should implement this interface in order to walk through + * the {@link EditScript EditScript} object created by the comparison + * of two sequences. This is a direct application of the visitor + * design pattern. The {@link EditScript#visit EditScript.visit} + * method takes an object implementing this interface as an argument, + * it will perform the loop over all commands in the script and the + * proper methods of the user class will be called as the commands are + * encountered. + *

+ *

+ * The implementation of the user visitor class will depend on the + * need. Here are two examples. + *

+ *

+ * The first example is a visitor that build the longest common + * subsequence: + *

+ *
+     * import org.apache.commons.collections4.comparators.sequence.CommandVisitor;
+     *
+     * import java.util.ArrayList;
+     *
+     * public class LongestCommonSubSequence implements CommandVisitor {
+     *
+     *   public LongestCommonSubSequence() {
+     *     a = new ArrayList();
+     *   }
+     *
+     *   public void visitInsertCommand(Object object) {
+     *   }
+     *
+     *   public void visitKeepCommand(Object object) {
+     *     a.add(object);
+     *   }
+     *
+     *   public void visitDeleteCommand(Object object) {
+     *   }
+     *
+     *   public Object[] getSubSequence() {
+     *     return a.toArray();
+     *   }
+     *
+     *   private ArrayList a;
+     *
+     * }
+     * 
+ *

+ * The second example is a visitor that shows the commands and the way + * they transform the first sequence into the second one: + *

+ *
+     * import org.apache.commons.collections4.comparators.sequence.CommandVisitor;
+     *
+     * import java.util.Arrays;
+     * import java.util.ArrayList;
+     * import java.util.Iterator;
+     *
+     * public class ShowVisitor implements CommandVisitor {
+     *
+     *   public ShowVisitor(Object[] sequence1) {
+     *     v = new ArrayList();
+     *     v.addAll(Arrays.asList(sequence1));
+     *     index = 0;
+     *   }
+     *
+     *   public void visitInsertCommand(Object object) {
+     *     v.insertElementAt(object, index++);
+     *     display("insert", object);
+     *   }
+     *
+     *   public void visitKeepCommand(Object object) {
+     *     ++index;
+     *     display("keep  ", object);
+     *   }
+     *
+     *   public void visitDeleteCommand(Object object) {
+     *     v.remove(index);
+     *     display("delete", object);
+     *   }
+     *
+     *   private void display(String commandName, Object object) {
+     *     System.out.println(commandName + " " + object + " ->" + this);
+     *   }
+     *
+     *   public String toString() {
+     *     StringBuffer buffer = new StringBuffer();
+     *     for (Iterator iter = v.iterator(); iter.hasNext();) {
+     *       buffer.append(' ').append(iter.next());
+     *     }
+     *     return buffer.toString();
+     *   }
+     *
+     *   private ArrayList v;
+     *   private int index;
+     *
+     * }
+     * 
+ * + * @since 4.0 + */ + public static interface CommandVisitor { + + /** + * Method called when an insert command is encountered. + * + * @param object object to insert (this object comes from the second sequence) + */ + void visitInsertCommand(T object); + + /** + * Method called when a keep command is encountered. + * + * @param object object to keep (this object comes from the first sequence) + */ + void visitKeepCommand(T object); + + /** + * Method called when a delete command is encountered. + * + * @param object object to delete (this object comes from the first sequence) + */ + void visitDeleteCommand(T object); + } } diff --git a/src/main/java/com/flipkart/zjsonpatch/JsonDiff.java b/src/main/java/com/flipkart/zjsonpatch/JsonDiff.java index effc963..6d4841c 100644 --- a/src/main/java/com/flipkart/zjsonpatch/JsonDiff.java +++ b/src/main/java/com/flipkart/zjsonpatch/JsonDiff.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; -import org.apache.commons.collections4.ListUtils; import java.util.ArrayList; import java.util.EnumSet; @@ -477,6 +476,6 @@ private void compareObjects(JsonPointer path, JsonNode source, JsonNode target) } private static List getLCS(final JsonNode first, final JsonNode second) { - return ListUtils.longestCommonSubsequence(InternalUtils.toList((ArrayNode) first), InternalUtils.toList((ArrayNode) second)); + return InternalUtils.longestCommonSubsequence(InternalUtils.toList((ArrayNode) first), InternalUtils.toList((ArrayNode) second)); } } diff --git a/src/main/java/com/flipkart/zjsonpatch/SequencesComparator.java b/src/main/java/com/flipkart/zjsonpatch/SequencesComparator.java new file mode 100644 index 0000000..142188b --- /dev/null +++ b/src/main/java/com/flipkart/zjsonpatch/SequencesComparator.java @@ -0,0 +1,360 @@ +/* + * Copyright 2021 flipkart.com zjsonpatch. + * + * 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 com.flipkart.zjsonpatch; + +import java.util.List; + +import com.flipkart.zjsonpatch.InternalUtils.DefaultEquator; +import com.flipkart.zjsonpatch.InternalUtils.Equator; +import com.flipkart.zjsonpatch.InternalUtils.KeepCommand; +import com.flipkart.zjsonpatch.InternalUtils.DeleteCommand; +import com.flipkart.zjsonpatch.InternalUtils.InsertCommand; + +/** + * This class allows to compare two objects sequences. + *

+ * The two sequences can hold any object type, as only the {@code equals} + * method is used to compare the elements of the sequences. It is guaranteed + * that the comparisons will always be done as {@code o1.equals(o2)} where + * {@code o1} belongs to the first sequence and {@code o2} belongs to + * the second sequence. This can be important if subclassing is used for some + * elements in the first sequence and the {@code equals} method is + * specialized. + *

+ *

+ * Comparison can be seen from two points of view: either as giving the smallest + * modification allowing to transform the first sequence into the second one, or + * as giving the longest sequence which is a subsequence of both initial + * sequences. The {@code equals} method is used to compare objects, so any + * object can be put into sequences. Modifications include deleting, inserting + * or keeping one object, starting from the beginning of the first sequence. + *

+ *

+ * This class implements the comparison algorithm, which is the very efficient + * algorithm from Eugene W. Myers + * + * An O(ND) Difference Algorithm and Its Variations. This algorithm produces + * the shortest possible + * {@link EditScript edit script} + * containing all the + * {@link EditCommand commands} + * needed to transform the first sequence into the second one. + *

+ *

+ * ATTRIBUTION NOTICE:
+ * This class contains source code copied from + * Apache commons-collection4 + * + *

+ * + * @see EditScript + * @see EditCommand + * @see CommandVisitor + * + * @since 4.0 + */ +public class SequencesComparator { + + /** First sequence. */ + private final List sequence1; + + /** Second sequence. */ + private final List sequence2; + + /** The equator used for testing object equality. */ + private final Equator equator; + + /** Temporary variables. */ + private final int[] vDown; + private final int[] vUp; + + /** + * Simple constructor. + *

+ * Creates a new instance of SequencesComparator using a {@link DefaultEquator}. + *

+ * It is guaranteed that the comparisons will always be done as + * {@code o1.equals(o2)} where {@code o1} belongs to the first + * sequence and {@code o2} belongs to the second sequence. This can be + * important if subclassing is used for some elements in the first sequence + * and the {@code equals} method is specialized. + * + * @param sequence1 first sequence to be compared + * @param sequence2 second sequence to be compared + */ + public SequencesComparator(final List sequence1, final List sequence2) { + this(sequence1, sequence2, DefaultEquator.defaultEquator()); + } + + /** + * Simple constructor. + *

+ * Creates a new instance of SequencesComparator with a custom {@link Equator}. + *

+ * It is guaranteed that the comparisons will always be done as + * {@code Equator.equate(o1, o2)} where {@code o1} belongs to the first + * sequence and {@code o2} belongs to the second sequence. + * + * @param sequence1 first sequence to be compared + * @param sequence2 second sequence to be compared + * @param equator the equator to use for testing object equality + */ + public SequencesComparator(final List sequence1, final List sequence2, final Equator equator) { + this.sequence1 = sequence1; + this.sequence2 = sequence2; + this.equator = equator; + + final int size = sequence1.size() + sequence2.size() + 2; + vDown = new int[size]; + vUp = new int[size]; + } + + /** + * Get the {@link EditScript} object. + *

+ * It is guaranteed that the objects embedded in the {@link InsertCommand + * insert commands} come from the second sequence and that the objects + * embedded in either the {@link DeleteCommand delete commands} or + * {@link KeepCommand keep commands} come from the first sequence. This can + * be important if subclassing is used for some elements in the first + * sequence and the {@code equals} method is specialized. + * + * @return the edit script resulting from the comparison of the two + * sequences + */ + public EditScript getScript() { + final EditScript script = new EditScript<>(); + buildScript(0, sequence1.size(), 0, sequence2.size(), script); + return script; + } + + /** + * Build a snake. + * + * @param start the value of the start of the snake + * @param diag the value of the diagonal of the snake + * @param end1 the value of the end of the first sequence to be compared + * @param end2 the value of the end of the second sequence to be compared + * @return the snake built + */ + private Snake buildSnake(final int start, final int diag, final int end1, final int end2) { + int end = start; + while (end - diag < end2 + && end < end1 + && equator.equate(sequence1.get(end), sequence2.get(end - diag))) { + ++end; + } + return new Snake(start, end, diag); + } + + /** + * Get the middle snake corresponding to two subsequences of the + * main sequences. + *

+ * The snake is found using the MYERS Algorithm (this algorithms has + * also been implemented in the GNU diff program). This algorithm is + * explained in Eugene Myers article: + * + * An O(ND) Difference Algorithm and Its Variations. + * + * @param start1 the begin of the first sequence to be compared + * @param end1 the end of the first sequence to be compared + * @param start2 the begin of the second sequence to be compared + * @param end2 the end of the second sequence to be compared + * @return the middle snake + */ + private Snake getMiddleSnake(final int start1, final int end1, final int start2, final int end2) { + // Myers Algorithm + // Initialisations + final int m = end1 - start1; + final int n = end2 - start2; + if (m == 0 || n == 0) { + return null; + } + + final int delta = m - n; + final int sum = n + m; + final int offset = (sum % 2 == 0 ? sum : sum + 1) / 2; + vDown[1+offset] = start1; + vUp[1+offset] = end1 + 1; + + for (int d = 0; d <= offset; ++d) { + // Down + for (int k = -d; k <= d; k += 2) { + // First step + + final int i = k + offset; + if (k == -d || k != d && vDown[i-1] < vDown[i+1]) { + vDown[i] = vDown[i+1]; + } else { + vDown[i] = vDown[i-1] + 1; + } + + int x = vDown[i]; + int y = x - start1 + start2 - k; + + while (x < end1 && y < end2 && equator.equate(sequence1.get(x), sequence2.get(y))) { + vDown[i] = ++x; + ++y; + } + // Second step + if (delta % 2 != 0 && delta - d <= k && k <= delta + d) { + if (vUp[i-delta] <= vDown[i]) { // NOPMD + return buildSnake(vUp[i-delta], k + start1 - start2, end1, end2); + } + } + } + + // Up + for (int k = delta - d; k <= delta + d; k += 2) { + // First step + final int i = k + offset - delta; + if (k == delta - d + || k != delta + d && vUp[i+1] <= vUp[i-1]) { + vUp[i] = vUp[i+1] - 1; + } else { + vUp[i] = vUp[i-1]; + } + + int x = vUp[i] - 1; + int y = x - start1 + start2 - k; + while (x >= start1 && y >= start2 + && equator.equate(sequence1.get(x), sequence2.get(y))) { + vUp[i] = x--; + y--; + } + // Second step + if (delta % 2 == 0 && -d <= k && k <= d ) { + if (vUp[i] <= vDown[i + delta]) { // NOPMD + return buildSnake(vUp[i], k + start1 - start2, end1, end2); + } + } + } + } + + // this should not happen + throw new RuntimeException("Internal Error"); + } + + + /** + * Build an edit script. + * + * @param start1 the begin of the first sequence to be compared + * @param end1 the end of the first sequence to be compared + * @param start2 the begin of the second sequence to be compared + * @param end2 the end of the second sequence to be compared + * @param script the edited script + */ + private void buildScript(final int start1, final int end1, final int start2, final int end2, + final EditScript script) { + + final Snake middle = getMiddleSnake(start1, end1, start2, end2); + + if (middle == null + || middle.getStart() == end1 && middle.getDiag() == end1 - end2 + || middle.getEnd() == start1 && middle.getDiag() == start1 - start2) { + + int i = start1; + int j = start2; + while (i < end1 || j < end2) { + if (i < end1 && j < end2 && equator.equate(sequence1.get(i), sequence2.get(j))) { + script.append(new KeepCommand<>(sequence1.get(i))); + ++i; + ++j; + } else { + if (end1 - start1 > end2 - start2) { + script.append(new DeleteCommand<>(sequence1.get(i))); + ++i; + } else { + script.append(new InsertCommand<>(sequence2.get(j))); + ++j; + } + } + } + + } else { + + buildScript(start1, middle.getStart(), + start2, middle.getStart() - middle.getDiag(), + script); + for (int i = middle.getStart(); i < middle.getEnd(); ++i) { + script.append(new KeepCommand<>(sequence1.get(i))); + } + buildScript(middle.getEnd(), end1, + middle.getEnd() - middle.getDiag(), end2, + script); + } + } + + /** + * This class is a simple placeholder to hold the end part of a path + * under construction in a {@link SequencesComparator SequencesComparator}. + */ + private class Snake { + + /** Start index. */ + private final int start; + + /** End index. */ + private final int end; + + /** Diagonal number. */ + private final int diag; + + /** + * Simple constructor. Creates a new instance of Snake with specified indices. + * + * @param start start index of the snake + * @param end end index of the snake + * @param diag diagonal number + */ + Snake(final int start, final int end, final int diag) { + this.start = start; + this.end = end; + this.diag = diag; + } + + /** + * Get the start index of the snake. + * + * @return start index of the snake + */ + public int getStart() { + return start; + } + + /** + * Get the end index of the snake. + * + * @return end index of the snake + */ + public int getEnd() { + return end; + } + + /** + * Get the diagonal number of the snake. + * + * @return diagonal number of the snake + */ + public int getDiag() { + return diag; + } + } + +} \ No newline at end of file diff --git a/src/test/java/com/flipkart/zjsonpatch/TestUtils.java b/src/test/java/com/flipkart/zjsonpatch/TestUtils.java index 049ab5c..b618d2a 100644 --- a/src/test/java/com/flipkart/zjsonpatch/TestUtils.java +++ b/src/test/java/com/flipkart/zjsonpatch/TestUtils.java @@ -1,14 +1,43 @@ +/* + * Copyright 2021 flipkart.com zjsonpatch. + * + * 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 com.flipkart.zjsonpatch; import com.fasterxml.jackson.databind.JsonNode; + import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.io.IOUtils; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import java.io.IOException; import java.io.InputStream; import java.util.Arrays; +import java.util.Collections; import java.util.List; +/** + *

+ * ATTRIBUTION NOTICE:
+ * This class contains source code copied from + * Apache commons-collection4 + * + *

+ */ public class TestUtils { public static final ObjectMapper DEFAULT_MAPPER = new ObjectMapper(); @@ -26,4 +55,44 @@ public static String loadFromResources(String path) throws IOException { InputStream resourceAsStream = PatchTestCase.class.getResourceAsStream(path); return IOUtils.toString(resourceAsStream, "UTF-8"); } + + public void testLongestCommonSubsequence() { + + try { + InternalUtils.longestCommonSubsequence((List) null, null); + fail("failed to check for null argument"); + } catch (final NullPointerException e) {} + + try { + InternalUtils.longestCommonSubsequence(Arrays.asList('A'), null); + fail("failed to check for null argument"); + } catch (final NullPointerException e) {} + + try { + InternalUtils.longestCommonSubsequence(null, Arrays.asList('A')); + fail("failed to check for null argument"); + } catch (final NullPointerException e) {} + + @SuppressWarnings("unchecked") + List lcs = InternalUtils.longestCommonSubsequence(Collections.EMPTY_LIST, Collections.EMPTY_LIST); + assertEquals(0, lcs.size()); + + final List list1 = Arrays.asList('B', 'A', 'N', 'A', 'N', 'A'); + final List list2 = Arrays.asList('A', 'N', 'A', 'N', 'A', 'S'); + lcs = InternalUtils.longestCommonSubsequence(list1, list2); + + List expected = Arrays.asList('A', 'N', 'A', 'N', 'A'); + assertEquals(expected, lcs); + + final List list3 = Arrays.asList('A', 'T', 'A', 'N', 'A'); + lcs = InternalUtils.longestCommonSubsequence(list1, list3); + + expected = Arrays.asList('A', 'A', 'N', 'A'); + assertEquals(expected, lcs); + + final List listZorro = Arrays.asList('Z', 'O', 'R', 'R', 'O'); + lcs = InternalUtils.longestCommonSubsequence(list1, listZorro); + + assertTrue(lcs.isEmpty()); + } }