diff --git a/atf-application/pom.xml b/atf-application/pom.xml index db7f6086..e33c4b17 100644 --- a/atf-application/pom.xml +++ b/atf-application/pom.xml @@ -283,6 +283,53 @@ + + + org.codehaus.mojo + findbugs-maven-plugin + 3.0.5 + + Max + + medium + + true + FindReturnRef,RuntimeExceptionCapture + + + + analyze-compile + compile + + check + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/atf-application/src/main/java/ru/bsc/test/autotester/diff/Diff.java b/atf-application/src/main/java/ru/bsc/test/autotester/diff/Diff.java index 5579c353..b37df1ab 100644 --- a/atf-application/src/main/java/ru/bsc/test/autotester/diff/Diff.java +++ b/atf-application/src/main/java/ru/bsc/test/autotester/diff/Diff.java @@ -1,97 +1,99 @@ -/* - * Diff Match and Patch - * Copyright 2018 The diff-match-patch Authors. - * https://github.com/google/diff-match-patch - * - * 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 ru.bsc.test.autotester.diff; - -/** - * Class representing one diff operation. - */ -@SuppressWarnings("all") -public class Diff { - /** - * One of: INSERT, DELETE or EQUAL. - */ - public Operation operation; - /** - * The text associated with this diff operation. - */ - public String text; - - /** - * Constructor. Initializes the diff with the provided values. - * @param operation One of INSERT, DELETE or EQUAL. - * @param text The text being applied. - */ - public Diff(Operation operation, String text) { - // Construct a diff with the specified operation and text. - this.operation = operation; - this.text = text; - } - - /** - * Display a human-readable version of this Diff. - * @return text version. - */ - public String toString() { - String prettyText = this.text.replace('\n', '\u00b6'); - return "Diff(" + this.operation + ",\"" + prettyText + "\")"; - } - - /** - * Create a numeric hash value for a Diff. - * This function is not used by DMP. - * @return Hash value. - */ - @Override - public int hashCode() { - final int prime = 31; - int result = (operation == null) ? 0 : operation.hashCode(); - result += prime * ((text == null) ? 0 : text.hashCode()); - return result; - } - - /** - * Is this Diff equivalent to another Diff? - * @param obj Another Diff to compare against. - * @return true or false. - */ - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - Diff other = (Diff) obj; - if (operation != other.operation) { - return false; - } - if (text == null) { - if (other.text != null) { - return false; - } - } else if (!text.equals(other.text)) { - return false; - } - return true; - } -} +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * 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 ru.bsc.test.autotester.diff; + +import java.io.Serializable; + +/** + * Class representing one diff operation. + */ +@SuppressWarnings("all") +public class Diff implements Serializable { + /** + * One of: INSERT, DELETE or EQUAL. + */ + public Operation operation; + /** + * The text associated with this diff operation. + */ + public String text; + + /** + * Constructor. Initializes the diff with the provided values. + * @param operation One of INSERT, DELETE or EQUAL. + * @param text The text being applied. + */ + public Diff(Operation operation, String text) { + // Construct a diff with the specified operation and text. + this.operation = operation; + this.text = text; + } + + /** + * Display a human-readable version of this Diff. + * @return text version. + */ + public String toString() { + String prettyText = this.text.replace('\n', '\u00b6'); + return "Diff(" + this.operation + ",\"" + prettyText + "\")"; + } + + /** + * Create a numeric hash value for a Diff. + * This function is not used by DMP. + * @return Hash value. + */ + @Override + public int hashCode() { + final int prime = 31; + int result = (operation == null) ? 0 : operation.hashCode(); + result += prime * ((text == null) ? 0 : text.hashCode()); + return result; + } + + /** + * Is this Diff equivalent to another Diff? + * @param obj Another Diff to compare against. + * @return true or false. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Diff other = (Diff) obj; + if (operation != other.operation) { + return false; + } + if (text == null) { + if (other.text != null) { + return false; + } + } else if (!text.equals(other.text)) { + return false; + } + return true; + } +} diff --git a/atf-application/src/main/java/ru/bsc/test/autotester/diff/DiffMatchPatch.java b/atf-application/src/main/java/ru/bsc/test/autotester/diff/DiffMatchPatch.java index d09cd8de..1b009245 100644 --- a/atf-application/src/main/java/ru/bsc/test/autotester/diff/DiffMatchPatch.java +++ b/atf-application/src/main/java/ru/bsc/test/autotester/diff/DiffMatchPatch.java @@ -1,2313 +1,2315 @@ -/* - * Diff Match and Patch - * Copyright 2018 The diff-match-patch Authors. - * https://github.com/google/diff-match-patch - * - * 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 ru.bsc.test.autotester.diff; - -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/* - * Functions for diff, match and patch. - * Computes the difference between two texts to create a patch. - * Applies the patch onto another text, allowing for errors. - * - * @author fraser@google.com (Neil Fraser) - */ - -/** - * Class containing the diff, match and patch methods. - * Also contains the behaviour settings. - */ -@SuppressWarnings("all") -public class DiffMatchPatch { - - // Defaults. - // Set these on your diff_match_patch instance to override the defaults. - - /** - * Number of seconds to map a diff before giving up (0 for infinity). - */ - public float Diff_Timeout = 1.0f; - /** - * Cost of an empty edit operation in terms of edit characters. - */ - public short Diff_EditCost = 4; - /** - * At what point is no match declared (0.0 = perfection, 1.0 = very loose). - */ - public float Match_Threshold = 0.5f; - /** - * How far to search for a match (0 = exact location, 1000+ = broad match). - * A match this many characters away from the expected location will add - * 1.0 to the score (0.0 is a perfect match). - */ - public int Match_Distance = 1000; - /** - * When deleting a large block of text (over ~64 characters), how close do - * the contents have to be to match the expected contents. (0.0 = perfection, - * 1.0 = very loose). Note that Match_Threshold controls how closely the - * end points of a delete need to match. - */ - public float Patch_DeleteThreshold = 0.5f; - /** - * Chunk size for context length. - */ - public short Patch_Margin = 4; - - /** - * The number of bits in an int. - */ - private short Match_MaxBits = 32; - - - // DIFF FUNCTIONS - - - /** - * Find the differences between two texts. - * Run a faster, slightly less optimal diff. - * This method allows the 'checklines' of diffMain() to be optional. - * Most of the time checklines is wanted, so default to true. - * - * @param text1 Old string to be diffed. - * @param text2 New string to be diffed. - * @return Linked List of Diff objects. - */ - public LinkedList diffMain(String text1, String text2) { - return diffMain(text1, text2, true); - } - - /** - * Find the differences between two texts. - * - * @param text1 Old string to be diffed. - * @param text2 New string to be diffed. - * @param checklines Speedup flag. If false, then don't run a - * line-level diff first to identify the changed areas. - * If true, then run a faster slightly less optimal diff. - * @return Linked List of Diff objects. - */ - public LinkedList diffMain(String text1, String text2, - boolean checklines) { - // Set a deadline by which time the diff must be complete. - long deadline; - if (Diff_Timeout <= 0) { - deadline = Long.MAX_VALUE; - } else { - deadline = System.currentTimeMillis() + (long) (Diff_Timeout * 1000); - } - return diffMain(text1, text2, checklines, deadline); - } - - /** - * Find the differences between two texts. Simplifies the problem by - * stripping any common prefix or suffix off the texts before diffing. - * - * @param text1 Old string to be diffed. - * @param text2 New string to be diffed. - * @param checklines Speedup flag. If false, then don't run a - * line-level diff first to identify the changed areas. - * If true, then run a faster slightly less optimal diff. - * @param deadline Time when the diff should be complete by. Used - * internally for recursive calls. Users should set DiffTimeout instead. - * @return Linked List of Diff objects. - */ - private LinkedList diffMain(String text1, String text2, - boolean checklines, long deadline) { - // Check for null inputs. - if (text1 == null || text2 == null) { - throw new IllegalArgumentException("Null inputs. (diffMain)"); - } - - // Check for equality (speedup). - LinkedList diffs; - if (text1.equals(text2)) { - diffs = new LinkedList(); - if (text1.length() != 0) { - diffs.add(new Diff(Operation.EQUAL, text1)); - } - return diffs; - } - - // Trim off common prefix (speedup). - int commonlength = diffCommonPrefix(text1, text2); - String commonprefix = text1.substring(0, commonlength); - text1 = text1.substring(commonlength); - text2 = text2.substring(commonlength); - - // Trim off common suffix (speedup). - commonlength = diffCommonSuffix(text1, text2); - String commonsuffix = text1.substring(text1.length() - commonlength); - text1 = text1.substring(0, text1.length() - commonlength); - text2 = text2.substring(0, text2.length() - commonlength); - - // Compute the diff on the middle block. - diffs = diffCompute(text1, text2, checklines, deadline); - - // Restore the prefix and suffix. - if (commonprefix.length() != 0) { - diffs.addFirst(new Diff(Operation.EQUAL, commonprefix)); - } - if (commonsuffix.length() != 0) { - diffs.addLast(new Diff(Operation.EQUAL, commonsuffix)); - } - - diffCleanupMerge(diffs); - return diffs; - } - - /** - * Find the differences between two texts. Assumes that the texts do not - * have any common prefix or suffix. - * - * @param text1 Old string to be diffed. - * @param text2 New string to be diffed. - * @param checklines Speedup flag. If false, then don't run a - * line-level diff first to identify the changed areas. - * If true, then run a faster slightly less optimal diff. - * @param deadline Time when the diff should be complete by. - * @return Linked List of Diff objects. - */ - private LinkedList diffCompute(String text1, String text2, - boolean checklines, long deadline) { - LinkedList diffs = new LinkedList(); - - if (text1.length() == 0) { - // Just add some text (speedup). - diffs.add(new Diff(Operation.INSERT, text2)); - return diffs; - } - - if (text2.length() == 0) { - // Just delete some text (speedup). - diffs.add(new Diff(Operation.DELETE, text1)); - return diffs; - } - - String longtext = text1.length() > text2.length() ? text1 : text2; - String shorttext = text1.length() > text2.length() ? text2 : text1; - int i = longtext.indexOf(shorttext); - if (i != -1) { - // Shorter text is inside the longer text (speedup). - Operation op = (text1.length() > text2.length()) ? - Operation.DELETE : Operation.INSERT; - diffs.add(new Diff(op, longtext.substring(0, i))); - diffs.add(new Diff(Operation.EQUAL, shorttext)); - diffs.add(new Diff(op, longtext.substring(i + shorttext.length()))); - return diffs; - } - - if (shorttext.length() == 1) { - // Single character string. - // After the previous speedup, the character can't be an equality. - diffs.add(new Diff(Operation.DELETE, text1)); - diffs.add(new Diff(Operation.INSERT, text2)); - return diffs; - } - - // Check to see if the problem can be split in two. - String[] hm = diffHalfMatch(text1, text2); - if (hm != null) { - // A half-match was found, sort out the return data. - String text1_a = hm[0]; - String text1_b = hm[1]; - String text2_a = hm[2]; - String text2_b = hm[3]; - String mid_common = hm[4]; - // Send both pairs off for separate processing. - LinkedList diffs_a = diffMain(text1_a, text2_a, - checklines, deadline); - LinkedList diffs_b = diffMain(text1_b, text2_b, - checklines, deadline); - // Merge the results. - diffs = diffs_a; - diffs.add(new Diff(Operation.EQUAL, mid_common)); - diffs.addAll(diffs_b); - return diffs; - } - - if (checklines && text1.length() > 100 && text2.length() > 100) { - return diffLineMode(text1, text2, deadline); - } - - return diffBisect(text1, text2, deadline); - } - - /** - * Do a quick line-level diff on both strings, then rediff the parts for - * greater accuracy. - * This speedup can produce non-minimal diffs. - * - * @param text1 Old string to be diffed. - * @param text2 New string to be diffed. - * @param deadline Time when the diff should be complete by. - * @return Linked List of Diff objects. - */ - private LinkedList diffLineMode(String text1, String text2, - long deadline) { - // Scan the text on a line-by-line basis first. - LinesToCharsResult b = diffLinesToChars(text1, text2); - text1 = b.chars1; - text2 = b.chars2; - List linearray = b.lineArray; - - LinkedList diffs = diffMain(text1, text2, false, deadline); - - // Convert the diff back to original text. - diffCharsToLines(diffs, linearray); - // Eliminate freak matches (e.g. blank lines) - diffCleanupSemantic(diffs); - - // Rediff any replacement blocks, this time character-by-character. - // Add a dummy entry at the end. - diffs.add(new Diff(Operation.EQUAL, "")); - int count_delete = 0; - int count_insert = 0; - String text_delete = ""; - String text_insert = ""; - ListIterator pointer = diffs.listIterator(); - Diff thisDiff = pointer.next(); - while (thisDiff != null) { - switch (thisDiff.operation) { - case INSERT: - count_insert++; - text_insert += thisDiff.text; - break; - case DELETE: - count_delete++; - text_delete += thisDiff.text; - break; - case EQUAL: - // Upon reaching an equality, check for prior redundancies. - if (count_delete >= 1 && count_insert >= 1) { - // Delete the offending records and add the merged ones. - pointer.previous(); - for (int j = 0; j < count_delete + count_insert; j++) { - pointer.previous(); - pointer.remove(); - } - for (Diff newDiff : diffMain(text_delete, text_insert, false, - deadline)) { - pointer.add(newDiff); - } - } - count_insert = 0; - count_delete = 0; - text_delete = ""; - text_insert = ""; - break; - } - thisDiff = pointer.hasNext() ? pointer.next() : null; - } - diffs.removeLast(); // Remove the dummy entry at the end. - - return diffs; - } - - /** - * Find the 'middle snake' of a diff, split the problem in two - * and return the recursively constructed diff. - * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. - * - * @param text1 Old string to be diffed. - * @param text2 New string to be diffed. - * @param deadline Time at which to bail if not yet complete. - * @return LinkedList of Diff objects. - */ - protected LinkedList diffBisect(String text1, String text2, - long deadline) { - // Cache the text lengths to prevent multiple calls. - int text1_length = text1.length(); - int text2_length = text2.length(); - int max_d = (text1_length + text2_length + 1) / 2; - int v_offset = max_d; - int v_length = 2 * max_d; - int[] v1 = new int[v_length]; - int[] v2 = new int[v_length]; - for (int x = 0; x < v_length; x++) { - v1[x] = -1; - v2[x] = -1; - } - v1[v_offset + 1] = 0; - v2[v_offset + 1] = 0; - int delta = text1_length - text2_length; - // If the total number of characters is odd, then the front path will - // collide with the reverse path. - boolean front = (delta % 2 != 0); - // Offsets for start and end of k loop. - // Prevents mapping of space beyond the grid. - int k1start = 0; - int k1end = 0; - int k2start = 0; - int k2end = 0; - for (int d = 0; d < max_d; d++) { - // Bail out if deadline is reached. - if (System.currentTimeMillis() > deadline) { - break; - } - - // Walk the front path one step. - for (int k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { - int k1_offset = v_offset + k1; - int x1; - if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) { - x1 = v1[k1_offset + 1]; - } else { - x1 = v1[k1_offset - 1] + 1; - } - int y1 = x1 - k1; - while (x1 < text1_length && y1 < text2_length - && text1.charAt(x1) == text2.charAt(y1)) { - x1++; - y1++; - } - v1[k1_offset] = x1; - if (x1 > text1_length) { - // Ran off the right of the graph. - k1end += 2; - } else if (y1 > text2_length) { - // Ran off the bottom of the graph. - k1start += 2; - } else if (front) { - int k2_offset = v_offset + delta - k1; - if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { - // Mirror x2 onto top-left coordinate system. - int x2 = text1_length - v2[k2_offset]; - if (x1 >= x2) { - // Overlap detected. - return diffBisectSplit(text1, text2, x1, y1, deadline); - } - } - } - } - - // Walk the reverse path one step. - for (int k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { - int k2_offset = v_offset + k2; - int x2; - if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) { - x2 = v2[k2_offset + 1]; - } else { - x2 = v2[k2_offset - 1] + 1; - } - int y2 = x2 - k2; - while (x2 < text1_length && y2 < text2_length - && text1.charAt(text1_length - x2 - 1) - == text2.charAt(text2_length - y2 - 1)) { - x2++; - y2++; - } - v2[k2_offset] = x2; - if (x2 > text1_length) { - // Ran off the left of the graph. - k2end += 2; - } else if (y2 > text2_length) { - // Ran off the top of the graph. - k2start += 2; - } else if (!front) { - int k1_offset = v_offset + delta - k2; - if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { - int x1 = v1[k1_offset]; - int y1 = v_offset + x1 - k1_offset; - // Mirror x2 onto top-left coordinate system. - x2 = text1_length - x2; - if (x1 >= x2) { - // Overlap detected. - return diffBisectSplit(text1, text2, x1, y1, deadline); - } - } - } - } - } - // Diff took too long and hit the deadline or - // number of diffs equals number of characters, no commonality at all. - LinkedList diffs = new LinkedList(); - diffs.add(new Diff(Operation.DELETE, text1)); - diffs.add(new Diff(Operation.INSERT, text2)); - return diffs; - } - - /** - * Given the location of the 'middle snake', split the diff in two parts - * and recurse. - * - * @param text1 Old string to be diffed. - * @param text2 New string to be diffed. - * @param x Index of split point in text1. - * @param y Index of split point in text2. - * @param deadline Time at which to bail if not yet complete. - * @return LinkedList of Diff objects. - */ - private LinkedList diffBisectSplit(String text1, String text2, - int x, int y, long deadline) { - String text1a = text1.substring(0, x); - String text2a = text2.substring(0, y); - String text1b = text1.substring(x); - String text2b = text2.substring(y); - - // Compute both diffs serially. - LinkedList diffs = diffMain(text1a, text2a, false, deadline); - LinkedList diffsb = diffMain(text1b, text2b, false, deadline); - - diffs.addAll(diffsb); - return diffs; - } - - /** - * Split two texts into a list of strings. Reduce the texts to a string of - * hashes where each Unicode character represents one line. - * - * @param text1 First string. - * @param text2 Second string. - * @return An object containing the encoded text1, the encoded text2 and - * the List of unique strings. The zeroth element of the List of - * unique strings is intentionally blank. - */ - protected LinesToCharsResult diffLinesToChars(String text1, String text2) { - List lineArray = new ArrayList(); - Map lineHash = new HashMap(); - // e.g. linearray[4] == "Hello\n" - // e.g. linehash.get("Hello\n") == 4 - - // "\x00" is a valid character, but various debuggers don't like it. - // So we'll insert a junk entry to avoid generating a null character. - lineArray.add(""); - - String chars1 = diffLinesToCharsMunge(text1, lineArray, lineHash); - String chars2 = diffLinesToCharsMunge(text2, lineArray, lineHash); - return new LinesToCharsResult(chars1, chars2, lineArray); - } - - /** - * Split a text into a list of strings. Reduce the texts to a string of - * hashes where each Unicode character represents one line. - * - * @param text String to encode. - * @param lineArray List of unique strings. - * @param lineHash Map of strings to indices. - * @return Encoded string. - */ - private String diffLinesToCharsMunge(String text, List lineArray, - Map lineHash) { - int lineStart = 0; - int lineEnd = -1; - String line; - StringBuilder chars = new StringBuilder(); - // Walk the text, pulling out a substring for each line. - // text.split('\n') would would temporarily double our memory footprint. - // Modifying text would create many large strings to garbage collect. - while (lineEnd < text.length() - 1) { - lineEnd = text.indexOf('\n', lineStart); - if (lineEnd == -1) { - lineEnd = text.length() - 1; - } - line = text.substring(lineStart, lineEnd + 1); - lineStart = lineEnd + 1; - - if (lineHash.containsKey(line)) { - chars.append(String.valueOf((char) (int) lineHash.get(line))); - } else { - lineArray.add(line); - lineHash.put(line, lineArray.size() - 1); - chars.append(String.valueOf((char) (lineArray.size() - 1))); - } - } - return chars.toString(); - } - - /** - * Rehydrate the text in a diff from a string of line hashes to real lines of - * text. - * - * @param diffs LinkedList of Diff objects. - * @param lineArray List of unique strings. - */ - protected void diffCharsToLines(LinkedList diffs, - List lineArray) { - StringBuilder text; - for (Diff diff : diffs) { - text = new StringBuilder(); - for (int y = 0; y < diff.text.length(); y++) { - text.append(lineArray.get(diff.text.charAt(y))); - } - diff.text = text.toString(); - } - } - - /** - * Determine the common prefix of two strings - * - * @param text1 First string. - * @param text2 Second string. - * @return The number of characters common to the start of each string. - */ - public int diffCommonPrefix(String text1, String text2) { - // Performance analysis: http://neil.fraser.name/news/2007/10/09/ - int n = Math.min(text1.length(), text2.length()); - for (int i = 0; i < n; i++) { - if (text1.charAt(i) != text2.charAt(i)) { - return i; - } - } - return n; - } - - /** - * Determine the common suffix of two strings - * - * @param text1 First string. - * @param text2 Second string. - * @return The number of characters common to the end of each string. - */ - public int diffCommonSuffix(String text1, String text2) { - // Performance analysis: http://neil.fraser.name/news/2007/10/09/ - int text1_length = text1.length(); - int text2_length = text2.length(); - int n = Math.min(text1_length, text2_length); - for (int i = 1; i <= n; i++) { - if (text1.charAt(text1_length - i) != text2.charAt(text2_length - i)) { - return i - 1; - } - } - return n; - } - - /** - * Determine if the suffix of one string is the prefix of another. - * - * @param text1 First string. - * @param text2 Second string. - * @return The number of characters common to the end of the first - * string and the start of the second string. - */ - protected int diffCommonOverlap(String text1, String text2) { - // Cache the text lengths to prevent multiple calls. - int text1_length = text1.length(); - int text2_length = text2.length(); - // Eliminate the null case. - if (text1_length == 0 || text2_length == 0) { - return 0; - } - // Truncate the longer string. - if (text1_length > text2_length) { - text1 = text1.substring(text1_length - text2_length); - } else if (text1_length < text2_length) { - text2 = text2.substring(0, text1_length); - } - int text_length = Math.min(text1_length, text2_length); - // Quick check for the worst case. - if (text1.equals(text2)) { - return text_length; - } - - // Start by looking for a single character match - // and increase length until no match is found. - // Performance analysis: http://neil.fraser.name/news/2010/11/04/ - int best = 0; - int length = 1; - while (true) { - String pattern = text1.substring(text_length - length); - int found = text2.indexOf(pattern); - if (found == -1) { - return best; - } - length += found; - if (found == 0 || text1.substring(text_length - length).equals( - text2.substring(0, length))) { - best = length; - length++; - } - } - } - - /** - * Do the two texts share a substring which is at least half the length of - * the longer text? - * This speedup can produce non-minimal diffs. - * - * @param text1 First string. - * @param text2 Second string. - * @return Five element String array, containing the prefix of text1, the - * suffix of text1, the prefix of text2, the suffix of text2 and the - * common middle. Or null if there was no match. - */ - protected String[] diffHalfMatch(String text1, String text2) { - if (Diff_Timeout <= 0) { - // Don't risk returning a non-optimal diff if we have unlimited time. - return null; - } - String longtext = text1.length() > text2.length() ? text1 : text2; - String shorttext = text1.length() > text2.length() ? text2 : text1; - if (longtext.length() < 4 || shorttext.length() * 2 < longtext.length()) { - return null; // Pointless. - } - - // First check if the second quarter is the seed for a half-match. - String[] hm1 = diffHalfMatchI(longtext, shorttext, - (longtext.length() + 3) / 4); - // Check again based on the third quarter. - String[] hm2 = diffHalfMatchI(longtext, shorttext, - (longtext.length() + 1) / 2); - String[] hm; - if (hm1 == null && hm2 == null) { - return null; - } else if (hm2 == null) { - hm = hm1; - } else if (hm1 == null) { - hm = hm2; - } else { - // Both matched. Select the longest. - hm = hm1[4].length() > hm2[4].length() ? hm1 : hm2; - } - - // A half-match was found, sort out the return data. - if (text1.length() > text2.length()) { - return hm; - //return new String[]{hm[0], hm[1], hm[2], hm[3], hm[4]}; - } else { - return new String[]{hm[2], hm[3], hm[0], hm[1], hm[4]}; - } - } - - /** - * Does a substring of shorttext exist within longtext such that the - * substring is at least half the length of longtext? - * - * @param longtext Longer string. - * @param shorttext Shorter string. - * @param i Start index of quarter length substring within longtext. - * @return Five element String array, containing the prefix of longtext, the - * suffix of longtext, the prefix of shorttext, the suffix of shorttext - * and the common middle. Or null if there was no match. - */ - private String[] diffHalfMatchI(String longtext, String shorttext, int i) { - // Start with a 1/4 length substring at position i as a seed. - String seed = longtext.substring(i, i + longtext.length() / 4); - int j = -1; - String best_common = ""; - String best_longtext_a = "", best_longtext_b = ""; - String best_shorttext_a = "", best_shorttext_b = ""; - while ((j = shorttext.indexOf(seed, j + 1)) != -1) { - int prefixLength = diffCommonPrefix(longtext.substring(i), - shorttext.substring(j)); - int suffixLength = diffCommonSuffix(longtext.substring(0, i), - shorttext.substring(0, j)); - if (best_common.length() < suffixLength + prefixLength) { - best_common = shorttext.substring(j - suffixLength, j) - + shorttext.substring(j, j + prefixLength); - best_longtext_a = longtext.substring(0, i - suffixLength); - best_longtext_b = longtext.substring(i + prefixLength); - best_shorttext_a = shorttext.substring(0, j - suffixLength); - best_shorttext_b = shorttext.substring(j + prefixLength); - } - } - if (best_common.length() * 2 >= longtext.length()) { - return new String[]{best_longtext_a, best_longtext_b, - best_shorttext_a, best_shorttext_b, best_common}; - } else { - return null; - } - } - - /** - * Reduce the number of edits by eliminating semantically trivial equalities. - * - * @param diffs LinkedList of Diff objects. - */ - public void diffCleanupSemantic(LinkedList diffs) { - if (diffs.isEmpty()) { - return; - } - boolean changes = false; - Stack equalities = new Stack(); // Stack of qualities. - String lastequality = null; // Always equal to equalities.lastElement().text - ListIterator pointer = diffs.listIterator(); - // Number of characters that changed prior to the equality. - int length_insertions1 = 0; - int length_deletions1 = 0; - // Number of characters that changed after the equality. - int length_insertions2 = 0; - int length_deletions2 = 0; - Diff thisDiff = pointer.next(); - while (thisDiff != null) { - if (thisDiff.operation == Operation.EQUAL) { - // Equality found. - equalities.push(thisDiff); - length_insertions1 = length_insertions2; - length_deletions1 = length_deletions2; - length_insertions2 = 0; - length_deletions2 = 0; - lastequality = thisDiff.text; - } else { - // An insertion or deletion. - if (thisDiff.operation == Operation.INSERT) { - length_insertions2 += thisDiff.text.length(); - } else { - length_deletions2 += thisDiff.text.length(); - } - // Eliminate an equality that is smaller or equal to the edits on both - // sides of it. - if (lastequality != null && (lastequality.length() - <= Math.max(length_insertions1, length_deletions1)) - && (lastequality.length() - <= Math.max(length_insertions2, length_deletions2))) { - //System.out.println("Splitting: '" + lastequality + "'"); - // Walk back to offending equality. - while (thisDiff != equalities.lastElement()) { - thisDiff = pointer.previous(); - } - pointer.next(); - - // Replace equality with a delete. - pointer.set(new Diff(Operation.DELETE, lastequality)); - // Insert a corresponding an insert. - pointer.add(new Diff(Operation.INSERT, lastequality)); - - equalities.pop(); // Throw away the equality we just deleted. - if (!equalities.empty()) { - // Throw away the previous equality (it needs to be reevaluated). - equalities.pop(); - } - if (equalities.empty()) { - // There are no previous equalities, walk back to the start. - while (pointer.hasPrevious()) { - pointer.previous(); - } - } else { - // There is a safe equality we can fall back to. - thisDiff = equalities.lastElement(); - while (thisDiff != pointer.previous()) { - // Intentionally empty loop. - } - } - - length_insertions1 = 0; // Reset the counters. - length_insertions2 = 0; - length_deletions1 = 0; - length_deletions2 = 0; - lastequality = null; - changes = true; - } - } - thisDiff = pointer.hasNext() ? pointer.next() : null; - } - - // Normalize the diff. - if (changes) { - diffCleanupMerge(diffs); - } - diffCleanupSemanticLossless(diffs); - - // Find any overlaps between deletions and insertions. - // e.g: abcxxxxxxdef - // -> abcxxxdef - // e.g: xxxabcdefxxx - // -> defxxxabc - // Only extract an overlap if it is as big as the edit ahead or behind it. - pointer = diffs.listIterator(); - Diff prevDiff = null; - thisDiff = null; - if (pointer.hasNext()) { - prevDiff = pointer.next(); - if (pointer.hasNext()) { - thisDiff = pointer.next(); - } - } - while (thisDiff != null) { - if (prevDiff.operation == Operation.DELETE && - thisDiff.operation == Operation.INSERT) { - String deletion = prevDiff.text; - String insertion = thisDiff.text; - int overlap_length1 = this.diffCommonOverlap(deletion, insertion); - int overlap_length2 = this.diffCommonOverlap(insertion, deletion); - if (overlap_length1 >= overlap_length2) { - if (overlap_length1 >= deletion.length() / 2.0 || - overlap_length1 >= insertion.length() / 2.0) { - // Overlap found. Insert an equality and trim the surrounding edits. - pointer.previous(); - pointer.add(new Diff(Operation.EQUAL, - insertion.substring(0, overlap_length1))); - prevDiff.text = - deletion.substring(0, deletion.length() - overlap_length1); - thisDiff.text = insertion.substring(overlap_length1); - // pointer.add inserts the element before the cursor, so there is - // no need to step past the new element. - } - } else { - if (overlap_length2 >= deletion.length() / 2.0 || - overlap_length2 >= insertion.length() / 2.0) { - // Reverse overlap found. - // Insert an equality and swap and trim the surrounding edits. - pointer.previous(); - pointer.add(new Diff(Operation.EQUAL, - deletion.substring(0, overlap_length2))); - prevDiff.operation = Operation.INSERT; - prevDiff.text = - insertion.substring(0, insertion.length() - overlap_length2); - thisDiff.operation = Operation.DELETE; - thisDiff.text = deletion.substring(overlap_length2); - // pointer.add inserts the element before the cursor, so there is - // no need to step past the new element. - } - } - thisDiff = pointer.hasNext() ? pointer.next() : null; - } - prevDiff = thisDiff; - thisDiff = pointer.hasNext() ? pointer.next() : null; - } - } - - /** - * Look for single edits surrounded on both sides by equalities - * which can be shifted sideways to align the edit to a word boundary. - * e.g: The cat came. -> The cat came. - * - * @param diffs LinkedList of Diff objects. - */ - public void diffCleanupSemanticLossless(LinkedList diffs) { - String equality1, edit, equality2; - String commonString; - int commonOffset; - int score, bestScore; - String bestEquality1, bestEdit, bestEquality2; - // Create a new iterator at the start. - ListIterator pointer = diffs.listIterator(); - Diff prevDiff = pointer.hasNext() ? pointer.next() : null; - Diff thisDiff = pointer.hasNext() ? pointer.next() : null; - Diff nextDiff = pointer.hasNext() ? pointer.next() : null; - // Intentionally ignore the first and last element (don't need checking). - while (nextDiff != null) { - if (prevDiff.operation == Operation.EQUAL && - nextDiff.operation == Operation.EQUAL) { - // This is a single edit surrounded by equalities. - equality1 = prevDiff.text; - edit = thisDiff.text; - equality2 = nextDiff.text; - - // First, shift the edit as far left as possible. - commonOffset = diffCommonSuffix(equality1, edit); - if (commonOffset != 0) { - commonString = edit.substring(edit.length() - commonOffset); - equality1 = equality1.substring(0, equality1.length() - commonOffset); - edit = commonString + edit.substring(0, edit.length() - commonOffset); - equality2 = commonString + equality2; - } - - // Second, step character by character right, looking for the best fit. - bestEquality1 = equality1; - bestEdit = edit; - bestEquality2 = equality2; - bestScore = diffCleanupSemanticScore(equality1, edit) - + diffCleanupSemanticScore(edit, equality2); - while (edit.length() != 0 && equality2.length() != 0 - && edit.charAt(0) == equality2.charAt(0)) { - equality1 += edit.charAt(0); - edit = edit.substring(1) + equality2.charAt(0); - equality2 = equality2.substring(1); - score = diffCleanupSemanticScore(equality1, edit) - + diffCleanupSemanticScore(edit, equality2); - // The >= encourages trailing rather than leading whitespace on edits. - if (score >= bestScore) { - bestScore = score; - bestEquality1 = equality1; - bestEdit = edit; - bestEquality2 = equality2; - } - } - - if (!prevDiff.text.equals(bestEquality1)) { - // We have an improvement, save it back to the diff. - if (bestEquality1.length() != 0) { - prevDiff.text = bestEquality1; - } else { - pointer.previous(); // Walk past nextDiff. - pointer.previous(); // Walk past thisDiff. - pointer.previous(); // Walk past prevDiff. - pointer.remove(); // Delete prevDiff. - pointer.next(); // Walk past thisDiff. - pointer.next(); // Walk past nextDiff. - } - thisDiff.text = bestEdit; - if (bestEquality2.length() != 0) { - nextDiff.text = bestEquality2; - } else { - pointer.remove(); // Delete nextDiff. - nextDiff = thisDiff; - thisDiff = prevDiff; - } - } - } - prevDiff = thisDiff; - thisDiff = nextDiff; - nextDiff = pointer.hasNext() ? pointer.next() : null; - } - } - - /** - * Given two strings, compute a score representing whether the internal - * boundary falls on logical boundaries. - * Scores range from 6 (best) to 0 (worst). - * - * @param one First string. - * @param two Second string. - * @return The score. - */ - private int diffCleanupSemanticScore(String one, String two) { - if (one.length() == 0 || two.length() == 0) { - // Edges are the best. - return 6; - } - - // Each port of this function behaves slightly differently due to - // subtle differences in each language's definition of things like - // 'whitespace'. Since this function's purpose is largely cosmetic, - // the choice has been made to use each language's native features - // rather than force total conformity. - char char1 = one.charAt(one.length() - 1); - char char2 = two.charAt(0); - boolean nonAlphaNumeric1 = !Character.isLetterOrDigit(char1); - boolean nonAlphaNumeric2 = !Character.isLetterOrDigit(char2); - boolean whitespace1 = nonAlphaNumeric1 && Character.isWhitespace(char1); - boolean whitespace2 = nonAlphaNumeric2 && Character.isWhitespace(char2); - boolean lineBreak1 = whitespace1 - && Character.getType(char1) == Character.CONTROL; - boolean lineBreak2 = whitespace2 - && Character.getType(char2) == Character.CONTROL; - boolean blankLine1 = lineBreak1 && BLANKLINEEND.matcher(one).find(); - boolean blankLine2 = lineBreak2 && BLANKLINESTART.matcher(two).find(); - - if (blankLine1 || blankLine2) { - // Five points for blank lines. - return 5; - } else if (lineBreak1 || lineBreak2) { - // Four points for line breaks. - return 4; - } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { - // Three points for end of sentences. - return 3; - } else if (whitespace1 || whitespace2) { - // Two points for whitespace. - return 2; - } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { - // One point for non-alphanumeric. - return 1; - } - return 0; - } - - // Define some regex patterns for matching boundaries. - private Pattern BLANKLINEEND - = Pattern.compile("\\n\\r?\\n\\Z", Pattern.DOTALL); - private Pattern BLANKLINESTART - = Pattern.compile("\\A\\r?\\n\\r?\\n", Pattern.DOTALL); - - /** - * Reduce the number of edits by eliminating operationally trivial equalities. - * - * @param diffs LinkedList of Diff objects. - */ - public void diffCleanupEfficiency(LinkedList diffs) { - if (diffs.isEmpty()) { - return; - } - boolean changes = false; - Stack equalities = new Stack(); // Stack of equalities. - String lastequality = null; // Always equal to equalities.lastElement().text - ListIterator pointer = diffs.listIterator(); - // Is there an insertion operation before the last equality. - boolean pre_ins = false; - // Is there a deletion operation before the last equality. - boolean pre_del = false; - // Is there an insertion operation after the last equality. - boolean post_ins = false; - // Is there a deletion operation after the last equality. - boolean post_del = false; - Diff thisDiff = pointer.next(); - Diff safeDiff = thisDiff; // The last Diff that is known to be unsplitable. - while (thisDiff != null) { - if (thisDiff.operation == Operation.EQUAL) { - // Equality found. - if (thisDiff.text.length() < Diff_EditCost && (post_ins || post_del)) { - // Candidate found. - equalities.push(thisDiff); - pre_ins = post_ins; - pre_del = post_del; - lastequality = thisDiff.text; - } else { - // Not a candidate, and can never become one. - equalities.clear(); - lastequality = null; - safeDiff = thisDiff; - } - post_ins = post_del = false; - } else { - // An insertion or deletion. - if (thisDiff.operation == Operation.DELETE) { - post_del = true; - } else { - post_ins = true; - } - /* - * Five types to be split: - * ABXYCD - * AXCD - * ABXC - * AXCD - * ABXC - */ - if (lastequality != null - && ((pre_ins && pre_del && post_ins && post_del) - || ((lastequality.length() < Diff_EditCost / 2) - && ((pre_ins ? 1 : 0) + (pre_del ? 1 : 0) - + (post_ins ? 1 : 0) + (post_del ? 1 : 0)) == 3))) { - //System.out.println("Splitting: '" + lastequality + "'"); - // Walk back to offending equality. - while (thisDiff != equalities.lastElement()) { - thisDiff = pointer.previous(); - } - pointer.next(); - - // Replace equality with a delete. - pointer.set(new Diff(Operation.DELETE, lastequality)); - // Insert a corresponding an insert. - pointer.add(thisDiff = new Diff(Operation.INSERT, lastequality)); - - equalities.pop(); // Throw away the equality we just deleted. - lastequality = null; - if (pre_ins && pre_del) { - // No changes made which could affect previous entry, keep going. - post_ins = post_del = true; - equalities.clear(); - safeDiff = thisDiff; - } else { - if (!equalities.empty()) { - // Throw away the previous equality (it needs to be reevaluated). - equalities.pop(); - } - if (equalities.empty()) { - // There are no previous questionable equalities, - // walk back to the last known safe diff. - thisDiff = safeDiff; - } else { - // There is an equality we can fall back to. - thisDiff = equalities.lastElement(); - } - while (thisDiff != pointer.previous()) { - // Intentionally empty loop. - } - post_ins = post_del = false; - } - - changes = true; - } - } - thisDiff = pointer.hasNext() ? pointer.next() : null; - } - - if (changes) { - diffCleanupMerge(diffs); - } - } - - /** - * Reorder and merge like edit sections. Merge equalities. - * Any edit section can move as long as it doesn't cross an equality. - * - * @param diffs LinkedList of Diff objects. - */ - public void diffCleanupMerge(LinkedList diffs) { - diffs.add(new Diff(Operation.EQUAL, "")); // Add a dummy entry at the end. - ListIterator pointer = diffs.listIterator(); - int count_delete = 0; - int count_insert = 0; - String text_delete = ""; - String text_insert = ""; - Diff thisDiff = pointer.next(); - Diff prevEqual = null; - int commonlength; - while (thisDiff != null) { - switch (thisDiff.operation) { - case INSERT: - count_insert++; - text_insert += thisDiff.text; - prevEqual = null; - break; - case DELETE: - count_delete++; - text_delete += thisDiff.text; - prevEqual = null; - break; - case EQUAL: - if (count_delete + count_insert > 1) { - boolean both_types = count_delete != 0 && count_insert != 0; - // Delete the offending records. - pointer.previous(); // Reverse direction. - while (count_delete-- > 0) { - pointer.previous(); - pointer.remove(); - } - while (count_insert-- > 0) { - pointer.previous(); - pointer.remove(); - } - if (both_types) { - // Factor out any common prefixies. - commonlength = diffCommonPrefix(text_insert, text_delete); - if (commonlength != 0) { - if (pointer.hasPrevious()) { - thisDiff = pointer.previous(); - assert thisDiff.operation == Operation.EQUAL - : "Previous diff should have been an equality."; - thisDiff.text += text_insert.substring(0, commonlength); - pointer.next(); - } else { - pointer.add(new Diff(Operation.EQUAL, - text_insert.substring(0, commonlength))); - } - text_insert = text_insert.substring(commonlength); - text_delete = text_delete.substring(commonlength); - } - // Factor out any common suffixies. - commonlength = diffCommonSuffix(text_insert, text_delete); - if (commonlength != 0) { - thisDiff = pointer.next(); - thisDiff.text = text_insert.substring(text_insert.length() - - commonlength) + thisDiff.text; - text_insert = text_insert.substring(0, text_insert.length() - - commonlength); - text_delete = text_delete.substring(0, text_delete.length() - - commonlength); - pointer.previous(); - } - } - // Insert the merged records. - if (text_delete.length() != 0) { - pointer.add(new Diff(Operation.DELETE, text_delete)); - } - if (text_insert.length() != 0) { - pointer.add(new Diff(Operation.INSERT, text_insert)); - } - // Step forward to the equality. - thisDiff = pointer.hasNext() ? pointer.next() : null; - } else if (prevEqual != null) { - // Merge this equality with the previous one. - prevEqual.text += thisDiff.text; - pointer.remove(); - thisDiff = pointer.previous(); - pointer.next(); // Forward direction - } - count_insert = 0; - count_delete = 0; - text_delete = ""; - text_insert = ""; - prevEqual = thisDiff; - break; - } - thisDiff = pointer.hasNext() ? pointer.next() : null; - } - if (diffs.getLast().text.length() == 0) { - diffs.removeLast(); // Remove the dummy entry at the end. - } - - /* - * Second pass: look for single edits surrounded on both sides by equalities - * which can be shifted sideways to eliminate an equality. - * e.g: ABAC -> ABAC - */ - boolean changes = false; - // Create a new iterator at the start. - // (As opposed to walking the current one back.) - pointer = diffs.listIterator(); - Diff prevDiff = pointer.hasNext() ? pointer.next() : null; - thisDiff = pointer.hasNext() ? pointer.next() : null; - Diff nextDiff = pointer.hasNext() ? pointer.next() : null; - // Intentionally ignore the first and last element (don't need checking). - while (nextDiff != null) { - if (prevDiff.operation == Operation.EQUAL && - nextDiff.operation == Operation.EQUAL) { - // This is a single edit surrounded by equalities. - if (thisDiff.text.endsWith(prevDiff.text)) { - // Shift the edit over the previous equality. - thisDiff.text = prevDiff.text - + thisDiff.text.substring(0, thisDiff.text.length() - - prevDiff.text.length()); - nextDiff.text = prevDiff.text + nextDiff.text; - pointer.previous(); // Walk past nextDiff. - pointer.previous(); // Walk past thisDiff. - pointer.previous(); // Walk past prevDiff. - pointer.remove(); // Delete prevDiff. - pointer.next(); // Walk past thisDiff. - thisDiff = pointer.next(); // Walk past nextDiff. - nextDiff = pointer.hasNext() ? pointer.next() : null; - changes = true; - } else if (thisDiff.text.startsWith(nextDiff.text)) { - // Shift the edit over the next equality. - prevDiff.text += nextDiff.text; - thisDiff.text = thisDiff.text.substring(nextDiff.text.length()) - + nextDiff.text; - pointer.remove(); // Delete nextDiff. - nextDiff = pointer.hasNext() ? pointer.next() : null; - changes = true; - } - } - prevDiff = thisDiff; - thisDiff = nextDiff; - nextDiff = pointer.hasNext() ? pointer.next() : null; - } - // If shifts were made, the diff needs reordering and another shift sweep. - if (changes) { - diffCleanupMerge(diffs); - } - } - - /** - * loc is a location in text1, compute and return the equivalent location in - * text2. - * e.g. "The cat" vs "The big cat", 1->1, 5->8 - * - * @param diffs LinkedList of Diff objects. - * @param loc Location within text1. - * @return Location within text2. - */ - public int diffXIndex(LinkedList diffs, int loc) { - int chars1 = 0; - int chars2 = 0; - int last_chars1 = 0; - int last_chars2 = 0; - Diff lastDiff = null; - for (Diff aDiff : diffs) { - if (aDiff.operation != Operation.INSERT) { - // Equality or deletion. - chars1 += aDiff.text.length(); - } - if (aDiff.operation != Operation.DELETE) { - // Equality or insertion. - chars2 += aDiff.text.length(); - } - if (chars1 > loc) { - // Overshot the location. - lastDiff = aDiff; - break; - } - last_chars1 = chars1; - last_chars2 = chars2; - } - if (lastDiff != null && lastDiff.operation == Operation.DELETE) { - // The location was deleted. - return last_chars2; - } - // Add the remaining character length. - return last_chars2 + (loc - last_chars1); - } - - /** - * Convert a Diff list into a pretty HTML report. - * - * @param diffs LinkedList of Diff objects. - * @return HTML representation. - */ - public String diffPrettyHtml(LinkedList diffs) { - StringBuilder html = new StringBuilder(); - for (Diff aDiff : diffs) { - String text = aDiff.text.replace("&", "&").replace("<", "<") - .replace(">", ">").replace("\n", "¶
"); - switch (aDiff.operation) { - case INSERT: - html.append("").append(text) - .append(""); - break; - case DELETE: - html.append("").append(text) - .append(""); - break; - case EQUAL: - html.append("").append(text).append(""); - break; - } - } - return html.toString(); - } - - /** - * Compute and return the source text (all equalities and deletions). - * - * @param diffs LinkedList of Diff objects. - * @return Source text. - */ - public String diffText1(LinkedList diffs) { - StringBuilder text = new StringBuilder(); - for (Diff aDiff : diffs) { - if (aDiff.operation != Operation.INSERT) { - text.append(aDiff.text); - } - } - return text.toString(); - } - - /** - * Compute and return the destination text (all equalities and insertions). - * - * @param diffs LinkedList of Diff objects. - * @return Destination text. - */ - public String diffText2(LinkedList diffs) { - StringBuilder text = new StringBuilder(); - for (Diff aDiff : diffs) { - if (aDiff.operation != Operation.DELETE) { - text.append(aDiff.text); - } - } - return text.toString(); - } - - /** - * Compute the Levenshtein distance; the number of inserted, deleted or - * substituted characters. - * - * @param diffs LinkedList of Diff objects. - * @return Number of changes. - */ - public int diffLevenshtein(LinkedList diffs) { - int levenshtein = 0; - int insertions = 0; - int deletions = 0; - for (Diff aDiff : diffs) { - switch (aDiff.operation) { - case INSERT: - insertions += aDiff.text.length(); - break; - case DELETE: - deletions += aDiff.text.length(); - break; - case EQUAL: - // A deletion and an insertion is one substitution. - levenshtein += Math.max(insertions, deletions); - insertions = 0; - deletions = 0; - break; - } - } - levenshtein += Math.max(insertions, deletions); - return levenshtein; - } - - /** - * Crush the diff into an encoded string which describes the operations - * required to transform text1 into text2. - * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. - * Operations are tab-separated. Inserted text is escaped using %xx notation. - * - * @param diffs Array of Diff objects. - * @return Delta text. - */ - public String diffToDelta(LinkedList diffs) { - StringBuilder text = new StringBuilder(); - for (Diff aDiff : diffs) { - switch (aDiff.operation) { - case INSERT: - try { - text.append("+").append(URLEncoder.encode(aDiff.text, "UTF-8") - .replace('+', ' ')).append("\t"); - } catch (UnsupportedEncodingException e) { - // Not likely on modern system. - throw new Error("This system does not support UTF-8.", e); - } - break; - case DELETE: - text.append("-").append(aDiff.text.length()).append("\t"); - break; - case EQUAL: - text.append("=").append(aDiff.text.length()).append("\t"); - break; - } - } - String delta = text.toString(); - if (delta.length() != 0) { - // Strip off trailing tab character. - delta = delta.substring(0, delta.length() - 1); - delta = new Patch().unescapeForEncodeUriCompatability(delta); - } - return delta; - } - - /** - * Given the original text1, and an encoded string which describes the - * operations required to transform text1 into text2, compute the full diff. - * - * @param text1 Source string for the diff. - * @param delta Delta text. - * @return Array of Diff objects or null if invalid. - * @throws IllegalArgumentException If invalid input. - */ - public LinkedList diffFromDelta(String text1, String delta) - throws IllegalArgumentException { - LinkedList diffs = new LinkedList(); - int pointer = 0; // Cursor in text1 - String[] tokens = delta.split("\t"); - for (String token : tokens) { - if (token.length() == 0) { - // Blank tokens are ok (from a trailing \t). - continue; - } - // Each token begins with a one character parameter which specifies the - // operation of this token (delete, insert, equality). - String param = token.substring(1); - switch (token.charAt(0)) { - case '+': - // decode would change all "+" to " " - param = param.replace("+", "%2B"); - try { - param = URLDecoder.decode(param, "UTF-8"); - } catch (UnsupportedEncodingException e) { - // Not likely on modern system. - throw new Error("This system does not support UTF-8.", e); - } catch (IllegalArgumentException e) { - // Malformed URI sequence. - throw new IllegalArgumentException( - "Illegal escape in diffFromDelta: " + param, e); - } - diffs.add(new Diff(Operation.INSERT, param)); - break; - case '-': - // Fall through. - case '=': - int n; - try { - n = Integer.parseInt(param); - } catch (NumberFormatException e) { - throw new IllegalArgumentException( - "Invalid number in diffFromDelta: " + param, e); - } - if (n < 0) { - throw new IllegalArgumentException( - "Negative number in diffFromDelta: " + param); - } - String text; - try { - text = text1.substring(pointer, pointer += n); - } catch (StringIndexOutOfBoundsException e) { - throw new IllegalArgumentException("Delta length (" + pointer - + ") larger than source text length (" + text1.length() - + ").", e); - } - if (token.charAt(0) == '=') { - diffs.add(new Diff(Operation.EQUAL, text)); - } else { - diffs.add(new Diff(Operation.DELETE, text)); - } - break; - default: - // Anything else is an error. - throw new IllegalArgumentException( - "Invalid diff operation in diffFromDelta: " + token.charAt(0)); - } - } - if (pointer != text1.length()) { - throw new IllegalArgumentException("Delta length (" + pointer - + ") smaller than source text length (" + text1.length() + ")."); - } - return diffs; - } - - - // MATCH FUNCTIONS - - - /** - * Locate the best instance of 'pattern' in 'text' near 'loc'. - * Returns -1 if no match found. - * - * @param text The text to search. - * @param pattern The pattern to search for. - * @param loc The location to search around. - * @return Best match index or -1. - */ - public int matchMain(String text, String pattern, int loc) { - // Check for null inputs. - if (text == null || pattern == null) { - throw new IllegalArgumentException("Null inputs. (matchMain)"); - } - - loc = Math.max(0, Math.min(loc, text.length())); - if (text.equals(pattern)) { - // Shortcut (potentially not guaranteed by the algorithm) - return 0; - } else if (text.length() == 0) { - // Nothing to match. - return -1; - } else if (loc + pattern.length() <= text.length() - && text.substring(loc, loc + pattern.length()).equals(pattern)) { - // Perfect match at the perfect spot! (Includes case of null pattern) - return loc; - } else { - // Do a fuzzy compare. - return matchBitap(text, pattern, loc); - } - } - - /** - * Locate the best instance of 'pattern' in 'text' near 'loc' using the - * Bitap algorithm. Returns -1 if no match found. - * - * @param text The text to search. - * @param pattern The pattern to search for. - * @param loc The location to search around. - * @return Best match index or -1. - */ - protected int matchBitap(String text, String pattern, int loc) { - assert (Match_MaxBits == 0 || pattern.length() <= Match_MaxBits) - : "Pattern too long for this application."; - - // Initialise the alphabet. - Map s = matchAlphabet(pattern); - - // Highest score beyond which we give up. - double score_threshold = Match_Threshold; - // Is there a nearby exact match? (speedup) - int best_loc = text.indexOf(pattern, loc); - if (best_loc != -1) { - score_threshold = Math.min(matchBitapScore(0, best_loc, loc, pattern), - score_threshold); - // What about in the other direction? (speedup) - best_loc = text.lastIndexOf(pattern, loc + pattern.length()); - if (best_loc != -1) { - score_threshold = Math.min(matchBitapScore(0, best_loc, loc, pattern), - score_threshold); - } - } - - // Initialise the bit arrays. - int matchmask = 1 << (pattern.length() - 1); - best_loc = -1; - - int bin_min, bin_mid; - int bin_max = pattern.length() + text.length(); - // Empty initialization added to appease Java compiler. - int[] last_rd = new int[0]; - for (int d = 0; d < pattern.length(); d++) { - // Scan for the best match; each iteration allows for one more error. - // Run a binary search to determine how far from 'loc' we can stray at - // this error level. - bin_min = 0; - bin_mid = bin_max; - while (bin_min < bin_mid) { - if (matchBitapScore(d, loc + bin_mid, loc, pattern) - <= score_threshold) { - bin_min = bin_mid; - } else { - bin_max = bin_mid; - } - bin_mid = (bin_max - bin_min) / 2 + bin_min; - } - // Use the result from this iteration as the maximum for the next. - bin_max = bin_mid; - int start = Math.max(1, loc - bin_mid + 1); - int finish = Math.min(loc + bin_mid, text.length()) + pattern.length(); - - int[] rd = new int[finish + 2]; - rd[finish + 1] = (1 << d) - 1; - for (int j = finish; j >= start; j--) { - int charMatch; - if (text.length() <= j - 1 || !s.containsKey(text.charAt(j - 1))) { - // Out of range. - charMatch = 0; - } else { - charMatch = s.get(text.charAt(j - 1)); - } - if (d == 0) { - // First pass: exact match. - rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; - } else { - // Subsequent passes: fuzzy match. - rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) - | (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1]; - } - if ((rd[j] & matchmask) != 0) { - double score = matchBitapScore(d, j - 1, loc, pattern); - // This match will almost certainly be better than any existing - // match. But check anyway. - if (score <= score_threshold) { - // Told you so. - score_threshold = score; - best_loc = j - 1; - if (best_loc > loc) { - // When passing loc, don't exceed our current distance from loc. - start = Math.max(1, 2 * loc - best_loc); - } else { - // Already passed loc, downhill from here on in. - break; - } - } - } - } - if (matchBitapScore(d + 1, loc, loc, pattern) > score_threshold) { - // No hope for a (better) match at greater error levels. - break; - } - last_rd = rd; - } - return best_loc; - } - - /** - * Compute and return the score for a match with e errors and x location. - * - * @param e Number of errors in match. - * @param x Location of match. - * @param loc Expected location of match. - * @param pattern Pattern being sought. - * @return Overall score for match (0.0 = good, 1.0 = bad). - */ - private double matchBitapScore(int e, int x, int loc, String pattern) { - float accuracy = (float) e / pattern.length(); - int proximity = Math.abs(loc - x); - if (Match_Distance == 0) { - // Dodge divide by zero error. - return proximity == 0 ? accuracy : 1.0; - } - return accuracy + (proximity / (float) Match_Distance); - } - - /** - * Initialise the alphabet for the Bitap algorithm. - * - * @param pattern The text to encode. - * @return Hash of character locations. - */ - protected Map matchAlphabet(String pattern) { - Map s = new HashMap(); - char[] char_pattern = pattern.toCharArray(); - for (char c : char_pattern) { - s.put(c, 0); - } - int i = 0; - for (char c : char_pattern) { - s.put(c, s.get(c) | (1 << (pattern.length() - i - 1))); - i++; - } - return s; - } - - - // PATCH FUNCTIONS - - - /** - * Increase the context until it is unique, - * but don't let the pattern expand beyond Match_MaxBits. - * - * @param patch The patch to grow. - * @param text Source text. - */ - protected void patch_addContext(Patch patch, String text) { - if (text.length() == 0) { - return; - } - String pattern = text.substring(patch.start2, patch.start2 + patch.length1); - int padding = 0; - - // Look for the first and last matches of pattern in text. If two different - // matches are found, increase the pattern length. - while (text.indexOf(pattern) != text.lastIndexOf(pattern) - && pattern.length() < Match_MaxBits - Patch_Margin - Patch_Margin) { - padding += Patch_Margin; - pattern = text.substring(Math.max(0, patch.start2 - padding), - Math.min(text.length(), patch.start2 + patch.length1 + padding)); - } - // Add one chunk for good luck. - padding += Patch_Margin; - - // Add the prefix. - String prefix = text.substring(Math.max(0, patch.start2 - padding), - patch.start2); - if (prefix.length() != 0) { - patch.diffs.addFirst(new Diff(Operation.EQUAL, prefix)); - } - // Add the suffix. - String suffix = text.substring(patch.start2 + patch.length1, - Math.min(text.length(), patch.start2 + patch.length1 + padding)); - if (suffix.length() != 0) { - patch.diffs.addLast(new Diff(Operation.EQUAL, suffix)); - } - - // Roll back the start points. - patch.start1 -= prefix.length(); - patch.start2 -= prefix.length(); - // Extend the lengths. - patch.length1 += prefix.length() + suffix.length(); - patch.length2 += prefix.length() + suffix.length(); - } - - /** - * Compute a list of patches to turn text1 into text2. - * A set of diffs will be computed. - * - * @param text1 Old text. - * @param text2 New text. - * @return LinkedList of Patch objects. - */ - public LinkedList patchMake(String text1, String text2) { - if (text1 == null || text2 == null) { - throw new IllegalArgumentException("Null inputs. (patchMake)"); - } - // No diffs provided, compute our own. - LinkedList diffs = diffMain(text1, text2, true); - if (diffs.size() > 2) { - diffCleanupSemantic(diffs); - diffCleanupEfficiency(diffs); - } - return patchMake(text1, diffs); - } - - /** - * Compute a list of patches to turn text1 into text2. - * text1 will be derived from the provided diffs. - * - * @param diffs Array of Diff objects for text1 to text2. - * @return LinkedList of Patch objects. - */ - public LinkedList patchMake(LinkedList diffs) { - if (diffs == null) { - throw new IllegalArgumentException("Null inputs. (patchMake)"); - } - // No origin string provided, compute our own. - String text1 = diffText1(diffs); - return patchMake(text1, diffs); - } - - /** - * Compute a list of patches to turn text1 into text2. - * text2 is ignored, diffs are the delta between text1 and text2. - * - * @param text1 Old text - * @param text2 Ignored. - * @param diffs Array of Diff objects for text1 to text2. - * @return LinkedList of Patch objects. - * @deprecated Prefer patchMake(String text1, LinkedList diffs). - */ - public LinkedList patchMake(String text1, String text2, - LinkedList diffs) { - return patchMake(text1, diffs); - } - - /** - * Compute a list of patches to turn text1 into text2. - * text2 is not provided, diffs are the delta between text1 and text2. - * - * @param text1 Old text. - * @param diffs Array of Diff objects for text1 to text2. - * @return LinkedList of Patch objects. - */ - public LinkedList patchMake(String text1, LinkedList diffs) { - if (text1 == null || diffs == null) { - throw new IllegalArgumentException("Null inputs. (patchMake)"); - } - - LinkedList patches = new LinkedList(); - if (diffs.isEmpty()) { - return patches; // Get rid of the null case. - } - Patch patch = new Patch(); - int char_count1 = 0; // Number of characters into the text1 string. - int char_count2 = 0; // Number of characters into the text2 string. - // Start with text1 (prepatch_text) and apply the diffs until we arrive at - // text2 (postpatch_text). We recreate the patches one by one to determine - // context info. - String prepatch_text = text1; - String postpatch_text = text1; - for (Diff aDiff : diffs) { - if (patch.diffs.isEmpty() && aDiff.operation != Operation.EQUAL) { - // A new patch starts here. - patch.start1 = char_count1; - patch.start2 = char_count2; - } - - switch (aDiff.operation) { - case INSERT: - patch.diffs.add(aDiff); - patch.length2 += aDiff.text.length(); - postpatch_text = postpatch_text.substring(0, char_count2) - + aDiff.text + postpatch_text.substring(char_count2); - break; - case DELETE: - patch.length1 += aDiff.text.length(); - patch.diffs.add(aDiff); - postpatch_text = postpatch_text.substring(0, char_count2) - + postpatch_text.substring(char_count2 + aDiff.text.length()); - break; - case EQUAL: - if (aDiff.text.length() <= 2 * Patch_Margin - && !patch.diffs.isEmpty() && aDiff != diffs.getLast()) { - // Small equality inside a patch. - patch.diffs.add(aDiff); - patch.length1 += aDiff.text.length(); - patch.length2 += aDiff.text.length(); - } - - if (aDiff.text.length() >= 2 * Patch_Margin) { - // Time for a new patch. - if (!patch.diffs.isEmpty()) { - patch_addContext(patch, prepatch_text); - patches.add(patch); - patch = new Patch(); - // Unlike Unidiff, our patch lists have a rolling context. - // http://code.google.com/p/google-diff-match-patch/wiki/Unidiff - // Update prepatch text & pos to reflect the application of the - // just completed patch. - prepatch_text = postpatch_text; - char_count1 = char_count2; - } - } - break; - } - - // Update the current character count. - if (aDiff.operation != Operation.INSERT) { - char_count1 += aDiff.text.length(); - } - if (aDiff.operation != Operation.DELETE) { - char_count2 += aDiff.text.length(); - } - } - // Pick up the leftover patch if not empty. - if (!patch.diffs.isEmpty()) { - patch_addContext(patch, prepatch_text); - patches.add(patch); - } - - return patches; - } - - /** - * Given an array of patches, return another array that is identical. - * - * @param patches Array of Patch objects. - * @return Array of Patch objects. - */ - public LinkedList patchDeepCopy(LinkedList patches) { - LinkedList patchesCopy = new LinkedList(); - for (Patch aPatch : patches) { - Patch patchCopy = new Patch(); - for (Diff aDiff : aPatch.diffs) { - Diff diffCopy = new Diff(aDiff.operation, aDiff.text); - patchCopy.diffs.add(diffCopy); - } - patchCopy.start1 = aPatch.start1; - patchCopy.start2 = aPatch.start2; - patchCopy.length1 = aPatch.length1; - patchCopy.length2 = aPatch.length2; - patchesCopy.add(patchCopy); - } - return patchesCopy; - } - - /** - * Merge a set of patches onto the text. Return a patched text, as well - * as an array of true/false values indicating which patches were applied. - * - * @param patches Array of Patch objects - * @param text Old text. - * @return Two element Object array, containing the new text and an array of - * boolean values. - */ - public Object[] patchApply(LinkedList patches, String text) { - if (patches.isEmpty()) { - return new Object[]{text, new boolean[0]}; - } - - // Deep copy the patches so that no changes are made to originals. - patches = patchDeepCopy(patches); - - String nullPadding = patchAddPadding(patches); - text = nullPadding + text + nullPadding; - patchSplitMax(patches); - - int x = 0; - // delta keeps track of the offset between the expected and actual location - // of the previous patch. If there are patches expected at positions 10 and - // 20, but the first patch was found at 12, delta is 2 and the second patch - // has an effective expected position of 22. - int delta = 0; - boolean[] results = new boolean[patches.size()]; - for (Patch aPatch : patches) { - int expected_loc = aPatch.start2 + delta; - String text1 = diffText1(aPatch.diffs); - int start_loc; - int end_loc = -1; - if (text1.length() > this.Match_MaxBits) { - // patchSplitMax will only provide an oversized pattern in the case of - // a monster delete. - start_loc = matchMain(text, - text1.substring(0, this.Match_MaxBits), expected_loc); - if (start_loc != -1) { - end_loc = matchMain(text, - text1.substring(text1.length() - this.Match_MaxBits), - expected_loc + text1.length() - this.Match_MaxBits); - if (end_loc == -1 || start_loc >= end_loc) { - // Can't find valid trailing context. Drop this patch. - start_loc = -1; - } - } - } else { - start_loc = matchMain(text, text1, expected_loc); - } - if (start_loc == -1) { - // No match found. :( - results[x] = false; - // Subtract the delta for this failed patch from subsequent patches. - delta -= aPatch.length2 - aPatch.length1; - } else { - // Found a match. :) - results[x] = true; - delta = start_loc - expected_loc; - String text2; - if (end_loc == -1) { - text2 = text.substring(start_loc, - Math.min(start_loc + text1.length(), text.length())); - } else { - text2 = text.substring(start_loc, - Math.min(end_loc + this.Match_MaxBits, text.length())); - } - if (text1.equals(text2)) { - // Perfect match, just shove the replacement text in. - text = text.substring(0, start_loc) + diffText2(aPatch.diffs) - + text.substring(start_loc + text1.length()); - } else { - // Imperfect match. Run a diff to get a framework of equivalent - // indices. - LinkedList diffs = diffMain(text1, text2, false); - if (text1.length() > this.Match_MaxBits - && diffLevenshtein(diffs) / (float) text1.length() - > this.Patch_DeleteThreshold) { - // The end points match, but the content is unacceptably bad. - results[x] = false; - } else { - diffCleanupSemanticLossless(diffs); - int index1 = 0; - for (Diff aDiff : aPatch.diffs) { - if (aDiff.operation != Operation.EQUAL) { - int index2 = diffXIndex(diffs, index1); - if (aDiff.operation == Operation.INSERT) { - // Insertion - text = text.substring(0, start_loc + index2) + aDiff.text - + text.substring(start_loc + index2); - } else if (aDiff.operation == Operation.DELETE) { - // Deletion - text = text.substring(0, start_loc + index2) - + text.substring(start_loc + diffXIndex(diffs, - index1 + aDiff.text.length())); - } - } - if (aDiff.operation != Operation.DELETE) { - index1 += aDiff.text.length(); - } - } - } - } - } - x++; - } - // Strip the padding off. - text = text.substring(nullPadding.length(), text.length() - - nullPadding.length()); - return new Object[]{text, results}; - } - - /** - * Add some padding on text start and end so that edges can match something. - * Intended to be called only from within patchApply. - * - * @param patches Array of Patch objects. - * @return The padding string added to each side. - */ - public String patchAddPadding(LinkedList patches) { - short paddingLength = this.Patch_Margin; - String nullPadding = ""; - for (short x = 1; x <= paddingLength; x++) { - nullPadding += String.valueOf((char) x); - } - - // Bump all the patches forward. - for (Patch aPatch : patches) { - aPatch.start1 += paddingLength; - aPatch.start2 += paddingLength; - } - - // Add some padding on start of first diff. - Patch patch = patches.getFirst(); - LinkedList diffs = patch.diffs; - if (diffs.isEmpty() || diffs.getFirst().operation != Operation.EQUAL) { - // Add nullPadding equality. - diffs.addFirst(new Diff(Operation.EQUAL, nullPadding)); - patch.start1 -= paddingLength; // Should be 0. - patch.start2 -= paddingLength; // Should be 0. - patch.length1 += paddingLength; - patch.length2 += paddingLength; - } else if (paddingLength > diffs.getFirst().text.length()) { - // Grow first equality. - Diff firstDiff = diffs.getFirst(); - int extraLength = paddingLength - firstDiff.text.length(); - firstDiff.text = nullPadding.substring(firstDiff.text.length()) - + firstDiff.text; - patch.start1 -= extraLength; - patch.start2 -= extraLength; - patch.length1 += extraLength; - patch.length2 += extraLength; - } - - // Add some padding on end of last diff. - patch = patches.getLast(); - diffs = patch.diffs; - if (diffs.isEmpty() || diffs.getLast().operation != Operation.EQUAL) { - // Add nullPadding equality. - diffs.addLast(new Diff(Operation.EQUAL, nullPadding)); - patch.length1 += paddingLength; - patch.length2 += paddingLength; - } else if (paddingLength > diffs.getLast().text.length()) { - // Grow last equality. - Diff lastDiff = diffs.getLast(); - int extraLength = paddingLength - lastDiff.text.length(); - lastDiff.text += nullPadding.substring(0, extraLength); - patch.length1 += extraLength; - patch.length2 += extraLength; - } - - return nullPadding; - } - - /** - * Look through the patches and break up any which are longer than the - * maximum limit of the match algorithm. - * Intended to be called only from within patchApply. - * - * @param patches LinkedList of Patch objects. - */ - public void patchSplitMax(LinkedList patches) { - short patch_size = Match_MaxBits; - String precontext, postcontext; - Patch patch; - int start1, start2; - boolean empty; - Operation diff_type; - String diff_text; - ListIterator pointer = patches.listIterator(); - Patch bigpatch = pointer.hasNext() ? pointer.next() : null; - while (bigpatch != null) { - if (bigpatch.length1 <= Match_MaxBits) { - bigpatch = pointer.hasNext() ? pointer.next() : null; - continue; - } - // Remove the big old patch. - pointer.remove(); - start1 = bigpatch.start1; - start2 = bigpatch.start2; - precontext = ""; - while (!bigpatch.diffs.isEmpty()) { - // Create one of several smaller patches. - patch = new Patch(); - empty = true; - patch.start1 = start1 - precontext.length(); - patch.start2 = start2 - precontext.length(); - if (precontext.length() != 0) { - patch.length1 = patch.length2 = precontext.length(); - patch.diffs.add(new Diff(Operation.EQUAL, precontext)); - } - while (!bigpatch.diffs.isEmpty() - && patch.length1 < patch_size - Patch_Margin) { - diff_type = bigpatch.diffs.getFirst().operation; - diff_text = bigpatch.diffs.getFirst().text; - if (diff_type == Operation.INSERT) { - // Insertions are harmless. - patch.length2 += diff_text.length(); - start2 += diff_text.length(); - patch.diffs.addLast(bigpatch.diffs.removeFirst()); - empty = false; - } else if (diff_type == Operation.DELETE && patch.diffs.size() == 1 - && patch.diffs.getFirst().operation == Operation.EQUAL - && diff_text.length() > 2 * patch_size) { - // This is a large deletion. Let it pass in one chunk. - patch.length1 += diff_text.length(); - start1 += diff_text.length(); - empty = false; - patch.diffs.add(new Diff(diff_type, diff_text)); - bigpatch.diffs.removeFirst(); - } else { - // Deletion or equality. Only take as much as we can stomach. - diff_text = diff_text.substring(0, Math.min(diff_text.length(), - patch_size - patch.length1 - Patch_Margin)); - patch.length1 += diff_text.length(); - start1 += diff_text.length(); - if (diff_type == Operation.EQUAL) { - patch.length2 += diff_text.length(); - start2 += diff_text.length(); - } else { - empty = false; - } - patch.diffs.add(new Diff(diff_type, diff_text)); - if (diff_text.equals(bigpatch.diffs.getFirst().text)) { - bigpatch.diffs.removeFirst(); - } else { - bigpatch.diffs.getFirst().text = bigpatch.diffs.getFirst().text - .substring(diff_text.length()); - } - } - } - // Compute the head context for the next patch. - precontext = diffText2(patch.diffs); - precontext = precontext.substring(Math.max(0, precontext.length() - - Patch_Margin)); - // Append the end context for this patch. - if (diffText1(bigpatch.diffs).length() > Patch_Margin) { - postcontext = diffText1(bigpatch.diffs).substring(0, Patch_Margin); - } else { - postcontext = diffText1(bigpatch.diffs); - } - if (postcontext.length() != 0) { - patch.length1 += postcontext.length(); - patch.length2 += postcontext.length(); - if (!patch.diffs.isEmpty() - && patch.diffs.getLast().operation == Operation.EQUAL) { - patch.diffs.getLast().text += postcontext; - } else { - patch.diffs.add(new Diff(Operation.EQUAL, postcontext)); - } - } - if (!empty) { - pointer.add(patch); - } - } - bigpatch = pointer.hasNext() ? pointer.next() : null; - } - } - - /** - * Take a list of patches and return a textual representation. - * - * @param patches List of Patch objects. - * @return Text representation of patches. - */ - public String patchToText(List patches) { - StringBuilder text = new StringBuilder(); - for (Patch aPatch : patches) { - text.append(aPatch); - } - return text.toString(); - } - - /** - * Parse a textual representation of patches and return a List of Patch - * objects. - * - * @param textline Text representation of patches. - * @return List of Patch objects. - * @throws IllegalArgumentException If invalid input. - */ - public List patchFromText(String textline) - throws IllegalArgumentException { - List patches = new LinkedList(); - if (textline.length() == 0) { - return patches; - } - List textList = Arrays.asList(textline.split("\n")); - LinkedList text = new LinkedList(textList); - Patch patch; - Pattern patchHeader - = Pattern.compile("^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@$"); - Matcher m; - char sign; - String line; - while (!text.isEmpty()) { - m = patchHeader.matcher(text.getFirst()); - if (!m.matches()) { - throw new IllegalArgumentException( - "Invalid patch string: " + text.getFirst()); - } - patch = new Patch(); - patches.add(patch); - patch.start1 = Integer.parseInt(m.group(1)); - if (m.group(2).length() == 0) { - patch.start1--; - patch.length1 = 1; - } else if (m.group(2).equals("0")) { - patch.length1 = 0; - } else { - patch.start1--; - patch.length1 = Integer.parseInt(m.group(2)); - } - - patch.start2 = Integer.parseInt(m.group(3)); - if (m.group(4).length() == 0) { - patch.start2--; - patch.length2 = 1; - } else if (m.group(4).equals("0")) { - patch.length2 = 0; - } else { - patch.start2--; - patch.length2 = Integer.parseInt(m.group(4)); - } - text.removeFirst(); - - while (!text.isEmpty()) { - try { - sign = text.getFirst().charAt(0); - } catch (IndexOutOfBoundsException e) { - // Blank line? Whatever. - text.removeFirst(); - continue; - } - line = text.getFirst().substring(1); - line = line.replace("+", "%2B"); // decode would change all "+" to " " - try { - line = URLDecoder.decode(line, "UTF-8"); - } catch (UnsupportedEncodingException e) { - // Not likely on modern system. - throw new Error("This system does not support UTF-8.", e); - } catch (IllegalArgumentException e) { - // Malformed URI sequence. - throw new IllegalArgumentException( - "Illegal escape in patchFromText: " + line, e); - } - if (sign == '-') { - // Deletion. - patch.diffs.add(new Diff(Operation.DELETE, line)); - } else if (sign == '+') { - // Insertion. - patch.diffs.add(new Diff(Operation.INSERT, line)); - } else if (sign == ' ') { - // Minor equality. - patch.diffs.add(new Diff(Operation.EQUAL, line)); - } else if (sign == '@') { - // Start of next patch. - break; - } else { - // WTF? - throw new IllegalArgumentException( - "Invalid patch mode '" + sign + "' in: " + line); - } - text.removeFirst(); - } - } - return patches; - } - - -} +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * 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 ru.bsc.test.autotester.diff; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/* + * Functions for diff, match and patch. + * Computes the difference between two texts to create a patch. + * Applies the patch onto another text, allowing for errors. + * + * @author fraser@google.com (Neil Fraser) + */ + +/** + * Class containing the diff, match and patch methods. + * Also contains the behaviour settings. + */ +@SuppressWarnings("all") +public class DiffMatchPatch { + + // Defaults. + // Set these on your diff_match_patch instance to override the defaults. + + /** + * Number of seconds to map a diff before giving up (0 for infinity). + */ + public float Diff_Timeout = 1.0f; + /** + * Cost of an empty edit operation in terms of edit characters. + */ + public short Diff_EditCost = 4; + /** + * At what point is no match declared (0.0 = perfection, 1.0 = very loose). + */ + public float Match_Threshold = 0.5f; + /** + * How far to search for a match (0 = exact location, 1000+ = broad match). + * A match this many characters away from the expected location will add + * 1.0 to the score (0.0 is a perfect match). + */ + public int Match_Distance = 1000; + /** + * When deleting a large block of text (over ~64 characters), how close do + * the contents have to be to match the expected contents. (0.0 = perfection, + * 1.0 = very loose). Note that Match_Threshold controls how closely the + * end points of a delete need to match. + */ + public float Patch_DeleteThreshold = 0.5f; + /** + * Chunk size for context length. + */ + public short Patch_Margin = 4; + + /** + * The number of bits in an int. + */ + private short Match_MaxBits = 32; + + + // DIFF FUNCTIONS + + + /** + * Find the differences between two texts. + * Run a faster, slightly less optimal diff. + * This method allows the 'checklines' of diffMain() to be optional. + * Most of the time checklines is wanted, so default to true. + * + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @return Linked List of Diff objects. + */ + public LinkedList diffMain(String text1, String text2) { + return diffMain(text1, text2, true); + } + + /** + * Find the differences between two texts. + * + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @return Linked List of Diff objects. + */ + public LinkedList diffMain(String text1, String text2, + boolean checklines) { + // Set a deadline by which time the diff must be complete. + long deadline; + if (Diff_Timeout <= 0) { + deadline = Long.MAX_VALUE; + } else { + deadline = System.currentTimeMillis() + (long) (Diff_Timeout * 1000); + } + return diffMain(text1, text2, checklines, deadline); + } + + /** + * Find the differences between two texts. Simplifies the problem by + * stripping any common prefix or suffix off the texts before diffing. + * + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @param deadline Time when the diff should be complete by. Used + * internally for recursive calls. Users should set DiffTimeout instead. + * @return Linked List of Diff objects. + */ + private LinkedList diffMain(String text1, String text2, + boolean checklines, long deadline) { + // Check for null inputs. + if (text1 == null || text2 == null) { + throw new IllegalArgumentException("Null inputs. (diffMain)"); + } + + // Check for equality (speedup). + LinkedList diffs; + if (text1.equals(text2)) { + diffs = new LinkedList(); + if (text1.length() != 0) { + diffs.add(new Diff(Operation.EQUAL, text1)); + } + return diffs; + } + + // Trim off common prefix (speedup). + int commonlength = diffCommonPrefix(text1, text2); + String commonprefix = text1.substring(0, commonlength); + text1 = text1.substring(commonlength); + text2 = text2.substring(commonlength); + + // Trim off common suffix (speedup). + commonlength = diffCommonSuffix(text1, text2); + String commonsuffix = text1.substring(text1.length() - commonlength); + text1 = text1.substring(0, text1.length() - commonlength); + text2 = text2.substring(0, text2.length() - commonlength); + + // Compute the diff on the middle block. + diffs = diffCompute(text1, text2, checklines, deadline); + + // Restore the prefix and suffix. + if (commonprefix.length() != 0) { + diffs.addFirst(new Diff(Operation.EQUAL, commonprefix)); + } + if (commonsuffix.length() != 0) { + diffs.addLast(new Diff(Operation.EQUAL, commonsuffix)); + } + + diffCleanupMerge(diffs); + return diffs; + } + + /** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @param deadline Time when the diff should be complete by. + * @return Linked List of Diff objects. + */ + private LinkedList diffCompute(String text1, String text2, + boolean checklines, long deadline) { + LinkedList diffs = new LinkedList(); + + if (text1.length() == 0) { + // Just add some text (speedup). + diffs.add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + if (text2.length() == 0) { + // Just delete some text (speedup). + diffs.add(new Diff(Operation.DELETE, text1)); + return diffs; + } + + String longtext = text1.length() > text2.length() ? text1 : text2; + String shorttext = text1.length() > text2.length() ? text2 : text1; + int i = longtext.indexOf(shorttext); + if (i != -1) { + // Shorter text is inside the longer text (speedup). + Operation op = (text1.length() > text2.length()) ? + Operation.DELETE : Operation.INSERT; + diffs.add(new Diff(op, longtext.substring(0, i))); + diffs.add(new Diff(Operation.EQUAL, shorttext)); + diffs.add(new Diff(op, longtext.substring(i + shorttext.length()))); + return diffs; + } + + if (shorttext.length() == 1) { + // Single character string. + // After the previous speedup, the character can't be an equality. + diffs.add(new Diff(Operation.DELETE, text1)); + diffs.add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + // Check to see if the problem can be split in two. + String[] hm = diffHalfMatch(text1, text2); + if (hm != null) { + // A half-match was found, sort out the return data. + String text1_a = hm[0]; + String text1_b = hm[1]; + String text2_a = hm[2]; + String text2_b = hm[3]; + String mid_common = hm[4]; + // Send both pairs off for separate processing. + LinkedList diffs_a = diffMain(text1_a, text2_a, + checklines, deadline); + LinkedList diffs_b = diffMain(text1_b, text2_b, + checklines, deadline); + // Merge the results. + diffs = diffs_a; + diffs.add(new Diff(Operation.EQUAL, mid_common)); + diffs.addAll(diffs_b); + return diffs; + } + + if (checklines && text1.length() > 100 && text2.length() > 100) { + return diffLineMode(text1, text2, deadline); + } + + return diffBisect(text1, text2, deadline); + } + + /** + * Do a quick line-level diff on both strings, then rediff the parts for + * greater accuracy. + * This speedup can produce non-minimal diffs. + * + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param deadline Time when the diff should be complete by. + * @return Linked List of Diff objects. + */ + private LinkedList diffLineMode(String text1, String text2, + long deadline) { + // Scan the text on a line-by-line basis first. + LinesToCharsResult b = diffLinesToChars(text1, text2); + text1 = b.chars1; + text2 = b.chars2; + List linearray = b.lineArray; + + LinkedList diffs = diffMain(text1, text2, false, deadline); + + // Convert the diff back to original text. + diffCharsToLines(diffs, linearray); + // Eliminate freak matches (e.g. blank lines) + diffCleanupSemantic(diffs); + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + diffs.add(new Diff(Operation.EQUAL, "")); + int count_delete = 0; + int count_insert = 0; + String text_delete = ""; + String text_insert = ""; + ListIterator pointer = diffs.listIterator(); + Diff thisDiff = pointer.next(); + while (thisDiff != null) { + switch (thisDiff.operation) { + case INSERT: + count_insert++; + text_insert += thisDiff.text; + break; + case DELETE: + count_delete++; + text_delete += thisDiff.text; + break; + case EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete >= 1 && count_insert >= 1) { + // Delete the offending records and add the merged ones. + pointer.previous(); + for (int j = 0; j < count_delete + count_insert; j++) { + pointer.previous(); + pointer.remove(); + } + for (Diff newDiff : diffMain(text_delete, text_insert, false, + deadline)) { + pointer.add(newDiff); + } + } + count_insert = 0; + count_delete = 0; + text_delete = ""; + text_insert = ""; + break; + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + diffs.removeLast(); // Remove the dummy entry at the end. + + return diffs; + } + + /** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param deadline Time at which to bail if not yet complete. + * @return LinkedList of Diff objects. + */ + protected LinkedList diffBisect(String text1, String text2, + long deadline) { + // Cache the text lengths to prevent multiple calls. + int text1_length = text1.length(); + int text2_length = text2.length(); + int max_d = (text1_length + text2_length + 1) / 2; + int v_offset = max_d; + int v_length = 2 * max_d; + int[] v1 = new int[v_length]; + int[] v2 = new int[v_length]; + for (int x = 0; x < v_length; x++) { + v1[x] = -1; + v2[x] = -1; + } + v1[v_offset + 1] = 0; + v2[v_offset + 1] = 0; + int delta = text1_length - text2_length; + // If the total number of characters is odd, then the front path will + // collide with the reverse path. + boolean front = (delta % 2 != 0); + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + int k1start = 0; + int k1end = 0; + int k2start = 0; + int k2end = 0; + for (int d = 0; d < max_d; d++) { + // Bail out if deadline is reached. + if (System.currentTimeMillis() > deadline) { + break; + } + + // Walk the front path one step. + for (int k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { + int k1_offset = v_offset + k1; + int x1; + if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) { + x1 = v1[k1_offset + 1]; + } else { + x1 = v1[k1_offset - 1] + 1; + } + int y1 = x1 - k1; + while (x1 < text1_length && y1 < text2_length + && text1.charAt(x1) == text2.charAt(y1)) { + x1++; + y1++; + } + v1[k1_offset] = x1; + if (x1 > text1_length) { + // Ran off the right of the graph. + k1end += 2; + } else if (y1 > text2_length) { + // Ran off the bottom of the graph. + k1start += 2; + } else if (front) { + int k2_offset = v_offset + delta - k1; + if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { + // Mirror x2 onto top-left coordinate system. + int x2 = text1_length - v2[k2_offset]; + if (x1 >= x2) { + // Overlap detected. + return diffBisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + + // Walk the reverse path one step. + for (int k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { + int k2_offset = v_offset + k2; + int x2; + if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) { + x2 = v2[k2_offset + 1]; + } else { + x2 = v2[k2_offset - 1] + 1; + } + int y2 = x2 - k2; + while (x2 < text1_length && y2 < text2_length + && text1.charAt(text1_length - x2 - 1) + == text2.charAt(text2_length - y2 - 1)) { + x2++; + y2++; + } + v2[k2_offset] = x2; + if (x2 > text1_length) { + // Ran off the left of the graph. + k2end += 2; + } else if (y2 > text2_length) { + // Ran off the top of the graph. + k2start += 2; + } else if (!front) { + int k1_offset = v_offset + delta - k2; + if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { + int x1 = v1[k1_offset]; + int y1 = v_offset + x1 - k1_offset; + // Mirror x2 onto top-left coordinate system. + x2 = text1_length - x2; + if (x1 >= x2) { + // Overlap detected. + return diffBisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + } + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + LinkedList diffs = new LinkedList(); + diffs.add(new Diff(Operation.DELETE, text1)); + diffs.add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + /** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param x Index of split point in text1. + * @param y Index of split point in text2. + * @param deadline Time at which to bail if not yet complete. + * @return LinkedList of Diff objects. + */ + private LinkedList diffBisectSplit(String text1, String text2, + int x, int y, long deadline) { + String text1a = text1.substring(0, x); + String text2a = text2.substring(0, y); + String text1b = text1.substring(x); + String text2b = text2.substring(y); + + // Compute both diffs serially. + LinkedList diffs = diffMain(text1a, text2a, false, deadline); + LinkedList diffsb = diffMain(text1b, text2b, false, deadline); + + diffs.addAll(diffsb); + return diffs; + } + + /** + * Split two texts into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * + * @param text1 First string. + * @param text2 Second string. + * @return An object containing the encoded text1, the encoded text2 and + * the List of unique strings. The zeroth element of the List of + * unique strings is intentionally blank. + */ + protected LinesToCharsResult diffLinesToChars(String text1, String text2) { + List lineArray = new ArrayList(); + Map lineHash = new HashMap(); + // e.g. linearray[4] == "Hello\n" + // e.g. linehash.get("Hello\n") == 4 + + // "\x00" is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + lineArray.add(""); + + String chars1 = diffLinesToCharsMunge(text1, lineArray, lineHash); + String chars2 = diffLinesToCharsMunge(text2, lineArray, lineHash); + return new LinesToCharsResult(chars1, chars2, lineArray); + } + + /** + * Split a text into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * + * @param text String to encode. + * @param lineArray List of unique strings. + * @param lineHash Map of strings to indices. + * @return Encoded string. + */ + private String diffLinesToCharsMunge(String text, List lineArray, + Map lineHash) { + int lineStart = 0; + int lineEnd = -1; + String line; + StringBuilder chars = new StringBuilder(); + // Walk the text, pulling out a substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + while (lineEnd < text.length() - 1) { + lineEnd = text.indexOf('\n', lineStart); + if (lineEnd == -1) { + lineEnd = text.length() - 1; + } + line = text.substring(lineStart, lineEnd + 1); + lineStart = lineEnd + 1; + + if (lineHash.containsKey(line)) { + chars.append(String.valueOf((char) (int) lineHash.get(line))); + } else { + lineArray.add(line); + lineHash.put(line, lineArray.size() - 1); + chars.append(String.valueOf((char) (lineArray.size() - 1))); + } + } + return chars.toString(); + } + + /** + * Rehydrate the text in a diff from a string of line hashes to real lines of + * text. + * + * @param diffs LinkedList of Diff objects. + * @param lineArray List of unique strings. + */ + protected void diffCharsToLines(LinkedList diffs, + List lineArray) { + StringBuilder text; + for (Diff diff : diffs) { + text = new StringBuilder(); + for (int y = 0; y < diff.text.length(); y++) { + text.append(lineArray.get(diff.text.charAt(y))); + } + diff.text = text.toString(); + } + } + + /** + * Determine the common prefix of two strings + * + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the start of each string. + */ + public int diffCommonPrefix(String text1, String text2) { + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + int n = Math.min(text1.length(), text2.length()); + for (int i = 0; i < n; i++) { + if (text1.charAt(i) != text2.charAt(i)) { + return i; + } + } + return n; + } + + /** + * Determine the common suffix of two strings + * + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of each string. + */ + public int diffCommonSuffix(String text1, String text2) { + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + int text1_length = text1.length(); + int text2_length = text2.length(); + int n = Math.min(text1_length, text2_length); + for (int i = 1; i <= n; i++) { + if (text1.charAt(text1_length - i) != text2.charAt(text2_length - i)) { + return i - 1; + } + } + return n; + } + + /** + * Determine if the suffix of one string is the prefix of another. + * + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of the first + * string and the start of the second string. + */ + protected int diffCommonOverlap(String text1, String text2) { + // Cache the text lengths to prevent multiple calls. + int text1_length = text1.length(); + int text2_length = text2.length(); + // Eliminate the null case. + if (text1_length == 0 || text2_length == 0) { + return 0; + } + // Truncate the longer string. + if (text1_length > text2_length) { + text1 = text1.substring(text1_length - text2_length); + } else if (text1_length < text2_length) { + text2 = text2.substring(0, text1_length); + } + int text_length = Math.min(text1_length, text2_length); + // Quick check for the worst case. + if (text1.equals(text2)) { + return text_length; + } + + // Start by looking for a single character match + // and increase length until no match is found. + // Performance analysis: http://neil.fraser.name/news/2010/11/04/ + int best = 0; + int length = 1; + while (true) { + String pattern = text1.substring(text_length - length); + int found = text2.indexOf(pattern); + if (found == -1) { + return best; + } + length += found; + if (found == 0 || text1.substring(text_length - length).equals( + text2.substring(0, length))) { + best = length; + length++; + } + } + } + + /** + * Do the two texts share a substring which is at least half the length of + * the longer text? + * This speedup can produce non-minimal diffs. + * + * @param text1 First string. + * @param text2 Second string. + * @return Five element String array, containing the prefix of text1, the + * suffix of text1, the prefix of text2, the suffix of text2 and the + * common middle. Or null if there was no match. + */ + protected String[] diffHalfMatch(String text1, String text2) { + if (Diff_Timeout <= 0) { + // Don't risk returning a non-optimal diff if we have unlimited time. + return null; + } + String longtext = text1.length() > text2.length() ? text1 : text2; + String shorttext = text1.length() > text2.length() ? text2 : text1; + if (longtext.length() < 4 || shorttext.length() * 2 < longtext.length()) { + return null; // Pointless. + } + + // First check if the second quarter is the seed for a half-match. + String[] hm1 = diffHalfMatchI(longtext, shorttext, + (longtext.length() + 3) / 4); + // Check again based on the third quarter. + String[] hm2 = diffHalfMatchI(longtext, shorttext, + (longtext.length() + 1) / 2); + String[] hm; + if (hm1 == null && hm2 == null) { + return null; + } else if (hm2 == null) { + hm = hm1; + } else if (hm1 == null) { + hm = hm2; + } else { + // Both matched. Select the longest. + hm = hm1[4].length() > hm2[4].length() ? hm1 : hm2; + } + + // A half-match was found, sort out the return data. + if (text1.length() > text2.length()) { + return hm; + //return new String[]{hm[0], hm[1], hm[2], hm[3], hm[4]}; + } else { + return new String[]{hm[2], hm[3], hm[0], hm[1], hm[4]}; + } + } + + /** + * Does a substring of shorttext exist within longtext such that the + * substring is at least half the length of longtext? + * + * @param longtext Longer string. + * @param shorttext Shorter string. + * @param i Start index of quarter length substring within longtext. + * @return Five element String array, containing the prefix of longtext, the + * suffix of longtext, the prefix of shorttext, the suffix of shorttext + * and the common middle. Or null if there was no match. + */ + private String[] diffHalfMatchI(String longtext, String shorttext, int i) { + // Start with a 1/4 length substring at position i as a seed. + String seed = longtext.substring(i, i + longtext.length() / 4); + int j = -1; + String best_common = ""; + String best_longtext_a = "", best_longtext_b = ""; + String best_shorttext_a = "", best_shorttext_b = ""; + while ((j = shorttext.indexOf(seed, j + 1)) != -1) { + int prefixLength = diffCommonPrefix(longtext.substring(i), + shorttext.substring(j)); + int suffixLength = diffCommonSuffix(longtext.substring(0, i), + shorttext.substring(0, j)); + if (best_common.length() < suffixLength + prefixLength) { + best_common = shorttext.substring(j - suffixLength, j) + + shorttext.substring(j, j + prefixLength); + best_longtext_a = longtext.substring(0, i - suffixLength); + best_longtext_b = longtext.substring(i + prefixLength); + best_shorttext_a = shorttext.substring(0, j - suffixLength); + best_shorttext_b = shorttext.substring(j + prefixLength); + } + } + if (best_common.length() * 2 >= longtext.length()) { + return new String[]{best_longtext_a, best_longtext_b, + best_shorttext_a, best_shorttext_b, best_common}; + } else { + return null; + } + } + + /** + * Reduce the number of edits by eliminating semantically trivial equalities. + * + * @param diffs LinkedList of Diff objects. + */ + public void diffCleanupSemantic(LinkedList diffs) { + if (diffs.isEmpty()) { + return; + } + boolean changes = false; + Stack equalities = new Stack(); // Stack of qualities. + String lastequality = null; // Always equal to equalities.lastElement().text + ListIterator pointer = diffs.listIterator(); + // Number of characters that changed prior to the equality. + int length_insertions1 = 0; + int length_deletions1 = 0; + // Number of characters that changed after the equality. + int length_insertions2 = 0; + int length_deletions2 = 0; + Diff thisDiff = pointer.next(); + while (thisDiff != null) { + if (thisDiff.operation == Operation.EQUAL) { + // Equality found. + equalities.push(thisDiff); + length_insertions1 = length_insertions2; + length_deletions1 = length_deletions2; + length_insertions2 = 0; + length_deletions2 = 0; + lastequality = thisDiff.text; + } else { + // An insertion or deletion. + if (thisDiff.operation == Operation.INSERT) { + length_insertions2 += thisDiff.text.length(); + } else { + length_deletions2 += thisDiff.text.length(); + } + // Eliminate an equality that is smaller or equal to the edits on both + // sides of it. + if (lastequality != null && (lastequality.length() + <= Math.max(length_insertions1, length_deletions1)) + && (lastequality.length() + <= Math.max(length_insertions2, length_deletions2))) { + //System.out.println("Splitting: '" + lastequality + "'"); + // Walk back to offending equality. + while (thisDiff != equalities.lastElement()) { + thisDiff = pointer.previous(); + } + pointer.next(); + + // Replace equality with a delete. + pointer.set(new Diff(Operation.DELETE, lastequality)); + // Insert a corresponding an insert. + pointer.add(new Diff(Operation.INSERT, lastequality)); + + equalities.pop(); // Throw away the equality we just deleted. + if (!equalities.empty()) { + // Throw away the previous equality (it needs to be reevaluated). + equalities.pop(); + } + if (equalities.empty()) { + // There are no previous equalities, walk back to the start. + while (pointer.hasPrevious()) { + pointer.previous(); + } + } else { + // There is a safe equality we can fall back to. + thisDiff = equalities.lastElement(); + while (thisDiff != pointer.previous()) { + // Intentionally empty loop. + } + } + + length_insertions1 = 0; // Reset the counters. + length_insertions2 = 0; + length_deletions1 = 0; + length_deletions2 = 0; + lastequality = null; + changes = true; + } + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + + // Normalize the diff. + if (changes) { + diffCleanupMerge(diffs); + } + diffCleanupSemanticLossless(diffs); + + // Find any overlaps between deletions and insertions. + // e.g: abcxxxxxxdef + // -> abcxxxdef + // e.g: xxxabcdefxxx + // -> defxxxabc + // Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = diffs.listIterator(); + Diff prevDiff = null; + thisDiff = null; + if (pointer.hasNext()) { + prevDiff = pointer.next(); + if (pointer.hasNext()) { + thisDiff = pointer.next(); + } + } + while (thisDiff != null) { + if (prevDiff.operation == Operation.DELETE && + thisDiff.operation == Operation.INSERT) { + String deletion = prevDiff.text; + String insertion = thisDiff.text; + int overlap_length1 = this.diffCommonOverlap(deletion, insertion); + int overlap_length2 = this.diffCommonOverlap(insertion, deletion); + if (overlap_length1 >= overlap_length2) { + if (overlap_length1 >= deletion.length() / 2.0 || + overlap_length1 >= insertion.length() / 2.0) { + // Overlap found. Insert an equality and trim the surrounding edits. + pointer.previous(); + pointer.add(new Diff(Operation.EQUAL, + insertion.substring(0, overlap_length1))); + prevDiff.text = + deletion.substring(0, deletion.length() - overlap_length1); + thisDiff.text = insertion.substring(overlap_length1); + // pointer.add inserts the element before the cursor, so there is + // no need to step past the new element. + } + } else { + if (overlap_length2 >= deletion.length() / 2.0 || + overlap_length2 >= insertion.length() / 2.0) { + // Reverse overlap found. + // Insert an equality and swap and trim the surrounding edits. + pointer.previous(); + pointer.add(new Diff(Operation.EQUAL, + deletion.substring(0, overlap_length2))); + prevDiff.operation = Operation.INSERT; + prevDiff.text = + insertion.substring(0, insertion.length() - overlap_length2); + thisDiff.operation = Operation.DELETE; + thisDiff.text = deletion.substring(overlap_length2); + // pointer.add inserts the element before the cursor, so there is + // no need to step past the new element. + } + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + prevDiff = thisDiff; + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + } + + /** + * Look for single edits surrounded on both sides by equalities + * which can be shifted sideways to align the edit to a word boundary. + * e.g: The cat came. -> The cat came. + * + * @param diffs LinkedList of Diff objects. + */ + public void diffCleanupSemanticLossless(LinkedList diffs) { + String equality1, edit, equality2; + String commonString; + int commonOffset; + int score, bestScore; + String bestEquality1, bestEdit, bestEquality2; + // Create a new iterator at the start. + ListIterator pointer = diffs.listIterator(); + Diff prevDiff = pointer.hasNext() ? pointer.next() : null; + Diff thisDiff = pointer.hasNext() ? pointer.next() : null; + Diff nextDiff = pointer.hasNext() ? pointer.next() : null; + // Intentionally ignore the first and last element (don't need checking). + while (nextDiff != null) { + if (prevDiff.operation == Operation.EQUAL && + nextDiff.operation == Operation.EQUAL) { + // This is a single edit surrounded by equalities. + equality1 = prevDiff.text; + edit = thisDiff.text; + equality2 = nextDiff.text; + + // First, shift the edit as far left as possible. + commonOffset = diffCommonSuffix(equality1, edit); + if (commonOffset != 0) { + commonString = edit.substring(edit.length() - commonOffset); + equality1 = equality1.substring(0, equality1.length() - commonOffset); + edit = commonString + edit.substring(0, edit.length() - commonOffset); + equality2 = commonString + equality2; + } + + // Second, step character by character right, looking for the best fit. + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + bestScore = diffCleanupSemanticScore(equality1, edit) + + diffCleanupSemanticScore(edit, equality2); + while (edit.length() != 0 && equality2.length() != 0 + && edit.charAt(0) == equality2.charAt(0)) { + equality1 += edit.charAt(0); + edit = edit.substring(1) + equality2.charAt(0); + equality2 = equality2.substring(1); + score = diffCleanupSemanticScore(equality1, edit) + + diffCleanupSemanticScore(edit, equality2); + // The >= encourages trailing rather than leading whitespace on edits. + if (score >= bestScore) { + bestScore = score; + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + } + } + + if (!prevDiff.text.equals(bestEquality1)) { + // We have an improvement, save it back to the diff. + if (bestEquality1.length() != 0) { + prevDiff.text = bestEquality1; + } else { + pointer.previous(); // Walk past nextDiff. + pointer.previous(); // Walk past thisDiff. + pointer.previous(); // Walk past prevDiff. + pointer.remove(); // Delete prevDiff. + pointer.next(); // Walk past thisDiff. + pointer.next(); // Walk past nextDiff. + } + thisDiff.text = bestEdit; + if (bestEquality2.length() != 0) { + nextDiff.text = bestEquality2; + } else { + pointer.remove(); // Delete nextDiff. + nextDiff = thisDiff; + thisDiff = prevDiff; + } + } + } + prevDiff = thisDiff; + thisDiff = nextDiff; + nextDiff = pointer.hasNext() ? pointer.next() : null; + } + } + + /** + * Given two strings, compute a score representing whether the internal + * boundary falls on logical boundaries. + * Scores range from 6 (best) to 0 (worst). + * + * @param one First string. + * @param two Second string. + * @return The score. + */ + private int diffCleanupSemanticScore(String one, String two) { + if (one.length() == 0 || two.length() == 0) { + // Edges are the best. + return 6; + } + + // Each port of this function behaves slightly differently due to + // subtle differences in each language's definition of things like + // 'whitespace'. Since this function's purpose is largely cosmetic, + // the choice has been made to use each language's native features + // rather than force total conformity. + char char1 = one.charAt(one.length() - 1); + char char2 = two.charAt(0); + boolean nonAlphaNumeric1 = !Character.isLetterOrDigit(char1); + boolean nonAlphaNumeric2 = !Character.isLetterOrDigit(char2); + boolean whitespace1 = nonAlphaNumeric1 && Character.isWhitespace(char1); + boolean whitespace2 = nonAlphaNumeric2 && Character.isWhitespace(char2); + boolean lineBreak1 = whitespace1 + && Character.getType(char1) == Character.CONTROL; + boolean lineBreak2 = whitespace2 + && Character.getType(char2) == Character.CONTROL; + boolean blankLine1 = lineBreak1 && BLANKLINEEND.matcher(one).find(); + boolean blankLine2 = lineBreak2 && BLANKLINESTART.matcher(two).find(); + + if (blankLine1 || blankLine2) { + // Five points for blank lines. + return 5; + } else if (lineBreak1 || lineBreak2) { + // Four points for line breaks. + return 4; + } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { + // Three points for end of sentences. + return 3; + } else if (whitespace1 || whitespace2) { + // Two points for whitespace. + return 2; + } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { + // One point for non-alphanumeric. + return 1; + } + return 0; + } + + // Define some regex patterns for matching boundaries. + private Pattern BLANKLINEEND + = Pattern.compile("\\n\\r?\\n\\Z", Pattern.DOTALL); + private Pattern BLANKLINESTART + = Pattern.compile("\\A\\r?\\n\\r?\\n", Pattern.DOTALL); + + /** + * Reduce the number of edits by eliminating operationally trivial equalities. + * + * @param diffs LinkedList of Diff objects. + */ + public void diffCleanupEfficiency(LinkedList diffs) { + if (diffs.isEmpty()) { + return; + } + boolean changes = false; + Stack equalities = new Stack(); // Stack of equalities. + String lastequality = null; // Always equal to equalities.lastElement().text + ListIterator pointer = diffs.listIterator(); + // Is there an insertion operation before the last equality. + boolean pre_ins = false; + // Is there a deletion operation before the last equality. + boolean pre_del = false; + // Is there an insertion operation after the last equality. + boolean post_ins = false; + // Is there a deletion operation after the last equality. + boolean post_del = false; + Diff thisDiff = pointer.next(); + Diff safeDiff = thisDiff; // The last Diff that is known to be unsplitable. + while (thisDiff != null) { + if (thisDiff.operation == Operation.EQUAL) { + // Equality found. + if (thisDiff.text.length() < Diff_EditCost && (post_ins || post_del)) { + // Candidate found. + equalities.push(thisDiff); + pre_ins = post_ins; + pre_del = post_del; + lastequality = thisDiff.text; + } else { + // Not a candidate, and can never become one. + equalities.clear(); + lastequality = null; + safeDiff = thisDiff; + } + post_ins = post_del = false; + } else { + // An insertion or deletion. + if (thisDiff.operation == Operation.DELETE) { + post_del = true; + } else { + post_ins = true; + } + /* + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + */ + if (lastequality != null + && ((pre_ins && pre_del && post_ins && post_del) + || ((lastequality.length() < Diff_EditCost / 2) + && ((pre_ins ? 1 : 0) + (pre_del ? 1 : 0) + + (post_ins ? 1 : 0) + (post_del ? 1 : 0)) == 3))) { + //System.out.println("Splitting: '" + lastequality + "'"); + // Walk back to offending equality. + while (thisDiff != equalities.lastElement()) { + thisDiff = pointer.previous(); + } + pointer.next(); + + // Replace equality with a delete. + pointer.set(new Diff(Operation.DELETE, lastequality)); + // Insert a corresponding an insert. + pointer.add(thisDiff = new Diff(Operation.INSERT, lastequality)); + + equalities.pop(); // Throw away the equality we just deleted. + lastequality = null; + if (pre_ins && pre_del) { + // No changes made which could affect previous entry, keep going. + post_ins = post_del = true; + equalities.clear(); + safeDiff = thisDiff; + } else { + if (!equalities.empty()) { + // Throw away the previous equality (it needs to be reevaluated). + equalities.pop(); + } + if (equalities.empty()) { + // There are no previous questionable equalities, + // walk back to the last known safe diff. + thisDiff = safeDiff; + } else { + // There is an equality we can fall back to. + thisDiff = equalities.lastElement(); + } + while (thisDiff != pointer.previous()) { + // Intentionally empty loop. + } + post_ins = post_del = false; + } + + changes = true; + } + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + + if (changes) { + diffCleanupMerge(diffs); + } + } + + /** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * + * @param diffs LinkedList of Diff objects. + */ + public void diffCleanupMerge(LinkedList diffs) { + diffs.add(new Diff(Operation.EQUAL, "")); // Add a dummy entry at the end. + ListIterator pointer = diffs.listIterator(); + int count_delete = 0; + int count_insert = 0; + String text_delete = ""; + String text_insert = ""; + Diff thisDiff = pointer.next(); + Diff prevEqual = null; + int commonlength; + while (thisDiff != null) { + switch (thisDiff.operation) { + case INSERT: + count_insert++; + text_insert += thisDiff.text; + prevEqual = null; + break; + case DELETE: + count_delete++; + text_delete += thisDiff.text; + prevEqual = null; + break; + case EQUAL: + if (count_delete + count_insert > 1) { + boolean both_types = count_delete != 0 && count_insert != 0; + // Delete the offending records. + pointer.previous(); // Reverse direction. + while (count_delete-- > 0) { + pointer.previous(); + pointer.remove(); + } + while (count_insert-- > 0) { + pointer.previous(); + pointer.remove(); + } + if (both_types) { + // Factor out any common prefixies. + commonlength = diffCommonPrefix(text_insert, text_delete); + if (commonlength != 0) { + if (pointer.hasPrevious()) { + thisDiff = pointer.previous(); + assert thisDiff.operation == Operation.EQUAL + : "Previous diff should have been an equality."; + thisDiff.text += text_insert.substring(0, commonlength); + pointer.next(); + } else { + pointer.add(new Diff(Operation.EQUAL, + text_insert.substring(0, commonlength))); + } + text_insert = text_insert.substring(commonlength); + text_delete = text_delete.substring(commonlength); + } + // Factor out any common suffixies. + commonlength = diffCommonSuffix(text_insert, text_delete); + if (commonlength != 0) { + thisDiff = pointer.next(); + thisDiff.text = text_insert.substring(text_insert.length() + - commonlength) + thisDiff.text; + text_insert = text_insert.substring(0, text_insert.length() + - commonlength); + text_delete = text_delete.substring(0, text_delete.length() + - commonlength); + pointer.previous(); + } + } + // Insert the merged records. + if (text_delete.length() != 0) { + pointer.add(new Diff(Operation.DELETE, text_delete)); + } + if (text_insert.length() != 0) { + pointer.add(new Diff(Operation.INSERT, text_insert)); + } + // Step forward to the equality. + thisDiff = pointer.hasNext() ? pointer.next() : null; + } else if (prevEqual != null) { + // Merge this equality with the previous one. + prevEqual.text += thisDiff.text; + pointer.remove(); + thisDiff = pointer.previous(); + pointer.next(); // Forward direction + } + count_insert = 0; + count_delete = 0; + text_delete = ""; + text_insert = ""; + prevEqual = thisDiff; + break; + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + if (diffs.getLast().text.length() == 0) { + diffs.removeLast(); // Remove the dummy entry at the end. + } + + /* + * Second pass: look for single edits surrounded on both sides by equalities + * which can be shifted sideways to eliminate an equality. + * e.g: ABAC -> ABAC + */ + boolean changes = false; + // Create a new iterator at the start. + // (As opposed to walking the current one back.) + pointer = diffs.listIterator(); + Diff prevDiff = pointer.hasNext() ? pointer.next() : null; + thisDiff = pointer.hasNext() ? pointer.next() : null; + Diff nextDiff = pointer.hasNext() ? pointer.next() : null; + // Intentionally ignore the first and last element (don't need checking). + while (nextDiff != null) { + if (prevDiff.operation == Operation.EQUAL && + nextDiff.operation == Operation.EQUAL) { + // This is a single edit surrounded by equalities. + if (thisDiff.text.endsWith(prevDiff.text)) { + // Shift the edit over the previous equality. + thisDiff.text = prevDiff.text + + thisDiff.text.substring(0, thisDiff.text.length() + - prevDiff.text.length()); + nextDiff.text = prevDiff.text + nextDiff.text; + pointer.previous(); // Walk past nextDiff. + pointer.previous(); // Walk past thisDiff. + pointer.previous(); // Walk past prevDiff. + pointer.remove(); // Delete prevDiff. + pointer.next(); // Walk past thisDiff. + thisDiff = pointer.next(); // Walk past nextDiff. + nextDiff = pointer.hasNext() ? pointer.next() : null; + changes = true; + } else if (thisDiff.text.startsWith(nextDiff.text)) { + // Shift the edit over the next equality. + prevDiff.text += nextDiff.text; + thisDiff.text = thisDiff.text.substring(nextDiff.text.length()) + + nextDiff.text; + pointer.remove(); // Delete nextDiff. + nextDiff = pointer.hasNext() ? pointer.next() : null; + changes = true; + } + } + prevDiff = thisDiff; + thisDiff = nextDiff; + nextDiff = pointer.hasNext() ? pointer.next() : null; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if (changes) { + diffCleanupMerge(diffs); + } + } + + /** + * loc is a location in text1, compute and return the equivalent location in + * text2. + * e.g. "The cat" vs "The big cat", 1->1, 5->8 + * + * @param diffs LinkedList of Diff objects. + * @param loc Location within text1. + * @return Location within text2. + */ + public int diffXIndex(LinkedList diffs, int loc) { + int chars1 = 0; + int chars2 = 0; + int last_chars1 = 0; + int last_chars2 = 0; + Diff lastDiff = null; + for (Diff aDiff : diffs) { + if (aDiff.operation != Operation.INSERT) { + // Equality or deletion. + chars1 += aDiff.text.length(); + } + if (aDiff.operation != Operation.DELETE) { + // Equality or insertion. + chars2 += aDiff.text.length(); + } + if (chars1 > loc) { + // Overshot the location. + lastDiff = aDiff; + break; + } + last_chars1 = chars1; + last_chars2 = chars2; + } + if (lastDiff != null && lastDiff.operation == Operation.DELETE) { + // The location was deleted. + return last_chars2; + } + // Add the remaining character length. + return last_chars2 + (loc - last_chars1); + } + + /** + * Convert a Diff list into a pretty HTML report. + * + * @param diffs LinkedList of Diff objects. + * @return HTML representation. + */ + public String diffPrettyHtml(LinkedList diffs) { + StringBuilder html = new StringBuilder(); + for (Diff aDiff : diffs) { + String text = aDiff.text.replace("&", "&").replace("<", "<") + .replace(">", ">").replace("\n", "¶
"); + switch (aDiff.operation) { + case INSERT: + html.append("").append(text) + .append(""); + break; + case DELETE: + html.append("").append(text) + .append(""); + break; + case EQUAL: + html.append("").append(text).append(""); + break; + } + } + return html.toString(); + } + + /** + * Compute and return the source text (all equalities and deletions). + * + * @param diffs LinkedList of Diff objects. + * @return Source text. + */ + public String diffText1(LinkedList diffs) { + StringBuilder text = new StringBuilder(); + for (Diff aDiff : diffs) { + if (aDiff.operation != Operation.INSERT) { + text.append(aDiff.text); + } + } + return text.toString(); + } + + /** + * Compute and return the destination text (all equalities and insertions). + * + * @param diffs LinkedList of Diff objects. + * @return Destination text. + */ + public String diffText2(LinkedList diffs) { + StringBuilder text = new StringBuilder(); + for (Diff aDiff : diffs) { + if (aDiff.operation != Operation.DELETE) { + text.append(aDiff.text); + } + } + return text.toString(); + } + + /** + * Compute the Levenshtein distance; the number of inserted, deleted or + * substituted characters. + * + * @param diffs LinkedList of Diff objects. + * @return Number of changes. + */ + public int diffLevenshtein(LinkedList diffs) { + int levenshtein = 0; + int insertions = 0; + int deletions = 0; + for (Diff aDiff : diffs) { + switch (aDiff.operation) { + case INSERT: + insertions += aDiff.text.length(); + break; + case DELETE: + deletions += aDiff.text.length(); + break; + case EQUAL: + // A deletion and an insertion is one substitution. + levenshtein += Math.max(insertions, deletions); + insertions = 0; + deletions = 0; + break; + } + } + levenshtein += Math.max(insertions, deletions); + return levenshtein; + } + + /** + * Crush the diff into an encoded string which describes the operations + * required to transform text1 into text2. + * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. + * Operations are tab-separated. Inserted text is escaped using %xx notation. + * + * @param diffs Array of Diff objects. + * @return Delta text. + */ + public String diffToDelta(LinkedList diffs) { + StringBuilder text = new StringBuilder(); + for (Diff aDiff : diffs) { + switch (aDiff.operation) { + case INSERT: + try { + text.append("+").append(URLEncoder.encode(aDiff.text, "UTF-8") + .replace('+', ' ')).append("\t"); + } catch (UnsupportedEncodingException e) { + // Not likely on modern system. + throw new Error("This system does not support UTF-8.", e); + } + break; + case DELETE: + text.append("-").append(aDiff.text.length()).append("\t"); + break; + case EQUAL: + text.append("=").append(aDiff.text.length()).append("\t"); + break; + } + } + String delta = text.toString(); + if (delta.length() != 0) { + // Strip off trailing tab character. + delta = delta.substring(0, delta.length() - 1); + delta = new Patch().unescapeForEncodeUriCompatability(delta); + } + return delta; + } + + /** + * Given the original text1, and an encoded string which describes the + * operations required to transform text1 into text2, compute the full diff. + * + * @param text1 Source string for the diff. + * @param delta Delta text. + * @return Array of Diff objects or null if invalid. + * @throws IllegalArgumentException If invalid input. + */ + public LinkedList diffFromDelta(String text1, String delta) + throws IllegalArgumentException { + LinkedList diffs = new LinkedList(); + int pointer = 0; // Cursor in text1 + String[] tokens = delta.split("\t"); + for (String token : tokens) { + if (token.length() == 0) { + // Blank tokens are ok (from a trailing \t). + continue; + } + // Each token begins with a one character parameter which specifies the + // operation of this token (delete, insert, equality). + String param = token.substring(1); + switch (token.charAt(0)) { + case '+': + // decode would change all "+" to " " + param = param.replace("+", "%2B"); + try { + param = URLDecoder.decode(param, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // Not likely on modern system. + throw new Error("This system does not support UTF-8.", e); + } catch (IllegalArgumentException e) { + // Malformed URI sequence. + throw new IllegalArgumentException( + "Illegal escape in diffFromDelta: " + param, e); + } + diffs.add(new Diff(Operation.INSERT, param)); + break; + case '-': + // Fall through. + case '=': + int n; + try { + n = Integer.parseInt(param); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Invalid number in diffFromDelta: " + param, e); + } + if (n < 0) { + throw new IllegalArgumentException( + "Negative number in diffFromDelta: " + param); + } + String text; + try { + text = text1.substring(pointer, pointer += n); + } catch (StringIndexOutOfBoundsException e) { + throw new IllegalArgumentException("Delta length (" + pointer + + ") larger than source text length (" + text1.length() + + ").", e); + } + if (token.charAt(0) == '=') { + diffs.add(new Diff(Operation.EQUAL, text)); + } else { + diffs.add(new Diff(Operation.DELETE, text)); + } + break; + default: + // Anything else is an error. + throw new IllegalArgumentException( + "Invalid diff operation in diffFromDelta: " + token.charAt(0)); + } + } + if (pointer != text1.length()) { + throw new IllegalArgumentException("Delta length (" + pointer + + ") smaller than source text length (" + text1.length() + ")."); + } + return diffs; + } + + + // MATCH FUNCTIONS + + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc'. + * Returns -1 if no match found. + * + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + public int matchMain(String text, String pattern, int loc) { + // Check for null inputs. + if (text == null || pattern == null) { + throw new IllegalArgumentException("Null inputs. (matchMain)"); + } + + loc = Math.max(0, Math.min(loc, text.length())); + if (text.equals(pattern)) { + // Shortcut (potentially not guaranteed by the algorithm) + return 0; + } else if (text.length() == 0) { + // Nothing to match. + return -1; + } else if (loc + pattern.length() <= text.length() + && text.substring(loc, loc + pattern.length()).equals(pattern)) { + // Perfect match at the perfect spot! (Includes case of null pattern) + return loc; + } else { + // Do a fuzzy compare. + return matchBitap(text, pattern, loc); + } + } + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the + * Bitap algorithm. Returns -1 if no match found. + * + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + protected int matchBitap(String text, String pattern, int loc) { + assert (Match_MaxBits == 0 || pattern.length() <= Match_MaxBits) + : "Pattern too long for this application."; + + // Initialise the alphabet. + Map s = matchAlphabet(pattern); + + // Highest score beyond which we give up. + double score_threshold = Match_Threshold; + // Is there a nearby exact match? (speedup) + int best_loc = text.indexOf(pattern, loc); + if (best_loc != -1) { + score_threshold = Math.min(matchBitapScore(0, best_loc, loc, pattern), + score_threshold); + // What about in the other direction? (speedup) + best_loc = text.lastIndexOf(pattern, loc + pattern.length()); + if (best_loc != -1) { + score_threshold = Math.min(matchBitapScore(0, best_loc, loc, pattern), + score_threshold); + } + } + + // Initialise the bit arrays. + int matchmask = 1 << (pattern.length() - 1); + best_loc = -1; + + int bin_min, bin_mid; + int bin_max = pattern.length() + text.length(); + // Empty initialization added to appease Java compiler. + int[] last_rd = new int[0]; + for (int d = 0; d < pattern.length(); d++) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from 'loc' we can stray at + // this error level. + bin_min = 0; + bin_mid = bin_max; + while (bin_min < bin_mid) { + if (matchBitapScore(d, loc + bin_mid, loc, pattern) + <= score_threshold) { + bin_min = bin_mid; + } else { + bin_max = bin_mid; + } + bin_mid = (bin_max - bin_min) / 2 + bin_min; + } + // Use the result from this iteration as the maximum for the next. + bin_max = bin_mid; + int start = Math.max(1, loc - bin_mid + 1); + int finish = Math.min(loc + bin_mid, text.length()) + pattern.length(); + + int[] rd = new int[finish + 2]; + rd[finish + 1] = (1 << d) - 1; + for (int j = finish; j >= start; j--) { + int charMatch; + if (text.length() <= j - 1 || !s.containsKey(text.charAt(j - 1))) { + // Out of range. + charMatch = 0; + } else { + charMatch = s.get(text.charAt(j - 1)); + } + if (d == 0) { + // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; + } else { + // Subsequent passes: fuzzy match. + rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) + | (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1]; + } + if ((rd[j] & matchmask) != 0) { + double score = matchBitapScore(d, j - 1, loc, pattern); + // This match will almost certainly be better than any existing + // match. But check anyway. + if (score <= score_threshold) { + // Told you so. + score_threshold = score; + best_loc = j - 1; + if (best_loc > loc) { + // When passing loc, don't exceed our current distance from loc. + start = Math.max(1, 2 * loc - best_loc); + } else { + // Already passed loc, downhill from here on in. + break; + } + } + } + } + if (matchBitapScore(d + 1, loc, loc, pattern) > score_threshold) { + // No hope for a (better) match at greater error levels. + break; + } + last_rd = rd; + } + return best_loc; + } + + /** + * Compute and return the score for a match with e errors and x location. + * + * @param e Number of errors in match. + * @param x Location of match. + * @param loc Expected location of match. + * @param pattern Pattern being sought. + * @return Overall score for match (0.0 = good, 1.0 = bad). + */ + private double matchBitapScore(int e, int x, int loc, String pattern) { + float accuracy = (float) e / pattern.length(); + int proximity = Math.abs(loc - x); + if (Match_Distance == 0) { + // Dodge divide by zero error. + return proximity == 0 ? accuracy : 1.0; + } + return accuracy + (proximity / (float) Match_Distance); + } + + /** + * Initialise the alphabet for the Bitap algorithm. + * + * @param pattern The text to encode. + * @return Hash of character locations. + */ + protected Map matchAlphabet(String pattern) { + Map s = new HashMap(); + char[] char_pattern = pattern.toCharArray(); + for (char c : char_pattern) { + s.put(c, 0); + } + int i = 0; + for (char c : char_pattern) { + s.put(c, s.get(c) | (1 << (pattern.length() - i - 1))); + i++; + } + return s; + } + + + // PATCH FUNCTIONS + + + /** + * Increase the context until it is unique, + * but don't let the pattern expand beyond Match_MaxBits. + * + * @param patch The patch to grow. + * @param text Source text. + */ + protected void patch_addContext(Patch patch, String text) { + if (text.length() == 0) { + return; + } + String pattern = text.substring(patch.start2, patch.start2 + patch.length1); + int padding = 0; + + // Look for the first and last matches of pattern in text. If two different + // matches are found, increase the pattern length. + while (text.indexOf(pattern) != text.lastIndexOf(pattern) + && pattern.length() < Match_MaxBits - Patch_Margin - Patch_Margin) { + padding += Patch_Margin; + pattern = text.substring(Math.max(0, patch.start2 - padding), + Math.min(text.length(), patch.start2 + patch.length1 + padding)); + } + // Add one chunk for good luck. + padding += Patch_Margin; + + // Add the prefix. + String prefix = text.substring(Math.max(0, patch.start2 - padding), + patch.start2); + if (prefix.length() != 0) { + patch.diffs.addFirst(new Diff(Operation.EQUAL, prefix)); + } + // Add the suffix. + String suffix = text.substring(patch.start2 + patch.length1, + Math.min(text.length(), patch.start2 + patch.length1 + padding)); + if (suffix.length() != 0) { + patch.diffs.addLast(new Diff(Operation.EQUAL, suffix)); + } + + // Roll back the start points. + patch.start1 -= prefix.length(); + patch.start2 -= prefix.length(); + // Extend the lengths. + patch.length1 += prefix.length() + suffix.length(); + patch.length2 += prefix.length() + suffix.length(); + } + + /** + * Compute a list of patches to turn text1 into text2. + * A set of diffs will be computed. + * + * @param text1 Old text. + * @param text2 New text. + * @return LinkedList of Patch objects. + */ + public LinkedList patchMake(String text1, String text2) { + if (text1 == null || text2 == null) { + throw new IllegalArgumentException("Null inputs. (patchMake)"); + } + // No diffs provided, compute our own. + LinkedList diffs = diffMain(text1, text2, true); + if (diffs.size() > 2) { + diffCleanupSemantic(diffs); + diffCleanupEfficiency(diffs); + } + return patchMake(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. + * text1 will be derived from the provided diffs. + * + * @param diffs Array of Diff objects for text1 to text2. + * @return LinkedList of Patch objects. + */ + public LinkedList patchMake(LinkedList diffs) { + if (diffs == null) { + throw new IllegalArgumentException("Null inputs. (patchMake)"); + } + // No origin string provided, compute our own. + String text1 = diffText1(diffs); + return patchMake(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. + * text2 is ignored, diffs are the delta between text1 and text2. + * + * @param text1 Old text + * @param text2 Ignored. + * @param diffs Array of Diff objects for text1 to text2. + * @return LinkedList of Patch objects. + * @deprecated Prefer patchMake(String text1, LinkedList diffs). + */ + public LinkedList patchMake(String text1, String text2, + LinkedList diffs) { + return patchMake(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. + * text2 is not provided, diffs are the delta between text1 and text2. + * + * @param text1 Old text. + * @param diffs Array of Diff objects for text1 to text2. + * @return LinkedList of Patch objects. + */ + public LinkedList patchMake(String text1, LinkedList diffs) { + if (text1 == null || diffs == null) { + throw new IllegalArgumentException("Null inputs. (patchMake)"); + } + + LinkedList patches = new LinkedList(); + if (diffs.isEmpty()) { + return patches; // Get rid of the null case. + } + Patch patch = new Patch(); + int char_count1 = 0; // Number of characters into the text1 string. + int char_count2 = 0; // Number of characters into the text2 string. + // Start with text1 (prepatch_text) and apply the diffs until we arrive at + // text2 (postpatch_text). We recreate the patches one by one to determine + // context info. + String prepatch_text = text1; + String postpatch_text = text1; + for (Diff aDiff : diffs) { + if (patch.diffs.isEmpty() && aDiff.operation != Operation.EQUAL) { + // A new patch starts here. + patch.start1 = char_count1; + patch.start2 = char_count2; + } + + switch (aDiff.operation) { + case INSERT: + patch.diffs.add(aDiff); + patch.length2 += aDiff.text.length(); + postpatch_text = postpatch_text.substring(0, char_count2) + + aDiff.text + postpatch_text.substring(char_count2); + break; + case DELETE: + patch.length1 += aDiff.text.length(); + patch.diffs.add(aDiff); + postpatch_text = postpatch_text.substring(0, char_count2) + + postpatch_text.substring(char_count2 + aDiff.text.length()); + break; + case EQUAL: + if (aDiff.text.length() <= 2 * Patch_Margin + && !patch.diffs.isEmpty() && aDiff != diffs.getLast()) { + // Small equality inside a patch. + patch.diffs.add(aDiff); + patch.length1 += aDiff.text.length(); + patch.length2 += aDiff.text.length(); + } + + if (aDiff.text.length() >= 2 * Patch_Margin) { + // Time for a new patch. + if (!patch.diffs.isEmpty()) { + patch_addContext(patch, prepatch_text); + patches.add(patch); + patch = new Patch(); + // Unlike Unidiff, our patch lists have a rolling context. + // http://code.google.com/p/google-diff-match-patch/wiki/Unidiff + // Update prepatch text & pos to reflect the application of the + // just completed patch. + prepatch_text = postpatch_text; + char_count1 = char_count2; + } + } + break; + } + + // Update the current character count. + if (aDiff.operation != Operation.INSERT) { + char_count1 += aDiff.text.length(); + } + if (aDiff.operation != Operation.DELETE) { + char_count2 += aDiff.text.length(); + } + } + // Pick up the leftover patch if not empty. + if (!patch.diffs.isEmpty()) { + patch_addContext(patch, prepatch_text); + patches.add(patch); + } + + return patches; + } + + /** + * Given an array of patches, return another array that is identical. + * + * @param patches Array of Patch objects. + * @return Array of Patch objects. + */ + public LinkedList patchDeepCopy(LinkedList patches) { + LinkedList patchesCopy = new LinkedList(); + for (Patch aPatch : patches) { + Patch patchCopy = new Patch(); + for (Diff aDiff : aPatch.diffs) { + Diff diffCopy = new Diff(aDiff.operation, aDiff.text); + patchCopy.diffs.add(diffCopy); + } + patchCopy.start1 = aPatch.start1; + patchCopy.start2 = aPatch.start2; + patchCopy.length1 = aPatch.length1; + patchCopy.length2 = aPatch.length2; + patchesCopy.add(patchCopy); + } + return patchesCopy; + } + + /** + * Merge a set of patches onto the text. Return a patched text, as well + * as an array of true/false values indicating which patches were applied. + * + * @param patches Array of Patch objects + * @param text Old text. + * @return Two element Object array, containing the new text and an array of + * boolean values. + */ + public Object[] patchApply(LinkedList patches, String text) { + if (patches.isEmpty()) { + return new Object[]{text, new boolean[0]}; + } + + // Deep copy the patches so that no changes are made to originals. + patches = patchDeepCopy(patches); + + String nullPadding = patchAddPadding(patches); + text = nullPadding + text + nullPadding; + patchSplitMax(patches); + + int x = 0; + // delta keeps track of the offset between the expected and actual location + // of the previous patch. If there are patches expected at positions 10 and + // 20, but the first patch was found at 12, delta is 2 and the second patch + // has an effective expected position of 22. + int delta = 0; + boolean[] results = new boolean[patches.size()]; + for (Patch aPatch : patches) { + int expected_loc = aPatch.start2 + delta; + String text1 = diffText1(aPatch.diffs); + int start_loc; + int end_loc = -1; + if (text1.length() > this.Match_MaxBits) { + // patchSplitMax will only provide an oversized pattern in the case of + // a monster delete. + start_loc = matchMain(text, + text1.substring(0, this.Match_MaxBits), expected_loc); + if (start_loc != -1) { + end_loc = matchMain(text, + text1.substring(text1.length() - this.Match_MaxBits), + expected_loc + text1.length() - this.Match_MaxBits); + if (end_loc == -1 || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = -1; + } + } + } else { + start_loc = matchMain(text, text1, expected_loc); + } + if (start_loc == -1) { + // No match found. :( + results[x] = false; + // Subtract the delta for this failed patch from subsequent patches. + delta -= aPatch.length2 - aPatch.length1; + } else { + // Found a match. :) + results[x] = true; + delta = start_loc - expected_loc; + String text2; + if (end_loc == -1) { + text2 = text.substring(start_loc, + Math.min(start_loc + text1.length(), text.length())); + } else { + text2 = text.substring(start_loc, + Math.min(end_loc + this.Match_MaxBits, text.length())); + } + if (text1.equals(text2)) { + // Perfect match, just shove the replacement text in. + text = text.substring(0, start_loc) + diffText2(aPatch.diffs) + + text.substring(start_loc + text1.length()); + } else { + // Imperfect match. Run a diff to get a framework of equivalent + // indices. + LinkedList diffs = diffMain(text1, text2, false); + if (text1.length() > this.Match_MaxBits + && diffLevenshtein(diffs) / (float) text1.length() + > this.Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + results[x] = false; + } else { + diffCleanupSemanticLossless(diffs); + int index1 = 0; + for (Diff aDiff : aPatch.diffs) { + if (aDiff.operation != Operation.EQUAL) { + int index2 = diffXIndex(diffs, index1); + if (aDiff.operation == Operation.INSERT) { + // Insertion + text = text.substring(0, start_loc + index2) + aDiff.text + + text.substring(start_loc + index2); + } else if (aDiff.operation == Operation.DELETE) { + // Deletion + text = text.substring(0, start_loc + index2) + + text.substring(start_loc + diffXIndex(diffs, + index1 + aDiff.text.length())); + } + } + if (aDiff.operation != Operation.DELETE) { + index1 += aDiff.text.length(); + } + } + } + } + } + x++; + } + // Strip the padding off. + text = text.substring(nullPadding.length(), text.length() + - nullPadding.length()); + return new Object[]{text, results}; + } + + /** + * Add some padding on text start and end so that edges can match something. + * Intended to be called only from within patchApply. + * + * @param patches Array of Patch objects. + * @return The padding string added to each side. + */ + public String patchAddPadding(LinkedList patches) { + short paddingLength = this.Patch_Margin; + String nullPadding = ""; + StringBuilder buffer = new StringBuilder(); + for (short x = 1; x <= paddingLength; x++) { + buffer.append(String.valueOf((char) x)); + } + nullPadding = buffer.toString(); + + // Bump all the patches forward. + for (Patch aPatch : patches) { + aPatch.start1 += paddingLength; + aPatch.start2 += paddingLength; + } + + // Add some padding on start of first diff. + Patch patch = patches.getFirst(); + LinkedList diffs = patch.diffs; + if (diffs.isEmpty() || diffs.getFirst().operation != Operation.EQUAL) { + // Add nullPadding equality. + diffs.addFirst(new Diff(Operation.EQUAL, nullPadding)); + patch.start1 -= paddingLength; // Should be 0. + patch.start2 -= paddingLength; // Should be 0. + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs.getFirst().text.length()) { + // Grow first equality. + Diff firstDiff = diffs.getFirst(); + int extraLength = paddingLength - firstDiff.text.length(); + firstDiff.text = nullPadding.substring(firstDiff.text.length()) + + firstDiff.text; + patch.start1 -= extraLength; + patch.start2 -= extraLength; + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + // Add some padding on end of last diff. + patch = patches.getLast(); + diffs = patch.diffs; + if (diffs.isEmpty() || diffs.getLast().operation != Operation.EQUAL) { + // Add nullPadding equality. + diffs.addLast(new Diff(Operation.EQUAL, nullPadding)); + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs.getLast().text.length()) { + // Grow last equality. + Diff lastDiff = diffs.getLast(); + int extraLength = paddingLength - lastDiff.text.length(); + lastDiff.text += nullPadding.substring(0, extraLength); + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + return nullPadding; + } + + /** + * Look through the patches and break up any which are longer than the + * maximum limit of the match algorithm. + * Intended to be called only from within patchApply. + * + * @param patches LinkedList of Patch objects. + */ + public void patchSplitMax(LinkedList patches) { + short patch_size = Match_MaxBits; + String precontext, postcontext; + Patch patch; + int start1, start2; + boolean empty; + Operation diff_type; + String diff_text; + ListIterator pointer = patches.listIterator(); + Patch bigpatch = pointer.hasNext() ? pointer.next() : null; + while (bigpatch != null) { + if (bigpatch.length1 <= Match_MaxBits) { + bigpatch = pointer.hasNext() ? pointer.next() : null; + continue; + } + // Remove the big old patch. + pointer.remove(); + start1 = bigpatch.start1; + start2 = bigpatch.start2; + precontext = ""; + while (!bigpatch.diffs.isEmpty()) { + // Create one of several smaller patches. + patch = new Patch(); + empty = true; + patch.start1 = start1 - precontext.length(); + patch.start2 = start2 - precontext.length(); + if (precontext.length() != 0) { + patch.length1 = patch.length2 = precontext.length(); + patch.diffs.add(new Diff(Operation.EQUAL, precontext)); + } + while (!bigpatch.diffs.isEmpty() + && patch.length1 < patch_size - Patch_Margin) { + diff_type = bigpatch.diffs.getFirst().operation; + diff_text = bigpatch.diffs.getFirst().text; + if (diff_type == Operation.INSERT) { + // Insertions are harmless. + patch.length2 += diff_text.length(); + start2 += diff_text.length(); + patch.diffs.addLast(bigpatch.diffs.removeFirst()); + empty = false; + } else if (diff_type == Operation.DELETE && patch.diffs.size() == 1 + && patch.diffs.getFirst().operation == Operation.EQUAL + && diff_text.length() > 2 * patch_size) { + // This is a large deletion. Let it pass in one chunk. + patch.length1 += diff_text.length(); + start1 += diff_text.length(); + empty = false; + patch.diffs.add(new Diff(diff_type, diff_text)); + bigpatch.diffs.removeFirst(); + } else { + // Deletion or equality. Only take as much as we can stomach. + diff_text = diff_text.substring(0, Math.min(diff_text.length(), + patch_size - patch.length1 - Patch_Margin)); + patch.length1 += diff_text.length(); + start1 += diff_text.length(); + if (diff_type == Operation.EQUAL) { + patch.length2 += diff_text.length(); + start2 += diff_text.length(); + } else { + empty = false; + } + patch.diffs.add(new Diff(diff_type, diff_text)); + if (diff_text.equals(bigpatch.diffs.getFirst().text)) { + bigpatch.diffs.removeFirst(); + } else { + bigpatch.diffs.getFirst().text = bigpatch.diffs.getFirst().text + .substring(diff_text.length()); + } + } + } + // Compute the head context for the next patch. + precontext = diffText2(patch.diffs); + precontext = precontext.substring(Math.max(0, precontext.length() + - Patch_Margin)); + // Append the end context for this patch. + if (diffText1(bigpatch.diffs).length() > Patch_Margin) { + postcontext = diffText1(bigpatch.diffs).substring(0, Patch_Margin); + } else { + postcontext = diffText1(bigpatch.diffs); + } + if (postcontext.length() != 0) { + patch.length1 += postcontext.length(); + patch.length2 += postcontext.length(); + if (!patch.diffs.isEmpty() + && patch.diffs.getLast().operation == Operation.EQUAL) { + patch.diffs.getLast().text += postcontext; + } else { + patch.diffs.add(new Diff(Operation.EQUAL, postcontext)); + } + } + if (!empty) { + pointer.add(patch); + } + } + bigpatch = pointer.hasNext() ? pointer.next() : null; + } + } + + /** + * Take a list of patches and return a textual representation. + * + * @param patches List of Patch objects. + * @return Text representation of patches. + */ + public String patchToText(List patches) { + StringBuilder text = new StringBuilder(); + for (Patch aPatch : patches) { + text.append(aPatch); + } + return text.toString(); + } + + /** + * Parse a textual representation of patches and return a List of Patch + * objects. + * + * @param textline Text representation of patches. + * @return List of Patch objects. + * @throws IllegalArgumentException If invalid input. + */ + public List patchFromText(String textline) + throws IllegalArgumentException { + List patches = new LinkedList(); + if (textline.length() == 0) { + return patches; + } + List textList = Arrays.asList(textline.split("\n")); + LinkedList text = new LinkedList(textList); + Patch patch; + Pattern patchHeader + = Pattern.compile("^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@$"); + Matcher m; + char sign; + String line; + while (!text.isEmpty()) { + m = patchHeader.matcher(text.getFirst()); + if (!m.matches()) { + throw new IllegalArgumentException( + "Invalid patch string: " + text.getFirst()); + } + patch = new Patch(); + patches.add(patch); + patch.start1 = Integer.parseInt(m.group(1)); + if (m.group(2).length() == 0) { + patch.start1--; + patch.length1 = 1; + } else if (m.group(2).equals("0")) { + patch.length1 = 0; + } else { + patch.start1--; + patch.length1 = Integer.parseInt(m.group(2)); + } + + patch.start2 = Integer.parseInt(m.group(3)); + if (m.group(4).length() == 0) { + patch.start2--; + patch.length2 = 1; + } else if (m.group(4).equals("0")) { + patch.length2 = 0; + } else { + patch.start2--; + patch.length2 = Integer.parseInt(m.group(4)); + } + text.removeFirst(); + + while (!text.isEmpty()) { + try { + sign = text.getFirst().charAt(0); + } catch (IndexOutOfBoundsException e) { + // Blank line? Whatever. + text.removeFirst(); + continue; + } + line = text.getFirst().substring(1); + line = line.replace("+", "%2B"); // decode would change all "+" to " " + try { + line = URLDecoder.decode(line, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // Not likely on modern system. + throw new Error("This system does not support UTF-8.", e); + } catch (IllegalArgumentException e) { + // Malformed URI sequence. + throw new IllegalArgumentException( + "Illegal escape in patchFromText: " + line, e); + } + if (sign == '-') { + // Deletion. + patch.diffs.add(new Diff(Operation.DELETE, line)); + } else if (sign == '+') { + // Insertion. + patch.diffs.add(new Diff(Operation.INSERT, line)); + } else if (sign == ' ') { + // Minor equality. + patch.diffs.add(new Diff(Operation.EQUAL, line)); + } else if (sign == '@') { + // Start of next patch. + break; + } else { + // WTF? + throw new IllegalArgumentException( + "Invalid patch mode '" + sign + "' in: " + line); + } + text.removeFirst(); + } + } + return patches; + } + + +} diff --git a/atf-application/src/main/java/ru/bsc/test/autotester/launcher/impl/LaunchResult.java b/atf-application/src/main/java/ru/bsc/test/autotester/launcher/impl/LaunchResult.java index 5481feb8..abf510d2 100644 --- a/atf-application/src/main/java/ru/bsc/test/autotester/launcher/impl/LaunchResult.java +++ b/atf-application/src/main/java/ru/bsc/test/autotester/launcher/impl/LaunchResult.java @@ -1,48 +1,51 @@ -/* - * Copyright 2018 BSC Msc, LLC - * - * This file is part of the AuTe Framework project - * - * 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 ru.bsc.test.autotester.launcher.impl; - -import lombok.ToString; -import ru.bsc.test.at.executor.model.ScenarioResult; - -import java.util.List; - -/** - * @author Pavel Golovkin - */ -@ToString -public class LaunchResult { - - private int failedTestsCount; - private int passedTestsCount; - - public boolean isFailed() { - return failedTestsCount > 0; - } - - LaunchResult(List scenarioResults) { - for (ScenarioResult scenarioResult: scenarioResults) { - if (scenarioResult.isFailed()) { - failedTestsCount++; - } else { - passedTestsCount++; - } - } - } -} +/* + * Copyright 2018 BSC Msc, LLC + * + * This file is part of the AuTe Framework project + * + * 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 ru.bsc.test.autotester.launcher.impl; + +import ru.bsc.test.at.executor.model.ScenarioResult; + +import java.util.List; + +/** + * @author Pavel Golovkin + */ +public class LaunchResult { + + private int failedTestsCount; + private int passedTestsCount; + + public boolean isFailed() { + return failedTestsCount > 0; + } + + LaunchResult(List scenarioResults) { + for (ScenarioResult scenarioResult: scenarioResults) { + if (scenarioResult.isFailed()) { + failedTestsCount++; + } else { + passedTestsCount++; + } + } + } + + @Override + public String toString() { + return "LaunchResult{" + "failedTestsCount=" + failedTestsCount + ", passedTestsCount=" + passedTestsCount + '}'; + } +} diff --git a/atf-application/src/main/java/ru/bsc/test/autotester/mapper/ExecutionResultRoMapper.java b/atf-application/src/main/java/ru/bsc/test/autotester/mapper/ExecutionResultRoMapper.java index 40bccf06..ff111ee5 100644 --- a/atf-application/src/main/java/ru/bsc/test/autotester/mapper/ExecutionResultRoMapper.java +++ b/atf-application/src/main/java/ru/bsc/test/autotester/mapper/ExecutionResultRoMapper.java @@ -1,90 +1,90 @@ -/* - * Copyright 2018 BSC Msc, LLC - * - * This file is part of the AuTe Framework project - * - * 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 ru.bsc.test.autotester.mapper; - -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import org.mapstruct.Mappings; -import org.springframework.beans.factory.annotation.Autowired; -import ru.bsc.test.at.executor.model.Step; -import ru.bsc.test.autotester.component.JsonDiffCalculator; -import ru.bsc.test.autotester.model.ExecutionResult; -import ru.bsc.test.autotester.ro.ExecutionResultRo; -import ru.bsc.test.autotester.ro.ScenarioResultRo; - -import java.util.List; -import java.util.stream.Collectors; - -@Mapper(config = Config.class) -public abstract class ExecutionResultRoMapper { - - @Autowired - private JsonDiffCalculator diffCalculator; - @Autowired - private ProjectRoMapper projectRoMapper; - @Autowired - private StepRoMapper stepRoMapper; - - @Mappings({ - @Mapping(target = "finished", source = "finished"), - @Mapping(target = "scenarioResultList", ignore = true), - }) - - /* default */ abstract ExecutionResultRo executionResultToRo(ExecutionResult executionResult); - - public ExecutionResultRo map(ExecutionResult executionResult) { - ExecutionResultRo executionResultRo = executionResultToRo(executionResult); - if (executionResultRo == null) { - return null; - } - - if (executionResult != null) { - List scenarioResultList = executionResult.getScenarioResults() - .stream() - .map(scenarioListEntry -> ScenarioResultRo.builder() - .scenario(projectRoMapper.scenarioToScenarioRo("", scenarioListEntry.getScenario())) - .stepResultList(stepRoMapper.convertStepResultListToStepResultRo(scenarioListEntry.getStepResultList())) - .totalSteps(scenarioListEntry - .getScenario() - .getStepList() - .stream() - .filter(step -> !step.getDisabled()) - .map(Step::getStepParameterSetList) - .mapToInt(list -> list != null ? (list.size() == 0 ? 1 : list.size()) : 1) - .sum() - ).build() - ) - .collect(Collectors.toList()); - executionResultRo.setScenarioResultList(scenarioResultList); - } - - addDiffToResult(executionResultRo); - return executionResultRo; - } - - private void addDiffToResult(ExecutionResultRo executionResultRo) { - if (executionResultRo.getScenarioResultList() == null) { - return; - } - executionResultRo.getScenarioResultList().stream() - .map(ScenarioResultRo::getStepResultList) - .flatMap(List::stream) - .forEach(result -> result.setDiff(diffCalculator.calculate(result.getActual(), result.getExpected()))); - } -} +/* + * Copyright 2018 BSC Msc, LLC + * + * This file is part of the AuTe Framework project + * + * 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 ru.bsc.test.autotester.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.springframework.beans.factory.annotation.Autowired; +import ru.bsc.test.at.executor.model.Step; +import ru.bsc.test.autotester.component.JsonDiffCalculator; +import ru.bsc.test.autotester.model.ExecutionResult; +import ru.bsc.test.autotester.ro.ExecutionResultRo; +import ru.bsc.test.autotester.ro.ScenarioResultRo; + +import java.util.List; +import java.util.stream.Collectors; + +@Mapper(config = Config.class) +public abstract class ExecutionResultRoMapper { + + @Autowired + private JsonDiffCalculator diffCalculator; + @Autowired + private ProjectRoMapper projectRoMapper; + @Autowired + private StepRoMapper stepRoMapper; + + @Mappings({ + @Mapping(target = "finished", source = "finished"), + @Mapping(target = "scenarioResultList", ignore = true), + }) + + /* default */ abstract ExecutionResultRo executionResultToRo(ExecutionResult executionResult); + + public ExecutionResultRo map(ExecutionResult executionResult) { + ExecutionResultRo executionResultRo = executionResultToRo(executionResult); + if (executionResultRo == null) { + return null; + } + + if (executionResult != null) { + List scenarioResultList = executionResult.getScenarioResults() + .stream() + .map(scenarioListEntry -> ScenarioResultRo.builder() + .scenario(projectRoMapper.scenarioToScenarioRo("", scenarioListEntry.getScenario())) + .stepResultList(stepRoMapper.convertStepResultListToStepResultRo(scenarioListEntry.getStepResultList())) + .totalSteps(scenarioListEntry + .getScenario() + .getStepList() + .stream() + .filter(step -> !step.getDisabled()) + .map(Step::getStepParameterSetList) + .mapToInt(list -> list != null ? list.size() == 0 ? 1 : list.size() : 1) + .sum() + ).build() + ) + .collect(Collectors.toList()); + executionResultRo.setScenarioResultList(scenarioResultList); + } + + addDiffToResult(executionResultRo); + return executionResultRo; + } + + private void addDiffToResult(ExecutionResultRo executionResultRo) { + if (executionResultRo.getScenarioResultList() == null) { + return; + } + executionResultRo.getScenarioResultList().stream() + .map(ScenarioResultRo::getStepResultList) + .flatMap(List::stream) + .forEach(result -> result.setDiff(diffCalculator.calculate(result.getActual(), result.getExpected()))); + } +} diff --git a/atf-application/src/main/java/ru/bsc/test/autotester/report/impl/allure/AllureReportGenerator.java b/atf-application/src/main/java/ru/bsc/test/autotester/report/impl/allure/AllureReportGenerator.java index f5e41c07..ab063bb3 100644 --- a/atf-application/src/main/java/ru/bsc/test/autotester/report/impl/allure/AllureReportGenerator.java +++ b/atf-application/src/main/java/ru/bsc/test/autotester/report/impl/allure/AllureReportGenerator.java @@ -62,9 +62,8 @@ import ru.yandex.qatools.allure.model.TestCaseResult; import ru.yandex.qatools.allure.model.TestSuiteResult; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; +import java.io.*; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -114,7 +113,8 @@ public synchronized void generate(File directory) throws Exception { } for (AllurePreparedData data : buildReportData(resultDirectory, getScenarioStepResultMap())) { - try (FileWriter writer = new FileWriter(data.getDataFile())) { + try (FileOutputStream fileStream = new FileOutputStream(data.getDataFile()); + OutputStreamWriter writer = new OutputStreamWriter(fileStream, StandardCharsets.UTF_8)) { gson.toJson(data.getSuiteResult(), writer); } catch (IOException e) { log.error("Could not convert testSuiteResult {} to json", data, e); diff --git a/atf-application/src/main/java/ru/bsc/test/autotester/repository/yaml/YamlScenarioRepositoryImpl.java b/atf-application/src/main/java/ru/bsc/test/autotester/repository/yaml/YamlScenarioRepositoryImpl.java index 77fb928b..3f03386c 100644 --- a/atf-application/src/main/java/ru/bsc/test/autotester/repository/yaml/YamlScenarioRepositoryImpl.java +++ b/atf-application/src/main/java/ru/bsc/test/autotester/repository/yaml/YamlScenarioRepositoryImpl.java @@ -1,198 +1,197 @@ -/* - * Copyright 2018 BSC Msc, LLC - * - * This file is part of the AuTe Framework project - * - * 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 ru.bsc.test.autotester.repository.yaml; - -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; -import org.yaml.snakeyaml.reader.ReaderException; -import ru.bsc.test.at.executor.model.Scenario; -import ru.bsc.test.autotester.component.Translator; -import ru.bsc.test.autotester.properties.EnvironmentProperties; -import ru.bsc.test.autotester.repository.ScenarioRepository; -import ru.bsc.test.autotester.repository.yaml.base.BaseYamlRepository; - -import java.io.File; -import java.io.IOException; -import java.nio.file.FileVisitOption; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.apache.commons.lang3.StringUtils.containsIgnoreCase; - -/** - * Created by sdoroshin on 27.10.2017. - * - */ - -@Repository -@Slf4j -public class YamlScenarioRepositoryImpl extends BaseYamlRepository implements ScenarioRepository { - - private final String projectsPath; - - @Autowired - public YamlScenarioRepositoryImpl(EnvironmentProperties environmentProperties, Translator translator) { - super(translator); - this.projectsPath = environmentProperties.getProjectsDirectoryPath(); - } - - @Override - public List findScenarios(String projectCode) { - return findScenarios(projectCode, false); - } - - @Override - public List findScenariosWithSteps(String projectCode) { - return findScenarios(projectCode, true); - } - - @Override - public Scenario findScenario(String projectCode, String scenarioPath) throws IOException { - String[] pathParts = scenarioPath.split("/"); - return loadScenarioFromFiles( - Paths.get(projectsPath, projectCode, "scenarios", scenarioPath).toFile(), - pathParts.length > 1 ? pathParts[0] : null, - true - ); - } - - @Override - public Scenario saveScenario(String projectCode, String scenarioPath, Scenario data, boolean updateDirectoryName) throws IOException { - if (StringUtils.isBlank(data.getName())) { - throw new IOException("Empty scenario name"); - } - - String newCode = updateDirectoryName ? translator.translate(data.getName()) : data.getCode(); - log.info("newCode: {}", newCode); - String newScenarioPath = getScenarioPath(data.getScenarioGroup(), newCode); - log.info("newScenarioPath: {}", newScenarioPath); - if (scenarioPath != null) { - String[] pathParts = scenarioPath.split("/"); - String codePart = pathParts.length > 1 ? pathParts[1] : pathParts[0]; - String groupPart = pathParts.length > 1 ? pathParts[0] : null; - String oldScenarioPath = getScenarioPath(groupPart, codePart); - - if (!Objects.equals(newScenarioPath, oldScenarioPath)) { - if (Paths.get(projectsPath, projectCode, "scenarios", newScenarioPath).toFile().exists()) { - throw new IOException("Directory already exists"); - } - } - - Path path = Paths.get(projectsPath, projectCode, "scenarios", scenarioPath); - if (Files.exists(path)) { - File renamed = new File(path.toFile().getParentFile(), data.getCode() + "-" + UUID.randomUUID().toString()); - boolean canRemove = path.toFile().renameTo(renamed); - if (canRemove) { - FileUtils.deleteDirectory(renamed); - } else { - throw new IOException("Old scenario directory not removed"); - } - } - } else { - if (Paths.get(projectsPath, projectCode, "scenarios", newScenarioPath).toFile().exists()) { - throw new IOException("Directory already exists"); - } - } - - data.setCode(newCode); - File scenarioFile = Paths.get( - projectsPath, - projectCode, - "scenarios", - newScenarioPath, - SCENARIO_YML_FILENAME - ).toFile(); - - log.info("scenarioFile: {}", scenarioFile); - - File scenarioRootDirectory = scenarioFile.getParentFile(); - saveScenarioToFiles(data, scenarioFile); - data.getStepList().forEach(step -> loadStepFromFiles(step, scenarioRootDirectory)); - return data; - } - - private String getScenarioPath(String scenarioGroup, String code) { - return StringUtils.isNotEmpty(scenarioGroup) ? scenarioGroup + "/" + code : code; - } - - @Override - public Set findByRelativeUrl(String projectCode, String relativeUrl) { - return findScenariosWithSteps(projectCode).stream() - .filter(scenario -> checkSteps(scenario, relativeUrl)) - .collect(Collectors.toSet()); - } - - @Override - public void delete(String projectCode, String scenarioPath) throws IOException { - Path scenarioDirectory = Paths.get(projectsPath, projectCode, "scenarios", scenarioPath); - try (Stream filesStream = Files.walk(scenarioDirectory, FileVisitOption.FOLLOW_LINKS)) { - filesStream.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); - } - } - - private List findScenarios(String projectCode, boolean fetchSteps) { - File scenariosDirectory = Paths.get(projectsPath, projectCode, "scenarios").toFile(); - if (!scenariosDirectory.exists()) { - return Collections.emptyList(); - } - File[] directories = scenariosDirectory.listFiles(File::isDirectory); - if (directories == null) { - return Collections.emptyList(); - } - List scenarios = new ArrayList<>(); - for (File directory : directories) { - File scenarioYml = new File(directory, SCENARIO_YML_FILENAME); - if (scenarioYml.exists()) { - try { - scenarios.add(loadScenarioFromFiles(directory, null, fetchSteps)); - } catch (IOException e) { - log.error("Read file " + scenarioYml.getAbsolutePath(), e); - } - } else { - File[] innerFileList = directory.listFiles(File::isDirectory); - if (innerFileList != null) { - for (File scenarioYmlInGroup : innerFileList) { - if (new File(scenarioYmlInGroup, SCENARIO_YML_FILENAME).exists()) { - try { - scenarios.add(loadScenarioFromFiles(scenarioYmlInGroup, directory.getName(), fetchSteps)); - } catch (IOException | ReaderException e) { - log.error("Read file {} {}", scenarioYmlInGroup, e); - } - } - } - } - } - } - return scenarios; - } - - private boolean checkSteps(Scenario scenario, String relativeUrl) { - return scenario.getStepList() - .stream() - .anyMatch(s -> containsIgnoreCase(s.getRelativeUrl(), relativeUrl)); - } - -} +/* + * Copyright 2018 BSC Msc, LLC + * + * This file is part of the AuTe Framework project + * + * 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 ru.bsc.test.autotester.repository.yaml; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.yaml.snakeyaml.reader.ReaderException; +import ru.bsc.test.at.executor.model.Scenario; +import ru.bsc.test.autotester.component.Translator; +import ru.bsc.test.autotester.properties.EnvironmentProperties; +import ru.bsc.test.autotester.repository.ScenarioRepository; +import ru.bsc.test.autotester.repository.yaml.base.BaseYamlRepository; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.apache.commons.lang3.StringUtils.containsIgnoreCase; + +/** + * Created by sdoroshin on 27.10.2017. + * + */ + +@Repository +@Slf4j +public class YamlScenarioRepositoryImpl extends BaseYamlRepository implements ScenarioRepository { + + private final String projectsPath; + + @Autowired + public YamlScenarioRepositoryImpl(EnvironmentProperties environmentProperties, Translator translator) { + super(translator); + this.projectsPath = environmentProperties.getProjectsDirectoryPath(); + } + + @Override + public List findScenarios(String projectCode) { + return findScenarios(projectCode, false); + } + + @Override + public List findScenariosWithSteps(String projectCode) { + return findScenarios(projectCode, true); + } + + @Override + public Scenario findScenario(String projectCode, String scenarioPath) throws IOException { + String[] pathParts = scenarioPath.split("/"); + return loadScenarioFromFiles( + Paths.get(projectsPath, projectCode, "scenarios", scenarioPath).toFile(), + pathParts.length > 1 ? pathParts[0] : null, + true + ); + } + + @Override + public Scenario saveScenario(String projectCode, String scenarioPath, Scenario data, boolean updateDirectoryName) throws IOException { + if (StringUtils.isBlank(data.getName())) { + throw new IOException("Empty scenario name"); + } + + String newCode = updateDirectoryName ? translator.translate(data.getName()) : data.getCode(); + log.info("newCode: {}", newCode); + String newScenarioPath = getScenarioPath(data.getScenarioGroup(), newCode); + log.info("newScenarioPath: {}", newScenarioPath); + if (scenarioPath != null) { + String[] pathParts = scenarioPath.split("/"); + String codePart = pathParts.length > 1 ? pathParts[1] : pathParts[0]; + String groupPart = pathParts.length > 1 ? pathParts[0] : null; + String oldScenarioPath = getScenarioPath(groupPart, codePart); + + if (!Objects.equals(newScenarioPath, oldScenarioPath) && + Paths.get(projectsPath, projectCode, "scenarios", newScenarioPath).toFile().exists()) { + throw new IOException("Directory already exists"); + } + + Path path = Paths.get(projectsPath, projectCode, "scenarios", scenarioPath); + if (Files.exists(path)) { + File renamed = new File(path.toFile().getParentFile(), data.getCode() + "-" + UUID.randomUUID().toString()); + boolean canRemove = path.toFile().renameTo(renamed); + if (canRemove) { + FileUtils.deleteDirectory(renamed); + } else { + throw new IOException("Old scenario directory not removed"); + } + } + } else { + if (Paths.get(projectsPath, projectCode, "scenarios", newScenarioPath).toFile().exists()) { + throw new IOException("Directory already exists"); + } + } + + data.setCode(newCode); + File scenarioFile = Paths.get( + projectsPath, + projectCode, + "scenarios", + newScenarioPath, + SCENARIO_YML_FILENAME + ).toFile(); + + log.info("scenarioFile: {}", scenarioFile); + + File scenarioRootDirectory = scenarioFile.getParentFile(); + saveScenarioToFiles(data, scenarioFile); + data.getStepList().forEach(step -> loadStepFromFiles(step, scenarioRootDirectory)); + return data; + } + + private String getScenarioPath(String scenarioGroup, String code) { + return StringUtils.isNotEmpty(scenarioGroup) ? scenarioGroup + "/" + code : code; + } + + @Override + public Set findByRelativeUrl(String projectCode, String relativeUrl) { + return findScenariosWithSteps(projectCode).stream() + .filter(scenario -> checkSteps(scenario, relativeUrl)) + .collect(Collectors.toSet()); + } + + @Override + public void delete(String projectCode, String scenarioPath) throws IOException { + Path scenarioDirectory = Paths.get(projectsPath, projectCode, "scenarios", scenarioPath); + try (Stream filesStream = Files.walk(scenarioDirectory, FileVisitOption.FOLLOW_LINKS)) { + filesStream.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } + } + + private List findScenarios(String projectCode, boolean fetchSteps) { + File scenariosDirectory = Paths.get(projectsPath, projectCode, "scenarios").toFile(); + if (!scenariosDirectory.exists()) { + return Collections.emptyList(); + } + File[] directories = scenariosDirectory.listFiles(File::isDirectory); + if (directories == null) { + return Collections.emptyList(); + } + List scenarios = new ArrayList<>(); + for (File directory : directories) { + File scenarioYml = new File(directory, SCENARIO_YML_FILENAME); + if (scenarioYml.exists()) { + try { + scenarios.add(loadScenarioFromFiles(directory, null, fetchSteps)); + } catch (IOException e) { + log.error("Read file " + scenarioYml.getAbsolutePath(), e); + } + } else { + File[] innerFileList = directory.listFiles(File::isDirectory); + if (innerFileList != null) { + for (File scenarioYmlInGroup : innerFileList) { + if (new File(scenarioYmlInGroup, SCENARIO_YML_FILENAME).exists()) { + try { + scenarios.add(loadScenarioFromFiles(scenarioYmlInGroup, directory.getName(), fetchSteps)); + } catch (IOException | ReaderException e) { + log.error("Read file {} {}", scenarioYmlInGroup, e); + } + } + } + } + } + } + return scenarios; + } + + private boolean checkSteps(Scenario scenario, String relativeUrl) { + return scenario.getStepList() + .stream() + .anyMatch(s -> containsIgnoreCase(s.getRelativeUrl(), relativeUrl)); + } + +} diff --git a/atf-application/src/main/java/ru/bsc/test/autotester/ro/ExpectedMqRequestRo.java b/atf-application/src/main/java/ru/bsc/test/autotester/ro/ExpectedMqRequestRo.java index 29772a8b..2c4ea91e 100644 --- a/atf-application/src/main/java/ru/bsc/test/autotester/ro/ExpectedMqRequestRo.java +++ b/atf-application/src/main/java/ru/bsc/test/autotester/ro/ExpectedMqRequestRo.java @@ -23,10 +23,12 @@ import lombok.Getter; import lombok.Setter; +import java.io.Serializable; + @Getter @Setter @ApiModel(description = "Request to MQ stub that need to check") -public class ExpectedMqRequestRo { +public class ExpectedMqRequestRo implements Serializable { @ApiModelProperty("Unique MQ request code") private String code; @ApiModelProperty("Name of service being tested queue, which define in sourceQueueName param of the properties.yml") diff --git a/atf-application/src/main/java/ru/bsc/test/autotester/ro/HeaderItemRo.java b/atf-application/src/main/java/ru/bsc/test/autotester/ro/HeaderItemRo.java index 222e36ba..3346faef 100644 --- a/atf-application/src/main/java/ru/bsc/test/autotester/ro/HeaderItemRo.java +++ b/atf-application/src/main/java/ru/bsc/test/autotester/ro/HeaderItemRo.java @@ -23,10 +23,12 @@ import lombok.Getter; import lombok.Setter; +import java.io.Serializable; + @Getter @Setter @ApiModel(description = "HTTP header") -public class HeaderItemRo { +public class HeaderItemRo implements Serializable { @ApiModelProperty("Name of header") String headerName; @ApiModelProperty("Value of header") diff --git a/atf-application/src/main/java/ru/bsc/test/autotester/ro/MqMockResponseRo.java b/atf-application/src/main/java/ru/bsc/test/autotester/ro/MqMockResponseRo.java index b952094c..311e03f2 100644 --- a/atf-application/src/main/java/ru/bsc/test/autotester/ro/MqMockResponseRo.java +++ b/atf-application/src/main/java/ru/bsc/test/autotester/ro/MqMockResponseRo.java @@ -23,6 +23,8 @@ import lombok.Getter; import lombok.Setter; +import java.io.Serializable; + /** * Created by smakarov * 21.05.2018 15:39 @@ -30,7 +32,7 @@ @Getter @Setter @ApiModel(description = "Response from MQ stub") -public class MqMockResponseRo { +public class MqMockResponseRo implements Serializable { @ApiModelProperty("Response message text") private String responseBody; @ApiModelProperty("Name of the destination queue") diff --git a/atf-application/src/main/java/ru/bsc/test/autotester/ro/MqMockRo.java b/atf-application/src/main/java/ru/bsc/test/autotester/ro/MqMockRo.java index aab47c1f..67928914 100644 --- a/atf-application/src/main/java/ru/bsc/test/autotester/ro/MqMockRo.java +++ b/atf-application/src/main/java/ru/bsc/test/autotester/ro/MqMockRo.java @@ -23,12 +23,13 @@ import lombok.Getter; import lombok.Setter; +import java.io.Serializable; import java.util.List; @Getter @Setter @ApiModel(description = "Responses which MQ stub returns") -public class MqMockRo { +public class MqMockRo implements Serializable { @ApiModelProperty("Unique response code") private String code; @ApiModelProperty("Name of service being tested queue, which define in sourceQueueName param of the properties.yml") diff --git a/atf-application/src/main/java/ru/bsc/test/autotester/ro/NameValuePropertyRo.java b/atf-application/src/main/java/ru/bsc/test/autotester/ro/NameValuePropertyRo.java index 0a069dac..4c81c688 100644 --- a/atf-application/src/main/java/ru/bsc/test/autotester/ro/NameValuePropertyRo.java +++ b/atf-application/src/main/java/ru/bsc/test/autotester/ro/NameValuePropertyRo.java @@ -23,10 +23,12 @@ import lombok.Getter; import lombok.Setter; +import java.io.Serializable; + @Getter @Setter @ApiModel(description = "Property which is send with message body to queue") -public class NameValuePropertyRo { +public class NameValuePropertyRo implements Serializable { @ApiModelProperty("Name of property") private String name; @ApiModelProperty("value of property") diff --git a/atf-application/src/main/java/ru/bsc/test/autotester/ro/RequestDataRo.java b/atf-application/src/main/java/ru/bsc/test/autotester/ro/RequestDataRo.java index abedc999..e61aafb1 100644 --- a/atf-application/src/main/java/ru/bsc/test/autotester/ro/RequestDataRo.java +++ b/atf-application/src/main/java/ru/bsc/test/autotester/ro/RequestDataRo.java @@ -23,10 +23,12 @@ import lombok.Getter; import lombok.Setter; +import java.io.Serializable; + @Getter @Setter @ApiModel(description = "Actual request made while step executing") -public class RequestDataRo { +public class RequestDataRo implements Serializable { @ApiModelProperty("Actual request body text") private String requestBody; @ApiModelProperty("Actual response body text") diff --git a/atf-application/src/main/java/ru/bsc/test/autotester/ro/ScenarioVariableFromMqRequestRo.java b/atf-application/src/main/java/ru/bsc/test/autotester/ro/ScenarioVariableFromMqRequestRo.java index 7ed49a9a..cd165c42 100644 --- a/atf-application/src/main/java/ru/bsc/test/autotester/ro/ScenarioVariableFromMqRequestRo.java +++ b/atf-application/src/main/java/ru/bsc/test/autotester/ro/ScenarioVariableFromMqRequestRo.java @@ -22,9 +22,11 @@ import io.swagger.annotations.ApiModelProperty; import lombok.Data; +import java.io.Serializable; + @Data @ApiModel(description = "Scenario variable definition which need to save from request to MQ stub") -public class ScenarioVariableFromMqRequestRo { +public class ScenarioVariableFromMqRequestRo implements Serializable { @ApiModelProperty("Name of service being tested queue, which define in sourceQueueName param of the properties.yml") private String sourceQueue; @ApiModelProperty("XPath according to which needs to save variable value") diff --git a/atf-application/src/main/java/ru/bsc/test/autotester/ro/ScenarioVariableFromServiceRequestRo.java b/atf-application/src/main/java/ru/bsc/test/autotester/ro/ScenarioVariableFromServiceRequestRo.java index 962b79f2..9e86cfc2 100644 --- a/atf-application/src/main/java/ru/bsc/test/autotester/ro/ScenarioVariableFromServiceRequestRo.java +++ b/atf-application/src/main/java/ru/bsc/test/autotester/ro/ScenarioVariableFromServiceRequestRo.java @@ -20,8 +20,10 @@ import lombok.Data; +import java.io.Serializable; + @Data -public class ScenarioVariableFromServiceRequestRo { +public class ScenarioVariableFromServiceRequestRo implements Serializable { private String scenarioVariableName; private String expression; private String matchingType; diff --git a/atf-application/src/main/java/ru/bsc/test/autotester/ro/StepResultRo.java b/atf-application/src/main/java/ru/bsc/test/autotester/ro/StepResultRo.java index f037d2fb..a876f641 100644 --- a/atf-application/src/main/java/ru/bsc/test/autotester/ro/StepResultRo.java +++ b/atf-application/src/main/java/ru/bsc/test/autotester/ro/StepResultRo.java @@ -24,6 +24,7 @@ import lombok.Setter; import ru.bsc.test.autotester.diff.Diff; +import java.io.Serializable; import java.util.List; /** @@ -32,7 +33,7 @@ @Getter @Setter @ApiModel(description = "Result of step execution") -public class StepResultRo { +public class StepResultRo implements Serializable { @ApiModelProperty("Random testId") private String testId; @ApiModelProperty("Step which has been executed") diff --git a/atf-commons/pom.xml b/atf-commons/pom.xml index 3d8a67db..19bec878 100644 --- a/atf-commons/pom.xml +++ b/atf-commons/pom.xml @@ -98,6 +98,53 @@ 1.8 + + + org.codehaus.mojo + findbugs-maven-plugin + 3.0.5 + + Max + + medium + + true + FindReturnRef,RuntimeExceptionCapture + + + + analyze-compile + compile + + check + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/atf-commons/src/main/java/ru/bsc/test/at/util/YamlUtils.java b/atf-commons/src/main/java/ru/bsc/test/at/util/YamlUtils.java index d643d8ca..317c95c3 100644 --- a/atf-commons/src/main/java/ru/bsc/test/at/util/YamlUtils.java +++ b/atf-commons/src/main/java/ru/bsc/test/at/util/YamlUtils.java @@ -61,8 +61,9 @@ public static boolean saveToFile(T data, Path path) { public static T loadAs(File fileName, Class type) throws IOException { Representer representer = new Representer(); representer.getPropertyUtils().setSkipMissingProperties(true); - try (FileReader fileReader = new FileReader(fileName)) { - return new Yaml(representer).loadAs(fileReader, type); + try (FileInputStream fileStream = new FileInputStream(fileName); + InputStreamReader reader = new InputStreamReader(fileStream, StandardCharsets.UTF_8)) { + return new Yaml(representer).loadAs(reader, type); } } diff --git a/atf-executor/pom.xml b/atf-executor/pom.xml index 52f4fc9a..38c7fc58 100644 --- a/atf-executor/pom.xml +++ b/atf-executor/pom.xml @@ -189,6 +189,53 @@ + + + org.codehaus.mojo + findbugs-maven-plugin + 3.0.5 + + Max + + medium + + true + FindReturnRef,RuntimeExceptionCapture + + + + analyze-compile + compile + + check + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/exception/ComparisonException.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/exception/ComparisonException.java index 362c3418..4476196b 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/exception/ComparisonException.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/exception/ComparisonException.java @@ -1,45 +1,45 @@ -/* - * Copyright 2018 BSC Msc, LLC - * - * This file is part of the AuTe Framework project - * - * 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 ru.bsc.test.at.executor.exception; - -import org.xmlunit.diff.Diff; - -/** - * Created by smakarov - * 20.02.2018 10:52 - */ -public class ComparisonException extends RuntimeException { - public ComparisonException(Diff diff, String expectedRequest, String actualRequest) { - super(String.format( - "Service request error (request differences):%s\n\tExpected: %s\n\tActual: %s\n", - diff, - expectedRequest, - actualRequest - )); - } - - public ComparisonException(String diff, String expectedRequest, String actualRequest) { - super(String.format( - "Service request error (request differences):%s\n\tExpected: %s\n\tActual: %s\n", - diff, - expectedRequest, - actualRequest - )); - } -} +/* + * Copyright 2018 BSC Msc, LLC + * + * This file is part of the AuTe Framework project + * + * 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 ru.bsc.test.at.executor.exception; + +import org.xmlunit.diff.Diff; + +/** + * Created by smakarov + * 20.02.2018 10:52 + */ +public class ComparisonException extends RuntimeException { + public ComparisonException(Diff diff, String expectedRequest, String actualRequest) { + super(String.format( + "Service request error (request differences):%s%n\tExpected: %s%n\tActual: %s%n", + diff, + expectedRequest, + actualRequest + )); + } + + public ComparisonException(String diff, String expectedRequest, String actualRequest) { + super(String.format( + "Service request error (request differences):%s%n\tExpected: %s%n\tActual: %s%n", + diff, + expectedRequest, + actualRequest + )); + } +} diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/exception/InvalidNumberOfMockRequests.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/exception/InvalidNumberOfMockRequests.java index d524faa1..1f108989 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/exception/InvalidNumberOfMockRequests.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/exception/InvalidNumberOfMockRequests.java @@ -16,8 +16,6 @@ package ru.bsc.test.at.executor.exception; -import java.util.List; -import java.util.stream.Collectors; /** * Created by smakarov diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/IgnoreTagsDifferenceEvaluator.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/IgnoreTagsDifferenceEvaluator.java index 1ec896a5..448dd6c8 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/IgnoreTagsDifferenceEvaluator.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/IgnoreTagsDifferenceEvaluator.java @@ -1,197 +1,195 @@ -/* - * Copyright 2018 BSC Msc, LLC - * - * This file is part of the AuTe Framework project - * - * 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 ru.bsc.test.at.executor.helper; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xmlunit.diff.Comparison; -import org.xmlunit.diff.ComparisonResult; -import org.xmlunit.diff.ComparisonType; -import org.xmlunit.diff.DifferenceEvaluator; -import ru.bsc.test.at.executor.validation.MaskComparator; - -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static org.xmlunit.diff.ComparisonType.*; - -/** - * Created by rmalyshev date: 30.11.12 - * - */ -class IgnoreTagsDifferenceEvaluator implements DifferenceEvaluator { - - private static final String TAG_REGEX = "\\s*(\\w+)\\s*\\(\\s*(\\w+)\\s*=\\s*([\\w#]+)\\s*\\)"; - private static final Set COMPARISON_TYPES = Collections.unmodifiableSet(EnumSet.of( - NAMESPACE_PREFIX, - NAMESPACE_URI, - NO_NAMESPACE_SCHEMA_LOCATION, - SCHEMA_LOCATION - )); - - private Set ignoredTags = new HashSet<>(); - - IgnoreTagsDifferenceEvaluator(Set ignoredTags) { - if (ignoredTags != null) { - this.ignoredTags = ignoredTags; - } - } - - private boolean isXSIType(Node node) { - return node.getNodeType() == Node.ATTRIBUTE_NODE && - node.getLocalName().compareTo("type") == 0 && - Objects.equals(node.getNamespaceURI(), "http://www.w3.org/2001/XMLSchema-instance"); - } - - private String getNameSpaceFromPrefix(Node node) { - final int beginIndex = node.getNodeValue().indexOf(':'); - if (beginIndex == -1) { - return ""; - } - return node.lookupNamespaceURI(node.getNodeValue().substring(0, beginIndex)); - } - - private String getNameWithoutPrefix(Node controlNode) { - final int beginIndex = controlNode.getNodeValue().indexOf(':'); - if (beginIndex == -1) { - return controlNode.getNodeValue(); - } - return controlNode.getNodeValue().substring(beginIndex); - } - - @Override - public ComparisonResult evaluate(Comparison comparison, ComparisonResult outcome) { - if (outcome == ComparisonResult.EQUAL) { - return outcome; - } - if (outcome == ComparisonResult.DIFFERENT && checkToDifferent(comparison)) { - return ComparisonResult.EQUAL; - } - - final Node controlNode = comparison.getControlDetails().getTarget(); - final Node testNode = comparison.getTestDetails().getTarget(); - - if (comparison.getType() == ComparisonType.ATTR_VALUE && isXSIType(controlNode) && isXSIType(testNode)) { - if (getNameSpaceFromPrefix(controlNode).compareTo(getNameSpaceFromPrefix(testNode)) != 0) { - return ComparisonResult.DIFFERENT; - } - - String withoutPrefixControl = getNameWithoutPrefix(controlNode); - String withoutPrefixTest = getNameWithoutPrefix(testNode); - if (withoutPrefixControl.compareTo(withoutPrefixTest) == 0) { - return ComparisonResult.EQUAL; - } - } - - if(controlNode == null || ignoredTags == null) { - return outcome; - } - - if (checkControlNode(controlNode)) { - return ComparisonResult.EQUAL; - } - - // *ignore* check - if (testNode != null && MaskComparator.compare(controlNode.getTextContent(), testNode.getTextContent())) { - return ComparisonResult.EQUAL; - } - - return outcome; - } - - private boolean checkControlNode(Node controlNode) { - Pattern pattern = Pattern.compile(TAG_REGEX); - for (String ignoredTag : this.ignoredTags) { - if (isControlNodeTag(ignoredTag, controlNode)) { - return true; - } - Matcher matcher = pattern.matcher(ignoredTag); - if(!matcher.find()) { - continue; - } - - String parent = matcher.group(1); - String childKey = matcher.group(2); - String childValue = matcher.group(3); - NodeList childNodes = getNodeList(controlNode, parent, childKey); - if (childNodes == null) { - continue; - } - for (int i = 0; i < childNodes.getLength(); i++) { - if (!(childNodes.item(i) instanceof Element) || - !((Element) childNodes.item(i)).getTagName().equals(childKey)) { - continue; - } - - Element childElement = (Element) childNodes.item(i); - if (childElement.getTagName().equals(childKey) && - childElement.getTextContent().equals(childValue)) { - return true; - } - } - } - return false; - } - - private NodeList getNodeList(Node controlNode, String parent, String childKey) { - if(!(controlNode.getParentNode() instanceof Element)) { - return null; - } - - Element controlElement = (Element) controlNode.getParentNode(); - if(!(controlElement.getParentNode() instanceof Element)) { - return null; - } - - Element parentElement = (Element) controlElement.getParentNode(); - if(!parentElement.getTagName().equals(parent) || !parentElement.hasChildNodes()) { - return null; - } - - NodeList childNodes = parentElement.getElementsByTagName(childKey); - if(childNodes.getLength() <= 0) { - return null; - } - return childNodes; - } - - private boolean isControlNodeTag(String ignoredTag, Node controlNode) { - if (!ignoredTag.matches(TAG_REGEX)) { - if (controlNode.getParentNode() instanceof Element) { - Element element = (Element) controlNode.getParentNode(); - return ignoredTag.equals(element.getTagName()); - } - } - return false; - } - - private boolean checkToDifferent(Comparison comparison) { - ComparisonType comparisonType = comparison.getType(); - if (ATTR_VALUE.equals(comparisonType) || ATTR_NAME_LOOKUP.equals(comparisonType)) { - Attr target = (Attr) comparison.getControlDetails().getTarget(); - String parentNodeName = target.getOwnerElement().getLocalName(); - return ignoredTags.contains(parentNodeName); - } - - return COMPARISON_TYPES.contains(comparisonType); - } -} +/* + * Copyright 2018 BSC Msc, LLC + * + * This file is part of the AuTe Framework project + * + * 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 ru.bsc.test.at.executor.helper; + +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xmlunit.diff.Comparison; +import org.xmlunit.diff.ComparisonResult; +import org.xmlunit.diff.ComparisonType; +import org.xmlunit.diff.DifferenceEvaluator; +import ru.bsc.test.at.executor.validation.MaskComparator; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.xmlunit.diff.ComparisonType.*; + +/** + * Created by rmalyshev date: 30.11.12 + * + */ +class IgnoreTagsDifferenceEvaluator implements DifferenceEvaluator { + + private static final String TAG_REGEX = "\\s*(\\w+)\\s*\\(\\s*(\\w+)\\s*=\\s*([\\w#]+)\\s*\\)"; + private static final Set COMPARISON_TYPES = Collections.unmodifiableSet(EnumSet.of( + NAMESPACE_PREFIX, + NAMESPACE_URI, + NO_NAMESPACE_SCHEMA_LOCATION, + SCHEMA_LOCATION + )); + + private Set ignoredTags = new HashSet<>(); + + IgnoreTagsDifferenceEvaluator(Set ignoredTags) { + if (ignoredTags != null) { + this.ignoredTags = ignoredTags; + } + } + + private boolean isXSIType(Node node) { + return node.getNodeType() == Node.ATTRIBUTE_NODE && + node.getLocalName().compareTo("type") == 0 && + Objects.equals(node.getNamespaceURI(), "http://www.w3.org/2001/XMLSchema-instance"); + } + + private String getNameSpaceFromPrefix(Node node) { + final int beginIndex = node.getNodeValue().indexOf(':'); + if (beginIndex == -1) { + return ""; + } + return node.lookupNamespaceURI(node.getNodeValue().substring(0, beginIndex)); + } + + private String getNameWithoutPrefix(Node controlNode) { + final int beginIndex = controlNode.getNodeValue().indexOf(':'); + if (beginIndex == -1) { + return controlNode.getNodeValue(); + } + return controlNode.getNodeValue().substring(beginIndex); + } + + @Override + public ComparisonResult evaluate(Comparison comparison, ComparisonResult outcome) { + if (outcome == ComparisonResult.EQUAL) { + return outcome; + } + if (outcome == ComparisonResult.DIFFERENT && checkToDifferent(comparison)) { + return ComparisonResult.EQUAL; + } + + final Node controlNode = comparison.getControlDetails().getTarget(); + final Node testNode = comparison.getTestDetails().getTarget(); + + if (comparison.getType() == ATTR_VALUE && isXSIType(controlNode) && isXSIType(testNode)) { + if (getNameSpaceFromPrefix(controlNode).compareTo(getNameSpaceFromPrefix(testNode)) != 0) { + return ComparisonResult.DIFFERENT; + } + + String withoutPrefixControl = getNameWithoutPrefix(controlNode); + String withoutPrefixTest = getNameWithoutPrefix(testNode); + if (withoutPrefixControl.compareTo(withoutPrefixTest) == 0) { + return ComparisonResult.EQUAL; + } + } + + if(controlNode == null || ignoredTags == null) { + return outcome; + } + + if (checkControlNode(controlNode)) { + return ComparisonResult.EQUAL; + } + + // *ignore* check + if (testNode != null && MaskComparator.compare(controlNode.getTextContent(), testNode.getTextContent())) { + return ComparisonResult.EQUAL; + } + + return outcome; + } + + private boolean checkControlNode(Node controlNode) { + Pattern pattern = Pattern.compile(TAG_REGEX); + for (String ignoredTag : this.ignoredTags) { + if (isControlNodeTag(ignoredTag, controlNode)) { + return true; + } + Matcher matcher = pattern.matcher(ignoredTag); + if(!matcher.find()) { + continue; + } + + String parent = matcher.group(1); + String childKey = matcher.group(2); + String childValue = matcher.group(3); + NodeList childNodes = getNodeList(controlNode, parent, childKey); + if (childNodes == null) { + continue; + } + for (int i = 0; i < childNodes.getLength(); i++) { + if (!(childNodes.item(i) instanceof Element) || + !((Element) childNodes.item(i)).getTagName().equals(childKey)) { + continue; + } + + Element childElement = (Element) childNodes.item(i); + if (childElement.getTagName().equals(childKey) && + childElement.getTextContent().equals(childValue)) { + return true; + } + } + } + return false; + } + + private NodeList getNodeList(Node controlNode, String parent, String childKey) { + if(!(controlNode.getParentNode() instanceof Element)) { + return null; + } + + Element controlElement = (Element) controlNode.getParentNode(); + if(!(controlElement.getParentNode() instanceof Element)) { + return null; + } + + Element parentElement = (Element) controlElement.getParentNode(); + if(!parentElement.getTagName().equals(parent) || !parentElement.hasChildNodes()) { + return null; + } + + NodeList childNodes = parentElement.getElementsByTagName(childKey); + if(childNodes.getLength() <= 0) { + return null; + } + return childNodes; + } + + private boolean isControlNodeTag(String ignoredTag, Node controlNode) { + if (!ignoredTag.matches(TAG_REGEX) && controlNode.getParentNode() instanceof Element) { + Element element = (Element) controlNode.getParentNode(); + return ignoredTag.equals(element.getTagName()); + } + return false; + } + + private boolean checkToDifferent(Comparison comparison) { + ComparisonType comparisonType = comparison.getType(); + if (ATTR_VALUE.equals(comparisonType) || ATTR_NAME_LOOKUP.equals(comparisonType)) { + Attr target = (Attr) comparison.getControlDetails().getTarget(); + String parentNodeName = target.getOwnerElement().getLocalName(); + return ignoredTags.contains(parentNodeName); + } + + return COMPARISON_TYPES.contains(comparisonType); + } +} diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/MqMockHelper.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/MqMockHelper.java index d66961b5..d4bf6a4d 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/MqMockHelper.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/MqMockHelper.java @@ -82,6 +82,7 @@ public void assertMqRequests(WireMockAdmin mqMockerAdmin, String testId, Step st }catch (JsonParseException ex) { // DO NOTING (не во всех wiremock есть mq) + log.error("Error parsing: ", ex.getMessage()); } return; } diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/client/impl/http/ClientHttpRequest.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/client/impl/http/ClientHttpRequest.java index e6469cc5..e746ab18 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/client/impl/http/ClientHttpRequest.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/client/impl/http/ClientHttpRequest.java @@ -26,43 +26,60 @@ /** * @author Pavel Golovkin */ -@AllArgsConstructor public class ClientHttpRequest implements ClientRequest { - protected final String url; - protected final Object body; - protected final HTTPMethod method; - protected final Map headers; - protected final String testId; - protected final String testIdHeaderName; + protected final String url; + protected final Object body; + protected final HTTPMethod method; + protected final Map headers; + protected final String testId; + protected final String testIdHeaderName; + protected final boolean useResponseAsBase64; - @Override - public String getResource() { - return url; + public ClientHttpRequest(String url, Object body, HTTPMethod method, Map headers, String testId, String testIdHeaderName) { + this(url, body, method, headers, testId, testIdHeaderName, false); } - @Override - public Object getBody() { - return body; + public ClientHttpRequest(String url, Object body, HTTPMethod method, Map headers, String testId, String testIdHeaderName, boolean useResponseAsBase64) { + this.url = url; + this.body = body; + this.method = method; + this.headers = headers; + this.testId = testId; + this.testIdHeaderName = testIdHeaderName; + this.useResponseAsBase64 = useResponseAsBase64; } @Override - public Map getHeaders() { - return headers; - } + public String getResource() { + return url; + } - @Override - public String getTestId() { - return testId; - } + @Override + public Object getBody() { + return body; + } - @Override - public String getTestIdHeaderName() { - return testIdHeaderName; - } + @Override + public Map getHeaders() { + return headers; + } - public HTTPMethod getMethod() { - return method; - } + @Override + public String getTestId() { + return testId; + } + + @Override + public String getTestIdHeaderName() { + return testIdHeaderName; + } + + public boolean getUseResponseAsBase64() { + return useResponseAsBase64; + } + public HTTPMethod getMethod() { + return method; + } } diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/client/impl/http/HttpClient.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/client/impl/http/HttpClient.java index fd4cdef3..e17a1705 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/client/impl/http/HttpClient.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/client/impl/http/HttpClient.java @@ -50,6 +50,7 @@ import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.*; +import java.util.Map.Entry; import java.util.stream.Collectors; /** @@ -74,10 +75,10 @@ public List getCookies() { public ClientCommonResponse request(ClientHttpRequest request) throws Exception { if (request instanceof ClientHttpRequestWithVariables) { return executeWithScenarioVariables((ClientHttpRequestWithVariables) request); - } else if (request instanceof ClientHttpRequest) { + } else if (request != null) { // every request is ClientHttpRequest! return executeWithoutScenarioVariables(request); } else { - throw new Exception("Unsupported request " + request.getClass()); + throw new Exception("Unsupported request"); } } @@ -163,12 +164,10 @@ private void setHeaders(HttpRequestBase request, Map headers) { if (headers == null || headers.isEmpty()) { return; } - for (Object key: headers.keySet()) { - if (key != null) { - String keyStr = (String) key; - if (StringUtils.isNotEmpty(keyStr)) { - request.addHeader(keyStr, (String) headers.get(key)); - } + for (Object entryObj : headers.entrySet()) { + Entry entry = (Entry)entryObj; + if (StringUtils.isNotEmpty(entry.getKey())) { + request.addHeader(entry.getKey(), entry.getValue()); } } } diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/client/impl/mq/MqClient.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/client/impl/mq/MqClient.java index c3bc5027..befa1ece 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/client/impl/mq/MqClient.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/helper/client/impl/mq/MqClient.java @@ -30,6 +30,7 @@ import javax.jms.Message; import javax.jms.TextMessage; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; @@ -91,7 +92,7 @@ public ClientCommonResponse waitMessage(String queueName, Long timeout, String t BytesMessage bytesMessage = (BytesMessage) message; byte[] data = new byte[(int) bytesMessage.getBodyLength()]; bytesMessage.readBytes(data); - return new ClientCommonResponse(0, new String(data), null); + return new ClientCommonResponse(0, new String(data, StandardCharsets.UTF_8), null); } else if (message instanceof TextMessage) { return new ClientCommonResponse(0, ((TextMessage) message).getText(), null); } else { @@ -109,7 +110,7 @@ public void close() throws IOException { } } - class ClientVoidResponse implements ClientResponse { + static class ClientVoidResponse implements ClientResponse { @Override public int getStatusCode() { return 0; diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/model/AbstractModel.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/model/AbstractModel.java index 7224d814..d4e3c123 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/model/AbstractModel.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/model/AbstractModel.java @@ -1,26 +1,28 @@ -/* - * Copyright 2018 BSC Msc, LLC - * - * This file is part of the AuTe Framework project - * - * 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 ru.bsc.test.at.executor.model; - -/** - * Created by sdoroshin on 31.10.2017. - * - */ -public interface AbstractModel { -} +/* + * Copyright 2018 BSC Msc, LLC + * + * This file is part of the AuTe Framework project + * + * 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 ru.bsc.test.at.executor.model; + +import java.io.Serializable; + +/** + * Created by sdoroshin on 31.10.2017. + * + */ +public interface AbstractModel extends Serializable { +} diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/model/CodeAccessible.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/model/CodeAccessible.java index 2220f9cd..c5f04149 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/model/CodeAccessible.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/model/CodeAccessible.java @@ -1,35 +1,36 @@ -/* - * Copyright 2018 BSC Msc, LLC - * - * This file is part of the AuTe Framework project - * - * 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 ru.bsc.test.at.executor.model; - -import java.util.UUID; - -/** - * Created by smakarov - * 20.03.2018 10:38 - */ -public interface CodeAccessible { - String getCode(); - - void setCode(String code); - - default void generateCode() { - setCode(UUID.randomUUID().toString()); - } -} +/* + * Copyright 2018 BSC Msc, LLC + * + * This file is part of the AuTe Framework project + * + * 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 ru.bsc.test.at.executor.model; + +import java.io.Serializable; +import java.util.UUID; + +/** + * Created by smakarov + * 20.03.2018 10:38 + */ +public interface CodeAccessible extends Serializable { + String getCode(); + + void setCode(String code); + + default void generateCode() { + setCode(UUID.randomUUID().toString()); + } +} diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/model/HeaderItem.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/model/HeaderItem.java index a833d8ea..c2495f3c 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/model/HeaderItem.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/model/HeaderItem.java @@ -1,38 +1,40 @@ -/* - * Copyright 2018 BSC Msc, LLC - * - * This file is part of the AuTe Framework project - * - * 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 ru.bsc.test.at.executor.model; - -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class HeaderItem { - String headerName; - String headerValue; - String compareType; - - public HeaderItem copy() { - HeaderItem copy = new HeaderItem(); - copy.setCompareType(getCompareType()); - copy.setHeaderValue(getHeaderValue()); - copy.setHeaderName(getHeaderName()); - return copy; - } -} +/* + * Copyright 2018 BSC Msc, LLC + * + * This file is part of the AuTe Framework project + * + * 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 ru.bsc.test.at.executor.model; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; + +@Getter +@Setter +public class HeaderItem implements Serializable { + String headerName; + String headerValue; + String compareType; + + public HeaderItem copy() { + HeaderItem copy = new HeaderItem(); + copy.setCompareType(getCompareType()); + copy.setHeaderValue(getHeaderValue()); + copy.setHeaderName(getHeaderName()); + return copy; + } +} diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/model/MqMessage.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/model/MqMessage.java index efa2bfa8..1388c9f2 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/model/MqMessage.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/model/MqMessage.java @@ -1,59 +1,58 @@ -/* - * Copyright 2018 BSC Msc, LLC - * - * This file is part of the AuTe Framework project - * - * 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 ru.bsc.test.at.executor.model; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.Getter; -import lombok.Setter; -import org.apache.commons.lang3.StringUtils; - -import java.io.Serializable; -import java.util.LinkedList; -import java.util.List; - -/** - * Created by smakarov - * 20.04.2018 12:10 - */ -@Getter -@Setter -public class MqMessage implements Serializable, AbstractModel { - private static final long serialVersionUID = -2284237307005166339L; - - private String queueName; - private String message; - private String messageFile; - private List properties = new LinkedList<>(); - - public MqMessage copy() { - MqMessage copy = new MqMessage(); - copy.setQueueName(getQueueName()); - copy.setMessage(getMessage()); - if (getProperties() != null) { - copy.setProperties(new LinkedList<>()); - properties.forEach(p -> copy.getProperties().add(p.copy())); - } - return copy; - } - - @JsonIgnore - public boolean isEmpty() { - return StringUtils.isEmpty(this.queueName) || StringUtils.isEmpty(this.message); - } -} +/* + * Copyright 2018 BSC Msc, LLC + * + * This file is part of the AuTe Framework project + * + * 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 ru.bsc.test.at.executor.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; + +import java.util.LinkedList; +import java.util.List; + +/** + * Created by smakarov + * 20.04.2018 12:10 + */ +@Getter +@Setter +public class MqMessage implements AbstractModel { + private static final long serialVersionUID = -2284237307005166339L; + + private String queueName; + private String message; + private String messageFile; + private List properties = new LinkedList<>(); + + public MqMessage copy() { + MqMessage copy = new MqMessage(); + copy.setQueueName(getQueueName()); + copy.setMessage(getMessage()); + if (getProperties() != null) { + copy.setProperties(new LinkedList<>()); + properties.forEach(p -> copy.getProperties().add(p.copy())); + } + return copy; + } + + @JsonIgnore + public boolean isEmpty() { + return StringUtils.isEmpty(this.queueName) || StringUtils.isEmpty(this.message); + } +} diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/model/MqMockResponse.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/model/MqMockResponse.java index b3bdde2c..a9f8008f 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/model/MqMockResponse.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/model/MqMockResponse.java @@ -1,41 +1,43 @@ -/* - * Copyright 2018 BSC Msc, LLC - * - * This file is part of the AuTe Framework project - * - * 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 ru.bsc.test.at.executor.model; - -import lombok.Getter; -import lombok.Setter; - -/** - * Created by smakarov - * 21.05.2018 15:34 - */ -@Getter -@Setter -public class MqMockResponse { - private String responseBody; - private String destinationQueueName; - private String responseFile; - - public MqMockResponse copy() { - MqMockResponse copy = new MqMockResponse(); - copy.setDestinationQueueName(getDestinationQueueName()); - copy.setResponseBody(getResponseBody()); - return copy; - } -} +/* + * Copyright 2018 BSC Msc, LLC + * + * This file is part of the AuTe Framework project + * + * 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 ru.bsc.test.at.executor.model; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; + +/** + * Created by smakarov + * 21.05.2018 15:34 + */ +@Getter +@Setter +public class MqMockResponse implements Serializable { + private String responseBody; + private String destinationQueueName; + private String responseFile; + + public MqMockResponse copy() { + MqMockResponse copy = new MqMockResponse(); + copy.setDestinationQueueName(getDestinationQueueName()); + copy.setResponseBody(getResponseBody()); + return copy; + } +} diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/model/NameValueProperty.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/model/NameValueProperty.java index d027f6fc..2447494c 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/model/NameValueProperty.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/model/NameValueProperty.java @@ -1,36 +1,38 @@ -/* - * Copyright 2018 BSC Msc, LLC - * - * This file is part of the AuTe Framework project - * - * 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 ru.bsc.test.at.executor.model; - -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class NameValueProperty { - private String name; - private String value; - - public NameValueProperty copy() { - NameValueProperty property = new NameValueProperty(); - property.setName(getName()); - property.setValue(getValue()); - return property; - } -} +/* + * Copyright 2018 BSC Msc, LLC + * + * This file is part of the AuTe Framework project + * + * 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 ru.bsc.test.at.executor.model; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; + +@Getter +@Setter +public class NameValueProperty implements Serializable{ + private String name; + private String value; + + public NameValueProperty copy() { + NameValueProperty property = new NameValueProperty(); + property.setName(getName()); + property.setValue(getValue()); + return property; + } +} diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/model/SqlData.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/model/SqlData.java index a1113eea..c82a4120 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/model/SqlData.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/model/SqlData.java @@ -1,40 +1,38 @@ -/* - * Copyright 2018 BSC Msc, LLC - * - * This file is part of the AuTe Framework project - * - * 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 ru.bsc.test.at.executor.model; - -import lombok.Data; - -import java.io.Serializable; - -@Data -public class SqlData implements Serializable, AbstractModel { - private static final long serialVersionUID = -5297373310164570345L; - - private String sql; - private String sqlSavedParameter; - private SqlResultType sqlReturnType = SqlResultType.MAP; - - public SqlData copy() { - SqlData sqlData = new SqlData(); - sqlData.setSql(getSql()); - sqlData.setSqlSavedParameter(getSqlSavedParameter()); - sqlData.setSqlReturnType(getSqlReturnType()); - return sqlData; - } -} +/* + * Copyright 2018 BSC Msc, LLC + * + * This file is part of the AuTe Framework project + * + * 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 ru.bsc.test.at.executor.model; + +import lombok.Data; + +@Data +public class SqlData implements AbstractModel { + private static final long serialVersionUID = -5297373310164570345L; + + private String sql; + private String sqlSavedParameter; + private SqlResultType sqlReturnType = SqlResultType.MAP; + + public SqlData copy() { + SqlData sqlData = new SqlData(); + sqlData.setSql(getSql()); + sqlData.setSqlSavedParameter(getSqlSavedParameter()); + sqlData.setSqlReturnType(getSqlReturnType()); + return sqlData; + } +} diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/mq/RabbitMqManager.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/mq/RabbitMqManager.java index 45f82251..618c3679 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/mq/RabbitMqManager.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/mq/RabbitMqManager.java @@ -1,129 +1,130 @@ -/* - * Copyright 2018 BSC Msc, LLC - * - * This file is part of the AuTe Framework project - * - * 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 ru.bsc.test.at.executor.mq; - -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.QueueingConsumer; -import com.rabbitmq.jms.client.message.RMQTextMessage; -import lombok.extern.slf4j.Slf4j; - -import javax.jms.Connection; -import javax.jms.JMSException; -import javax.jms.Message; -import java.io.IOException; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.HashMap; -import java.util.Map; - -// Это реализация для работы с RabbitMQ. -// Сообщения не отправляются в очередь через стандартные интерфейсы JMS, если у очереди указан параметр x-dead-letter-exchange - -@Slf4j -class RabbitMqManager extends AbstractMqManager { - - private final com.rabbitmq.client.Connection senderConnection; - - RabbitMqManager(String host, int port, String username, String password) throws JMSException { - - try { - ConnectionFactory connectionFactory = new ConnectionFactory(); - connectionFactory.setHost(host); - connectionFactory.setPort(port); - connectionFactory.setUsername(username); - connectionFactory.setPassword(password); - senderConnection = connectionFactory.newConnection(); - } catch (Exception e) { - log.error("{}", e); - throw new RuntimeException(e); - } - } - - @Override - public void sendTextMessage(String queueName, String message, Map properties, String testIdHeaderName, String testId) throws Exception { - Channel channel = senderConnection.createChannel(); - - AMQP.BasicProperties.Builder propertiesBuilder = new AMQP.BasicProperties().builder(); - - Map headers = new HashMap<>(); - - if (properties != null) { - properties.forEach((name, value) -> { - String stringValue = value instanceof String ? (String) value : null; - if ("messageId".equals(name)) { - propertiesBuilder.messageId(stringValue); - } else if ("contentType".equals(name)) { - propertiesBuilder.contentType(stringValue); - } else if ("contentEncoding".equals(name)) { - propertiesBuilder.contentEncoding(stringValue); - } else if ("correlationId".equals(name)) { - propertiesBuilder.correlationId(stringValue); - } else if ("replyTo".equals(name)) { - propertiesBuilder.replyTo(stringValue); - } else if ("timestamp".equals(name)) { - try { - DateFormat formatter = new SimpleDateFormat("dd-MM-yyyy,HH:mm:ss SSS"); - propertiesBuilder.timestamp(formatter.parse(stringValue)); - } catch (ParseException e) { - log.error("{}", e); - } - } else { - headers.put(name, value); - } - }); - } - - channel.basicPublish("", queueName, propertiesBuilder.headers(headers).build(), message.getBytes()); - channel.close(); - } - - @Override - Connection getConnection() { - throw new UnsupportedOperationException("getConnection unsupported in RabbitMqManager. RabbitMQ using custom implementation."); - } - - @Override - public void close() throws IOException { - senderConnection.close(); - } - - @Override - public Message waitMessage(String queueName, Long timeoutMs, String testIdHeaderName, String testId) throws JMSException { - try { - Channel channel = senderConnection.createChannel(); - final QueueingConsumer consumer = new QueueingConsumer(channel); - channel.basicConsume(queueName, true, consumer); - - QueueingConsumer.Delivery delivery = consumer.nextDelivery(timeoutMs); - if (delivery != null) { - channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); - RMQTextMessage message = new RMQTextMessage(); - message.setText(new String(delivery.getBody())); - return message; - } - return null; - } catch (IOException | InterruptedException e) { - log.error("RabbitMQ waitMessage error: {}", e); - } - return null; - } -} +/* + * Copyright 2018 BSC Msc, LLC + * + * This file is part of the AuTe Framework project + * + * 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 ru.bsc.test.at.executor.mq; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.QueueingConsumer; +import com.rabbitmq.jms.client.message.RMQTextMessage; +import lombok.extern.slf4j.Slf4j; + +import javax.jms.Connection; +import javax.jms.JMSException; +import javax.jms.Message; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.Map; + +// Это реализация для работы с RabbitMQ. +// Сообщения не отправляются в очередь через стандартные интерфейсы JMS, если у очереди указан параметр x-dead-letter-exchange + +@Slf4j +class RabbitMqManager extends AbstractMqManager { + + private final com.rabbitmq.client.Connection senderConnection; + + RabbitMqManager(String host, int port, String username, String password) throws JMSException { + + try { + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost(host); + connectionFactory.setPort(port); + connectionFactory.setUsername(username); + connectionFactory.setPassword(password); + senderConnection = connectionFactory.newConnection(); + } catch (Exception e) { + log.error("{}", e); + throw new RuntimeException(e); + } + } + + @Override + public void sendTextMessage(String queueName, String message, Map properties, String testIdHeaderName, String testId) throws Exception { + Channel channel = senderConnection.createChannel(); + + AMQP.BasicProperties.Builder propertiesBuilder = new AMQP.BasicProperties().builder(); + + Map headers = new HashMap<>(); + + if (properties != null) { + properties.forEach((name, value) -> { + String stringValue = value instanceof String ? (String) value : null; + if ("messageId".equals(name)) { + propertiesBuilder.messageId(stringValue); + } else if ("contentType".equals(name)) { + propertiesBuilder.contentType(stringValue); + } else if ("contentEncoding".equals(name)) { + propertiesBuilder.contentEncoding(stringValue); + } else if ("correlationId".equals(name)) { + propertiesBuilder.correlationId(stringValue); + } else if ("replyTo".equals(name)) { + propertiesBuilder.replyTo(stringValue); + } else if ("timestamp".equals(name)) { + try { + DateFormat formatter = new SimpleDateFormat("dd-MM-yyyy,HH:mm:ss SSS"); + propertiesBuilder.timestamp(formatter.parse(stringValue)); + } catch (ParseException e) { + log.error("{}", e); + } + } else { + headers.put(name, value); + } + }); + } + + channel.basicPublish("", queueName, propertiesBuilder.headers(headers).build(), message.getBytes(StandardCharsets.UTF_8)); + channel.close(); + } + + @Override + Connection getConnection() { + throw new UnsupportedOperationException("getConnection unsupported in RabbitMqManager. RabbitMQ using custom implementation."); + } + + @Override + public void close() throws IOException { + senderConnection.close(); + } + + @Override + public Message waitMessage(String queueName, Long timeoutMs, String testIdHeaderName, String testId) throws JMSException { + try { + Channel channel = senderConnection.createChannel(); + final QueueingConsumer consumer = new QueueingConsumer(channel); + channel.basicConsume(queueName, true, consumer); + + QueueingConsumer.Delivery delivery = consumer.nextDelivery(timeoutMs); + if (delivery != null) { + channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); + RMQTextMessage message = new RMQTextMessage(); + message.setText(new String(delivery.getBody(), StandardCharsets.UTF_8)); + return message; + } + return null; + } catch (IOException | InterruptedException e) { + log.error("RabbitMQ waitMessage error: {}", e); + } + return null; + } +} diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/ExecutorUtils.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/ExecutorUtils.java index f15c52cf..d2b7ccfc 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/ExecutorUtils.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/ExecutorUtils.java @@ -201,7 +201,7 @@ static void executeSql(Connection connection, Step step, Map sce List queryList = new LinkedList<>(); stepResult.setSqlQueryList(queryList); for (SqlData sqlData : step.getSqlDataList()) { - if (StringUtils.isNotEmpty(sqlData.getSql()) && StringUtils.isNotEmpty(sqlData.getSqlSavedParameter())) { + if (isNotEmpty(sqlData.getSql()) && isNotEmpty(sqlData.getSqlSavedParameter())) { String query = evaluateExpressions(sqlData.getSql(), scenarioVariables); queryList.add(query); try (NamedParameterStatement statement = new NamedParameterStatement(connection, query)) { diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/MqStepExecutor.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/MqStepExecutor.java index 063c62d8..fa7a8df6 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/MqStepExecutor.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/MqStepExecutor.java @@ -70,9 +70,9 @@ public void execute(WireMockAdmin wireMockAdmin, Connection connection, Stand st for (int repetitionCounter = 0; repetitionCounter < numberRepetitions; repetitionCounter++) { StepRequester stepRequester; if (step.getUsePolling()) { - stepRequester = new MqPollingStepRequester(stepResult, step, requestBody, testId, project, mqClient, scenarioVariables, projectPath); + stepRequester = new MqPollingStepRequester(stepResult, step, requestBody, testId, project, mqClient, scenarioVariables); } else { - stepRequester = new MqSimpleStepRequester(stepResult, step, requestBody, testId, project, mqClient, scenarioVariables, projectPath); + stepRequester = new MqSimpleStepRequester(stepResult, step, requestBody, testId, project, mqClient, scenarioVariables); } stepRequester.request(); } diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/RestStepExecutor.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/RestStepExecutor.java index 95b93d73..6f2eea65 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/RestStepExecutor.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/RestStepExecutor.java @@ -91,7 +91,7 @@ public void execute(WireMockAdmin wireMockAdmin, Connection connection, Stand st for (int repetitionCounter = 0; repetitionCounter < numberRepetitions; repetitionCounter++) { // COM-123 Timeout - if (step.getTimeoutMs() != null && !step.getTimeoutMs().isEmpty() && (step.isTimeoutEachRepetition())) { + if (step.getTimeoutMs() != null && !step.getTimeoutMs().isEmpty() && step.isTimeoutEachRepetition()) { delayUtilities.delay(parseLongOrVariable(scenarioVariables, step.getTimeoutMs(), 0)); } diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/MqAbstractStepRequester.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/MqAbstractStepRequester.java index 69f037f4..01fb3c77 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/MqAbstractStepRequester.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/MqAbstractStepRequester.java @@ -48,9 +48,8 @@ public abstract class MqAbstractStepRequester implements StepRequester { protected String requestBody; protected String testId; protected Project project; - protected MqClient mqClient; + private MqClient mqClient; protected Map scenarioVariables; - protected String projectPath; protected abstract ClientResponse call() throws Exception; diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/MqPollingStepRequester.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/MqPollingStepRequester.java index 165a1217..2d14b850 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/MqPollingStepRequester.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/MqPollingStepRequester.java @@ -35,8 +35,8 @@ @Slf4j public class MqPollingStepRequester extends MqAbstractStepRequester { - public MqPollingStepRequester(StepResult stepResult, Step step, String requestBody, String testId, Project project, MqClient mqClient, Map scenarioVariables, String projectPath) { - super(stepResult, step, requestBody, testId, project, mqClient, scenarioVariables, projectPath); + public MqPollingStepRequester(StepResult stepResult, Step step, String requestBody, String testId, Project project, MqClient mqClient, Map scenarioVariables) { + super(stepResult, step, requestBody, testId, project, mqClient, scenarioVariables); } @Override diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/MqSimpleStepRequester.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/MqSimpleStepRequester.java index b940a5e0..80719ce6 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/MqSimpleStepRequester.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/MqSimpleStepRequester.java @@ -31,8 +31,8 @@ */ public class MqSimpleStepRequester extends MqAbstractStepRequester { - public MqSimpleStepRequester(StepResult stepResult, Step step, String requestBody, String testId, Project project, MqClient mqClient, Map scenarioVariables, String projectPath) { - super(stepResult, step, requestBody, testId, project, mqClient, scenarioVariables, projectPath); + public MqSimpleStepRequester(StepResult stepResult, Step step, String requestBody, String testId, Project project, MqClient mqClient, Map scenarioVariables) { + super(stepResult, step, requestBody, testId, project, mqClient, scenarioVariables); } @Override diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/RequesterUtils.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/RequesterUtils.java index 214bf8b5..0f8e550e 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/RequesterUtils.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/RequesterUtils.java @@ -147,7 +147,7 @@ static boolean tryUsePolling(Step step, ClientResponse clientResponse) { } boolean retry = true; try { - if (StringUtils.isNotEmpty(content) && JsonPath.read(content, step.getPollingJsonXPath()) != null) { + if (isNotEmpty(content) && JsonPath.read(content, step.getPollingJsonXPath()) != null) { log.info("Required attribute for polling found in path {}. Stop polling", step.getPollingJsonXPath()); retry = false; } @@ -175,8 +175,8 @@ static long calculateNextPollingDelay(long prevDelay) { private static void jsonComparing(String expectedResponse, String responseContent, String jsonCompareMode) throws Exception { log.debug("Json comparing {} {} {}", expectedResponse, responseContent, jsonCompareMode); - if ((StringUtils.isNotEmpty(expectedResponse) || StringUtils.isNotEmpty(responseContent)) && - (!responseContent.equals(expectedResponse))) { + if ((isNotEmpty(expectedResponse) || isNotEmpty(responseContent)) && + !responseContent.equals(expectedResponse)) { try { JSONAssert.assertEquals( expectedResponse == null ? "" : expectedResponse.replaceAll(" ", " "), diff --git a/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/RestAbstractStepRequester.java b/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/RestAbstractStepRequester.java index ee172563..52f5fa78 100644 --- a/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/RestAbstractStepRequester.java +++ b/atf-executor/src/main/java/ru/bsc/test/at/executor/step/executor/requester/RestAbstractStepRequester.java @@ -86,7 +86,7 @@ public void request() throws Exception { // 6.2. Проверить код статуса ответа Integer expectedStatusCode = step.getExpectedStatusCode(); log.debug("Expected status is {} and actual status is {}", expectedStatusCode, responseData.getStatusCode()); - if ((expectedStatusCode != null) && (expectedStatusCode != responseData.getStatusCode())) { + if (expectedStatusCode != null && expectedStatusCode != responseData.getStatusCode()) { throw new Exception(String.format( "Expected status code: %d. Actual status code: %d", expectedStatusCode, diff --git a/atf-wiremock/pom.xml b/atf-wiremock/pom.xml index 3a2e7d69..b75e2687 100644 --- a/atf-wiremock/pom.xml +++ b/atf-wiremock/pom.xml @@ -180,6 +180,55 @@ org.springframework.boot spring-boot-maven-plugin + + + org.codehaus.mojo + findbugs-maven-plugin + 3.0.5 + + Max + + medium + + true + FindReturnRef,RuntimeExceptionCapture + + + + analyze-compile + compile + + check + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/atf-wiremock/src/main/java/ru/bsc/test/at/mock/filter/utils/MultipartToBase64ConverterServletRequest.java b/atf-wiremock/src/main/java/ru/bsc/test/at/mock/filter/utils/MultipartToBase64ConverterServletRequest.java index 9faeccfb..1d4a58ca 100644 --- a/atf-wiremock/src/main/java/ru/bsc/test/at/mock/filter/utils/MultipartToBase64ConverterServletRequest.java +++ b/atf-wiremock/src/main/java/ru/bsc/test/at/mock/filter/utils/MultipartToBase64ConverterServletRequest.java @@ -133,7 +133,7 @@ public ServletInputStream getInputStream() throws IOException { if (rawData == null) { initialize(); } - return new ByteArrayServletInputStream((rawData)); + return new ByteArrayServletInputStream(rawData); } @Override @@ -141,7 +141,7 @@ public BufferedReader getReader() throws IOException { if (rawData == null) { initialize(); } - return new BufferedReader(new InputStreamReader(new ByteArrayServletInputStream((rawData)))); + return new BufferedReader(new InputStreamReader(new ByteArrayServletInputStream(rawData), StandardCharsets.UTF_8)); } private MultiMap getParams(){ @@ -224,7 +224,9 @@ private void processEvalField(List fileItems, ConvertedRequestBody con if(configProperties.isBoundaryStaticEnabled()) { changeMultipartHeaderBoundary(convertedRequestBody.getStaticBoundary()); } - rawData = convertedRequestBody.getAllDataBody().toString().replace(EVAL_FIELD, convertedRequestBody.getStaticBoundary()).getBytes(); + rawData = convertedRequestBody.getAllDataBody() + .toString() + .replace(EVAL_FIELD, convertedRequestBody.getStaticBoundary()).getBytes(StandardCharsets.UTF_8); } } @@ -271,9 +273,9 @@ private String buildPartMultipartRequest(String staticBoundary, FileItem fileIte byte[] file = fileItem.get(); if(!org.apache.commons.codec.binary.Base64.isArrayByteBase64(file)) { - multipart.append(new String(Base64.getEncoder().encode(file))); + multipart.append(new String(Base64.getEncoder().encode(file), StandardCharsets.UTF_8)); } else { - multipart.append(new String(file)); + multipart.append(new String(file, StandardCharsets.UTF_8)); } multipart.append(NEW_LINE); if(file.length == 0 && countHeaderIterm == 0) { @@ -282,7 +284,7 @@ private String buildPartMultipartRequest(String staticBoundary, FileItem fileIte return multipart.toString(); } - private class ByteArrayServletInputStream extends ServletInputStream { + private static class ByteArrayServletInputStream extends ServletInputStream { private final ByteArrayInputStream inputStream; diff --git a/atf-wiremock/src/main/java/ru/bsc/test/at/mock/mq/worker/ActiveMQWorker.java b/atf-wiremock/src/main/java/ru/bsc/test/at/mock/mq/worker/ActiveMQWorker.java index 50f991b0..76c8ba59 100644 --- a/atf-wiremock/src/main/java/ru/bsc/test/at/mock/mq/worker/ActiveMQWorker.java +++ b/atf-wiremock/src/main/java/ru/bsc/test/at/mock/mq/worker/ActiveMQWorker.java @@ -98,15 +98,17 @@ public void run() { for (MockMessageResponse mockResponse : mockMessage.getResponses()) { byte[] response; - if (StringUtils.isNotEmpty(mockResponse.getResponseBody())) { - response = transformer.transform(mockMessage.getGuid(), stringBody, extractor.createContext(message), mockResponse.getResponseBody()).getBytes(); - } else if (StringUtils.isNotEmpty(mockMessage.getHttpUrl())) { + if (isNotEmpty(mockResponse.getResponseBody())) { + response = transformer.transform(mockMessage.getGuid(), stringBody, extractor.createContext(message), mockResponse.getResponseBody()).getBytes(StandardCharsets.UTF_8); + } else if (isNotEmpty(mockMessage.getHttpUrl())) { try (HttpClient httpClient = new HttpClient()) { - response = httpClient.sendPost(mockMessage.getHttpUrl(), new String(message.getContent().getData(), StandardCharsets.UTF_8), getTestIdHeaderName(), testId).getBytes(); + response = httpClient.sendPost(mockMessage.getHttpUrl(), + new String(message.getContent().getData(), StandardCharsets.UTF_8), + getTestIdHeaderName(), testId).getBytes(StandardCharsets.UTF_8); } mockedRequest.setHttpRequestUrl(mockMessage.getHttpUrl()); } else { - response = stringBody.getBytes(); + response = stringBody.getBytes(StandardCharsets.UTF_8); } mockedRequest.setDestinationQueue(mockResponse.getDestinationQueueName()); diff --git a/atf-wiremock/src/main/java/ru/bsc/test/at/mock/mq/worker/IbmMQWorker.java b/atf-wiremock/src/main/java/ru/bsc/test/at/mock/mq/worker/IbmMQWorker.java index ec847cd5..e5697cda 100644 --- a/atf-wiremock/src/main/java/ru/bsc/test/at/mock/mq/worker/IbmMQWorker.java +++ b/atf-wiremock/src/main/java/ru/bsc/test/at/mock/mq/worker/IbmMQWorker.java @@ -77,7 +77,7 @@ public void run() { connectionFactory.setHostName(getBrokerUrl()); connectionFactory.setPort(port); connectionFactory.setTransportType(WMQ_CM_CLIENT); - if (StringUtils.isNotEmpty(channel)) { + if (isNotEmpty(channel)) { connectionFactory.setChannel(channel); } } catch (JMSException e) { @@ -134,15 +134,15 @@ public void run() { for (MockMessageResponse mockResponse : mockMessage.getResponses()) { byte[] response; - if (StringUtils.isNotEmpty(mockResponse.getResponseBody())) { - response = transformer.transform(mockMessage.getGuid(), stringBody, extractor.createContext(receivedMessage), mockResponse.getResponseBody()).getBytes(); - } else if (StringUtils.isNotEmpty(mockMessage.getHttpUrl())) { + if (isNotEmpty(mockResponse.getResponseBody())) { + response = transformer.transform(mockMessage.getGuid(), stringBody, extractor.createContext(receivedMessage), mockResponse.getResponseBody()).getBytes(StandardCharsets.UTF_8); + } else if (isNotEmpty(mockMessage.getHttpUrl())) { try (HttpClient httpClient = new HttpClient()) { - response = httpClient.sendPost(mockMessage.getHttpUrl(), stringBody, getTestIdHeaderName(), testId).getBytes(); + response = httpClient.sendPost(mockMessage.getHttpUrl(), stringBody, getTestIdHeaderName(), testId).getBytes(StandardCharsets.UTF_8); } mockedRequest.setHttpRequestUrl(mockMessage.getHttpUrl()); } else { - response = stringBody.getBytes(); + response = stringBody.getBytes(StandardCharsets.UTF_8); } String destinationQueue = isNotEmpty(mockResponse.getDestinationQueueName()) @@ -162,7 +162,7 @@ public void run() { consumer.close(); session.close(); connection.close(); - } catch (Exception e) { + } catch (JMSException e) { log.error("Caught:", e); } } @@ -186,7 +186,7 @@ private void sendMessage(Session session, Message receivedMessage, MockedRequest // Переслать сообщение в очередь-назначение producer.send(newMessage); producer.close(); - log.info(" [x] Send >>> {} '{}'", destinationQueue, messageBody); + log.info(" [x] Send >>> {} '{}'", destinationQueue, messageBody, StandardCharsets.UTF_8); } else { log.info(" [x] Send >>> ***black hole***"); } diff --git a/atf-wiremock/src/main/java/ru/bsc/test/at/mock/mq/worker/RabbitMQWorker.java b/atf-wiremock/src/main/java/ru/bsc/test/at/mock/mq/worker/RabbitMQWorker.java index 03abc371..4bfb4b68 100644 --- a/atf-wiremock/src/main/java/ru/bsc/test/at/mock/mq/worker/RabbitMQWorker.java +++ b/atf-wiremock/src/main/java/ru/bsc/test/at/mock/mq/worker/RabbitMQWorker.java @@ -18,11 +18,7 @@ package ru.bsc.test.at.mock.mq.worker; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.DefaultConsumer; -import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.*; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.Buffer; import org.apache.commons.lang3.StringUtils; @@ -44,7 +40,7 @@ public class RabbitMQWorker extends AbstractMqWorker { private final Buffer fifo; private Channel channelFrom; private Channel channelTo; - private com.rabbitmq.client.Connection connection; + private Connection connection; private int port; private VelocityTransformer velocityTransformer; @@ -115,10 +111,10 @@ public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProp byte[] response; if (StringUtils.isNotEmpty(mockResponse.getResponseBody())) { - response = velocityTransformer.transform(mockMessage.getGuid(), stringBody, null, mockResponse.getResponseBody()).getBytes(); + response = velocityTransformer.transform(mockMessage.getGuid(), stringBody, null, mockResponse.getResponseBody()).getBytes(StandardCharsets.UTF_8); } else if (StringUtils.isNotEmpty(mockMessage.getHttpUrl())) { try (HttpClient httpClient = new HttpClient()) { - response = httpClient.sendPost(mockMessage.getHttpUrl(), new String(body, StandardCharsets.UTF_8), getTestIdHeaderName(), testId).getBytes(); + response = httpClient.sendPost(mockMessage.getHttpUrl(), new String(body, StandardCharsets.UTF_8), getTestIdHeaderName(), testId).getBytes(StandardCharsets.UTF_8); } mockedRequest.setHttpRequestUrl(mockMessage.getHttpUrl()); } else { diff --git a/atf-wiremock/src/main/java/ru/bsc/test/at/mock/wiremock/transformers/CustomVelocityResponseTransformer.java b/atf-wiremock/src/main/java/ru/bsc/test/at/mock/wiremock/transformers/CustomVelocityResponseTransformer.java index d2265e20..f2d2fc98 100644 --- a/atf-wiremock/src/main/java/ru/bsc/test/at/mock/wiremock/transformers/CustomVelocityResponseTransformer.java +++ b/atf-wiremock/src/main/java/ru/bsc/test/at/mock/wiremock/transformers/CustomVelocityResponseTransformer.java @@ -44,6 +44,7 @@ import java.io.StringWriter; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -145,26 +146,26 @@ private byte[] parseConvertBody(String body) { // обрабатываем каждую часть multipart'a - ищем base64 for (String part : partsMultipart) { if (isNotBoundaryAndInBase64(part)) { - result = Base64.decode(part.getBytes()); + result = Base64.decode(part.getBytes(StandardCharsets.UTF_8)); } else { - result = part.getBytes(); + result = part.getBytes(StandardCharsets.UTF_8); } buffer.addAll(Arrays.asList(ArrayUtils.toObject(result))); } - result = ArrayUtils.toPrimitive((Byte[]) buffer.toArray()); + result = ArrayUtils.toPrimitive(buffer.toArray(new Byte[buffer.size()])); } else { // если не начинается с boundary - проверяем, не base64 лежит в корне - if (isBase64(body.getBytes())) { - result = Base64.decode(body.getBytes()); + if (isBase64(body.getBytes(StandardCharsets.UTF_8))) { + result = Base64.decode(body.getBytes(StandardCharsets.UTF_8)); } else { - result = body.getBytes(); + result = body.getBytes(StandardCharsets.UTF_8); } } return result; } private boolean isNotBoundaryAndInBase64(String part) { - return !part.startsWith(DOUBLE_DASH) && isBase64(part.getBytes()); + return !part.startsWith(DOUBLE_DASH) && isBase64(part.getBytes(StandardCharsets.UTF_8)); } private Boolean templateDeclared(final ResponseDefinition response) { diff --git a/atf-wiremock/src/main/java/ru/bsc/test/at/mock/wiremock/webcontextlistener/configuration/CustomWarConfiguration.java b/atf-wiremock/src/main/java/ru/bsc/test/at/mock/wiremock/webcontextlistener/configuration/CustomWarConfiguration.java index 1e5f06e3..6b2a228c 100644 --- a/atf-wiremock/src/main/java/ru/bsc/test/at/mock/wiremock/webcontextlistener/configuration/CustomWarConfiguration.java +++ b/atf-wiremock/src/main/java/ru/bsc/test/at/mock/wiremock/webcontextlistener/configuration/CustomWarConfiguration.java @@ -64,7 +64,7 @@ public Map extensionsOfType(Class extensionT properties.setProperty("resource.loader", "file"); properties.setProperty("file.resource.loader.path", "." + File.separator + "velocity"); - try (final InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(VELOCITY_PROPERTIES.getValue())) { + try (InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(VELOCITY_PROPERTIES.getValue())) { properties.load(stream); } catch (Exception e) { log.warn("Error while loading properties: {}. Using default values", VELOCITY_PROPERTIES.getValue()); diff --git a/pom.xml b/pom.xml index 77c50a3a..0066ea65 100644 --- a/pom.xml +++ b/pom.xml @@ -169,6 +169,61 @@ org.apache.maven.plugins maven-release-plugin + + org.apache.maven.plugins + maven-pmd-plugin + 3.12.0 + + + UTF-8 + 30 + 1.8 + true + + **/*StepRoMapperImpl.java + + + + target/generated-sources/annotations + + + + + analyze-compile + compile + + pmd + + + check + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.1.0 + + + + + + true + checkstyleOutput.txt + + + + + analyze-compile + compile + + checkstyle + + + + \ No newline at end of file diff --git a/rulesets/exclude-pmd.properties b/rulesets/exclude-pmd.properties new file mode 100644 index 00000000..763c69df --- /dev/null +++ b/rulesets/exclude-pmd.properties @@ -0,0 +1,17 @@ +# +# AuTe Framework project +# Copyright 2018 BSC Msc, LLC +# +# ATF project is licensed under +# The Apache 2.0 License +# http://www.apache.org/licenses/LICENSE-2.0.html +# +# For more information visit http://www.bsc-ideas.com/ru/ +# +# Files ru.bsc.test.autotester.diff.DiffMatchPatch.java, ru.bsc.test.autotester.diff.Diff.java, +# ru.bsc.test.autotester.diff.LinesToCharsResult, ru.bsc.test.autotester.diff.Operation, +# ru.bsc.test.autotester.diff.Patch +# are copied from https://github.com/google/diff-match-patch +# + +ru.bsc.test.autotester.mapper.StepRoMapperImpl=UnnecessaryFullyQualifiedName \ No newline at end of file