| /******************************************************************************* |
| * Copyright (c) 2000, 2011 IBM Corporation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.compare.internal.core.patch; |
| |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import org.eclipse.compare.patch.IFilePatchResult; |
| import org.eclipse.compare.patch.IHunk; |
| import org.eclipse.compare.patch.PatchConfiguration; |
| import org.eclipse.core.runtime.Assert; |
| |
| /** |
| * A Hunk describes a range of changed lines and some context lines. |
| */ |
| public class Hunk implements IHunk { |
| private FilePatch2 fParent; |
| private int fOldStart, fOldLength; |
| private int fNewStart, fNewLength; |
| private String[] fLines; |
| private int hunkType; |
| private String charset = null; |
| |
| public static Hunk createHunk(FilePatch2 parent, int[] oldRange, int[] newRange, |
| List<String> lines, boolean hasLineAdditions, boolean hasLineDeletions, boolean hasContextLines) { |
| int oldStart = 0; |
| int oldLength = 0; |
| int newStart = 0; |
| int newLength = 0; |
| if (oldRange[0] > 0) |
| oldStart= oldRange[0]-1; // line number start at 0! |
| else |
| oldStart= 0; |
| oldLength= oldRange[1]; |
| if (newRange[0] > 0) |
| newStart= newRange[0]-1; // line number start at 0! |
| else |
| newStart= 0; |
| newLength= newRange[1]; |
| int hunkType = FilePatch2.CHANGE; |
| if (!hasContextLines) { |
| if (hasLineAdditions && !hasLineDeletions) { |
| hunkType = FilePatch2.ADDITION; |
| } else if (!hasLineAdditions && hasLineDeletions) { |
| hunkType = FilePatch2.DELETION; |
| } |
| } |
| return new Hunk(parent, hunkType, oldStart, oldLength, newStart, newLength, lines.toArray(new String[lines.size()])); |
| } |
| |
| public Hunk(FilePatch2 parent, int hunkType, int oldStart, int oldLength, |
| int newStart, int newLength, String[] lines) { |
| this.fParent = parent; |
| if (this.fParent != null) { |
| this.fParent.add(this); |
| } |
| this.hunkType = hunkType; |
| this.fOldLength = oldLength; |
| this.fOldStart = oldStart; |
| this.fNewLength = newLength; |
| this.fNewStart = newStart; |
| this.fLines = lines; |
| } |
| |
| public Hunk(FilePatch2 parent, Hunk toCopy) { |
| this(parent, toCopy.hunkType, toCopy.fOldStart, toCopy.fOldLength, toCopy.fNewStart, toCopy.fNewLength, toCopy.fLines); |
| } |
| |
| /* |
| * Returns the contents of this hunk. |
| * Each line starts with a control character. Their meaning is as follows: |
| * <ul> |
| * <li> |
| * '+': add the line |
| * <li> |
| * '-': delete the line |
| * <li> |
| * ' ': no change, context line |
| * </ul> |
| */ |
| public String getContent() { |
| StringBuffer sb= new StringBuffer(); |
| for (int i= 0; i < this.fLines.length; i++) { |
| String line= this.fLines[i]; |
| sb.append(line.substring(0, LineReader.length(line))); |
| sb.append('\n'); |
| } |
| return sb.toString(); |
| } |
| |
| /* |
| * Returns a descriptive String for this hunk. |
| * It is in the form old_start,old_length -> new_start,new_length. |
| */ |
| String getDescription() { |
| StringBuffer sb= new StringBuffer(); |
| sb.append(Integer.toString(this.fOldStart)); |
| sb.append(','); |
| sb.append(Integer.toString(this.fOldLength)); |
| sb.append(" -> "); //$NON-NLS-1$ |
| sb.append(Integer.toString(this.fNewStart)); |
| sb.append(','); |
| sb.append(Integer.toString(this.fNewLength)); |
| return sb.toString(); |
| } |
| |
| public String getRejectedDescription() { |
| StringBuffer sb= new StringBuffer(); |
| sb.append("@@ -"); //$NON-NLS-1$ |
| sb.append(Integer.toString(this.fOldStart)); |
| sb.append(','); |
| sb.append(Integer.toString(this.fOldLength)); |
| sb.append(" +"); //$NON-NLS-1$ |
| sb.append(Integer.toString(this.fNewStart)); |
| sb.append(','); |
| sb.append(Integer.toString(this.fNewLength)); |
| sb.append(" @@"); //$NON-NLS-1$ |
| return sb.toString(); |
| } |
| |
| public int getHunkType(boolean reverse) { |
| if (reverse) { |
| if (this.hunkType == FilePatch2.ADDITION) |
| return FilePatch2.DELETION; |
| if (this.hunkType == FilePatch2.DELETION) |
| return FilePatch2.ADDITION; |
| } |
| return this.hunkType; |
| } |
| |
| void setHunkType(int hunkType) { |
| this.hunkType = hunkType; |
| } |
| |
| public String[] getLines() { |
| return this.fLines; |
| } |
| |
| @Override |
| public String[] getUnifiedLines() { |
| String[] ret = new String[this.fLines.length]; |
| System.arraycopy(this.fLines, 0, ret, 0, this.fLines.length); |
| return ret; |
| } |
| |
| /** |
| * Set the parent of this hunk. This method |
| * should only be invoked from {@link FilePatch2#add(Hunk)} |
| * @param diff the parent of this hunk |
| */ |
| void setParent(FilePatch2 diff) { |
| if (this.fParent == diff) |
| return; |
| if (this.fParent != null) |
| this.fParent.remove(this); |
| this.fParent = diff; |
| } |
| |
| public FilePatch2 getParent() { |
| return this.fParent; |
| } |
| |
| /* |
| * Tries to apply the given hunk on the specified lines. |
| * The parameter shift is added to the line numbers given |
| * in the hunk. |
| */ |
| public boolean tryPatch(PatchConfiguration configuration, List<String> lines, int shift, int fuzz) { |
| boolean reverse = configuration.isReversed(); |
| int pos = getStart(reverse) + shift; |
| List<String> contextLines = new ArrayList<>(); |
| boolean contextLinesMatched = true; |
| boolean precedingLinesChecked = false; |
| for (int i= 0; i < this.fLines.length; i++) { |
| String s = this.fLines[i]; |
| Assert.isTrue(s.length() > 0); |
| String line = s.substring(1); |
| char controlChar = s.charAt(0); |
| |
| if (controlChar == ' ') { // context lines |
| |
| if (pos < 0 || pos >= lines.size()) |
| return false; |
| contextLines.add(line); |
| if (linesMatch(configuration, line, lines.get(pos))) { |
| pos++; |
| continue; |
| } else if (fuzz > 0) { |
| // doesn't match, use the fuzz factor |
| contextLinesMatched = false; |
| pos++; |
| continue; |
| } |
| return false; |
| } else if (isDeletedDelimeter(controlChar, reverse)) { |
| // deleted lines |
| |
| if (precedingLinesChecked && !contextLinesMatched && contextLines.size() > 0) |
| // context lines inside hunk don't match |
| return false; |
| |
| // check following context lines if exist |
| // use the fuzz factor if needed |
| if (!precedingLinesChecked |
| && !contextLinesMatched |
| && contextLines.size() >= fuzz |
| && !checkPrecedingContextLines(configuration, lines, |
| fuzz, pos, contextLines)) |
| return false; |
| // else if there is less or equal context line to the fuzz |
| // factor we ignore them all and treat as matching |
| |
| precedingLinesChecked = true; |
| contextLines.clear(); |
| contextLinesMatched = true; |
| |
| if (pos < 0 || pos >= lines.size()) // out of the file |
| return false; |
| if (linesMatch(configuration, line, lines.get(pos))) { |
| pos++; |
| continue; // line matched, continue with the next one |
| } |
| |
| // We must remove all lines at once, return false if this |
| // fails. In other words, all lines considered for deletion |
| // must be found one by one. |
| return false; |
| } else if (isAddedDelimeter(controlChar, reverse)) { |
| |
| if (precedingLinesChecked && !contextLinesMatched && contextLines.size() > 0) |
| return false; |
| |
| if (!precedingLinesChecked |
| && !contextLinesMatched |
| && contextLines.size() >= fuzz |
| && !checkPrecedingContextLines(configuration, lines, |
| fuzz, pos, contextLines)) |
| return false; |
| |
| precedingLinesChecked = true; |
| contextLines.clear(); |
| contextLinesMatched = true; |
| |
| // we don't have to do anything more for a 'try' |
| } else |
| Assert.isTrue(false, "tryPatch: unknown control character: " + controlChar); //$NON-NLS-1$ |
| } |
| |
| // check following context lines if exist |
| if (!contextLinesMatched |
| && fuzz > 0 |
| && contextLines.size() > fuzz |
| && !checkFollowingContextLines(configuration, lines, fuzz, pos, |
| contextLines)) |
| return false; |
| |
| return true; |
| } |
| |
| private boolean checkPrecedingContextLines( |
| PatchConfiguration configuration, List<String> lines, int fuzz, int pos, |
| List<String> contextLines) { |
| // ignore from the beginning |
| for (int j = fuzz; j < contextLines.size(); j++) { |
| if (!linesMatch(configuration, contextLines.get(j), |
| lines.get(pos - contextLines.size() + j))) |
| return false; |
| } |
| return true; |
| } |
| |
| private boolean checkFollowingContextLines( |
| PatchConfiguration configuration, List<String> lines, int fuzz, int pos, |
| List<String> contextLines) { |
| if (!contextLines.isEmpty()) { |
| // ignore from the end |
| for (int j = 0; j < contextLines.size() - fuzz; j++) { |
| if (!linesMatch(configuration, contextLines.get(j), |
| lines.get(pos - contextLines.size() + j))) |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| public int getStart(boolean after) { |
| if (after) { |
| return this.fNewStart; |
| } |
| return this.fOldStart; |
| } |
| |
| public void setStart(int start, boolean after) { |
| if (after) { |
| this.fNewStart = start; |
| } else { |
| this.fOldStart = start; |
| } |
| } |
| |
| public int getLength(boolean after) { |
| if (after) { |
| return this.fNewLength; |
| } |
| return this.fOldLength; |
| } |
| |
| private int getShift(boolean reverse) { |
| if (reverse) { |
| return this.fOldLength - this.fNewLength; |
| } |
| return this.fNewLength - this.fOldLength; |
| } |
| |
| int doPatch(PatchConfiguration configuration, List<String> lines, int shift, int fuzz) { |
| boolean reverse = configuration.isReversed(); |
| int pos = getStart(reverse) + shift; |
| List<String> contextLines = new ArrayList<>(); |
| boolean contextLinesMatched = true; |
| boolean precedingLinesChecked = false; |
| String lineDelimiter = getLineDelimiter(lines); |
| |
| for (int i= 0; i < this.fLines.length; i++) { |
| String s= this.fLines[i]; |
| Assert.isTrue(s.length() > 0); |
| String line= s.substring(1); |
| char controlChar= s.charAt(0); |
| if (controlChar == ' ') { |
| // context lines |
| Assert.isTrue(pos < lines.size(), "doPatch: inconsistency in context"); //$NON-NLS-1$ |
| contextLines.add(line); |
| if (linesMatch(configuration, line, lines.get(pos))) { |
| pos++; |
| continue; |
| } else if (fuzz > 0) { |
| // doesn't match, use the fuzz factor |
| contextLinesMatched = false; |
| pos++; |
| continue; |
| } |
| Assert.isTrue(false, "doPatch: context doesn't match"); //$NON-NLS-1$ |
| // pos++; |
| } else if (isDeletedDelimeter(controlChar, reverse)) { |
| // deleted lines |
| if (precedingLinesChecked && !contextLinesMatched && contextLines.size() > 0) |
| // context lines inside hunk don't match |
| Assert.isTrue(false, "doPatch: context lines inside hunk don't match"); //$NON-NLS-1$ |
| |
| // check following context lines if exist |
| // use the fuzz factor if needed |
| if (!precedingLinesChecked |
| && !contextLinesMatched |
| && contextLines.size() >= fuzz |
| && !checkPrecedingContextLines(configuration, lines, |
| fuzz, pos, contextLines)) |
| Assert.isTrue(false, "doPatch: preceding context lines don't match, even though fuzz factor has been used"); //$NON-NLS-1$; |
| // else if there is less or equal context line to the fuzz |
| // factor we ignore them all and treat as matching |
| |
| precedingLinesChecked = true; |
| contextLines.clear(); |
| contextLinesMatched = true; |
| |
| lines.remove(pos); |
| } else if (isAddedDelimeter(controlChar, reverse)) { |
| // added lines |
| if (precedingLinesChecked && !contextLinesMatched && contextLines.size() > 0) |
| Assert.isTrue(false, "doPatch: context lines inside hunk don't match"); //$NON-NLS-1$ |
| |
| if (!precedingLinesChecked |
| && !contextLinesMatched |
| && contextLines.size() >= fuzz |
| && !checkPrecedingContextLines(configuration, lines, |
| fuzz, pos, contextLines)) |
| Assert.isTrue(false, "doPatch: preceding context lines don't match, even though fuzz factor has been used"); //$NON-NLS-1$; |
| |
| precedingLinesChecked = true; |
| contextLines.clear(); |
| contextLinesMatched = true; |
| |
| // if the line contains a delimiter, use a proper one |
| if (line.length() > LineReader.length(line)) |
| line = line.substring(0, LineReader.length(line)) + lineDelimiter; |
| |
| if (getLength(reverse) == 0 && pos+1 < lines.size()) |
| lines.add(pos+1, line); |
| else |
| lines.add(pos, line); |
| pos++; |
| } else |
| Assert.isTrue(false, "doPatch: unknown control character: " + controlChar); //$NON-NLS-1$ |
| } |
| return getShift(reverse); |
| } |
| |
| private boolean isDeletedDelimeter(char controlChar, boolean reverse) { |
| return (!reverse && controlChar == '-') || (reverse && controlChar == '+'); |
| } |
| |
| private boolean isAddedDelimeter(char controlChar, boolean reverse) { |
| return (reverse && controlChar == '-') || (!reverse && controlChar == '+'); |
| } |
| |
| /* |
| * Compares two strings. |
| * If fIgnoreWhitespace is true whitespace is ignored. |
| */ |
| private boolean linesMatch(PatchConfiguration configuration, String line1, String line2) { |
| if (configuration.isIgnoreWhitespace()) |
| return stripWhiteSpace(line1).equals(stripWhiteSpace(line2)); |
| if (isIgnoreLineDelimiter()) { |
| int l1= LineReader.length(line1); |
| int l2= LineReader.length(line2); |
| if (l1 != l2) |
| return false; |
| return line1.regionMatches(0, line2, 0, l1); |
| } |
| return line1.equals(line2); |
| } |
| |
| private boolean isIgnoreLineDelimiter() { |
| return true; |
| } |
| |
| private String getLineDelimiter(List<String> lines) { |
| if (lines.size() > 0) { |
| // get a line separator from the file being patched |
| String line0 = lines.get(0); |
| return line0.substring(LineReader.length(line0)); |
| } else if (this.fLines.length > 0) { |
| // if the file doesn't exist use a line separator from the patch |
| return this.fLines[0].substring(LineReader.length(this.fLines[0])); |
| } |
| return System.getProperty("line.separator"); //$NON-NLS-1$ |
| } |
| |
| /* |
| * Returns the given string with all whitespace characters removed. |
| * Whitespace is defined by <code>Character.isWhitespace(...)</code>. |
| */ |
| private String stripWhiteSpace(String s) { |
| StringBuffer sb= new StringBuffer(); |
| int l= s.length(); |
| for (int i= 0; i < l; i++) { |
| char c= s.charAt(i); |
| if (!Character.isWhitespace(c)) |
| sb.append(c); |
| } |
| return sb.toString(); |
| } |
| |
| public String getContents(boolean isAfterState, boolean reverse) { |
| StringBuffer result= new StringBuffer(); |
| for (int i= 0; i<this.fLines.length; i++) { |
| String line= this.fLines[i]; |
| String rest= line.substring(1); |
| char c = line.charAt(0); |
| if (c == ' ') { |
| result.append(rest); |
| } else if (isDeletedDelimeter(c, reverse) && !isAfterState) { |
| result.append(rest); |
| } else if (isAddedDelimeter(c, reverse) && isAfterState) { |
| result.append(rest); |
| } |
| } |
| return result.toString(); |
| } |
| |
| @Override |
| public String getLabel() { |
| return getDescription(); |
| } |
| |
| @Override |
| public int getStartPosition() { |
| return getStart(false); |
| } |
| |
| @Override |
| public InputStream getOriginalContents() { |
| String contents = getContents(false, false); |
| return asInputStream(contents); |
| } |
| |
| @Override |
| public InputStream getPatchedContents() { |
| String contents = getContents(true, false); |
| return asInputStream(contents); |
| } |
| |
| private InputStream asInputStream(String contents) { |
| String charSet = getCharset(); |
| return FileDiffResult.asInputStream(contents, charSet); |
| } |
| |
| void setCharset(String charset) { |
| this.charset = charset; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * @deprecated This method can be called before the first attempt to apply |
| * the hunk when it is impossible to determine the encoding and |
| * in this case it always returns null. Please see |
| * {@link IFilePatchResult#getCharset()} as a proper way to |
| * obtain charset. |
| */ |
| @Deprecated |
| @Override |
| public String getCharset() { |
| return this.charset; |
| } |
| |
| } |