From 97ec04515aaf8bf2dab2adb8bce160e6c9efedfb Mon Sep 17 00:00:00 2001 From: jerry <> Date: Fri, 26 Mar 2021 22:01:38 -0700 Subject: [PATCH 1/2] Removed apache commons dependency Just copy-pasta of the relevant parts from apache commons and added required license and attribution notices to keep it legal. This releases the 754KB dependency to entire apache commons collections lib for this one method (LCS) --- pom.xml | 5 - .../com/flipkart/zjsonpatch/EditScript.java | 147 +++++ .../flipkart/zjsonpatch/InternalUtils.java | 538 ++++++++++++++++-- .../com/flipkart/zjsonpatch/JsonDiff.java | 3 +- .../zjsonpatch/SequencesComparator.java | 360 ++++++++++++ 5 files changed, 1009 insertions(+), 44 deletions(-) create mode 100644 src/main/java/com/flipkart/zjsonpatch/EditScript.java create mode 100644 src/main/java/com/flipkart/zjsonpatch/SequencesComparator.java 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 From 154ff6253dc5fbb602c71beab46004100062d789 Mon Sep 17 00:00:00 2001 From: jerry <> Date: Fri, 26 Mar 2021 22:10:40 -0700 Subject: [PATCH 2/2] Update TestUtils.java --- .../com/flipkart/zjsonpatch/TestUtils.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) 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()); + } }