1 /*******************************************************************************
2  * Copyright (c) 2006, 2011 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *******************************************************************************/
14 package org.eclipse.compare.internal.core.patch;
15 
16 import java.io.ByteArrayInputStream;
17 import java.io.InputStream;
18 import java.io.UnsupportedEncodingException;
19 import java.util.ArrayList;
20 import java.util.HashMap;
21 import java.util.List;
22 import java.util.Map;
23 
24 import org.eclipse.compare.internal.core.ComparePlugin;
25 import org.eclipse.compare.internal.core.Messages;
26 import org.eclipse.compare.patch.IFilePatchResult;
27 import org.eclipse.compare.patch.IHunk;
28 import org.eclipse.compare.patch.PatchConfiguration;
29 import org.eclipse.compare.patch.ReaderCreator;
30 import org.eclipse.core.runtime.IPath;
31 import org.eclipse.core.runtime.IProgressMonitor;
32 import org.eclipse.core.runtime.NullProgressMonitor;
33 import org.eclipse.osgi.util.NLS;
34 
35 public class FileDiffResult implements IFilePatchResult {
36 	private FilePatch2 fDiff;
37 	private boolean fMatches= false;
38 	private boolean fDiffProblem;
39 	private String fErrorMessage;
40 	private Map<Hunk, HunkResult> fHunkResults = new HashMap<>();
41 	private List<String> fBeforeLines, fAfterLines;
42 	private final PatchConfiguration configuration;
43 	private String charset;
44 
FileDiffResult(FilePatch2 diff, PatchConfiguration configuration)45 	public FileDiffResult(FilePatch2 diff, PatchConfiguration configuration) {
46 		super();
47 		this.fDiff = diff;
48 		this.configuration = configuration;
49 	}
50 
getConfiguration()51 	public PatchConfiguration getConfiguration() {
52 		return this.configuration;
53 	}
54 
canApplyHunk(Hunk hunk)55 	public boolean canApplyHunk(Hunk hunk) {
56 		HunkResult result = getHunkResult(hunk);
57 		return result.isOK() && !this.fDiffProblem;
58 	}
59 
60 	/**
61 	 * Refreshes the state of the diff to {no matches, no problems} and checks to see what hunks contained
62 	 * by this Diff can actually be applied.
63 	 *
64 	 * Checks to see:
65 	 * 1) if the target file specified in fNewPath exists and is patchable
66 	 * 2) which hunks contained by this diff can actually be applied to the file
67 	 * @param content the contents being patched or <code>null</code> for an addition
68 	 * @param monitor a progress monitor or <code>null</code> if no progress monitoring is desired
69 	 */
refresh(ReaderCreator content, IProgressMonitor monitor)70 	public void refresh(ReaderCreator content, IProgressMonitor monitor) {
71 		this.fMatches= false;
72 		this.fDiffProblem= false;
73 		boolean create= false;
74 		this.charset = Utilities.getCharset(content);
75 		//If this diff is an addition, make sure that it doesn't already exist
76 		boolean exists = targetExists(content);
77 		if (this.fDiff.getDiffType(getConfiguration().isReversed()) == FilePatch2.ADDITION) {
78 			if ((!exists || isEmpty(content)) && canCreateTarget(content)) {
79 				this.fMatches= true;
80 			} else {
81 				// file already exists
82 				this.fDiffProblem= true;
83 				this.fErrorMessage= Messages.FileDiffResult_0;
84 			}
85 			create= true;
86 		} else { //This diff is not an addition, try to find a match for it
87 			//Ensure that the file described by the path exists and is modifiable
88 			if (exists) {
89 				this.fMatches= true;
90 			} else {
91 				// file doesn't exist
92 				this.fDiffProblem= true;
93 				this.fErrorMessage= Messages.FileDiffResult_1;
94 			}
95 		}
96 
97 		if (this.fDiffProblem) {
98 			// We couldn't find the target file or the patch is trying to add a
99 			// file that already exists but we need to initialize the hunk
100 			// results for display
101 			this.fBeforeLines = new ArrayList<>(getLines(content, false));
102 			this.fAfterLines = this.fMatches ? new ArrayList<>() : this.fBeforeLines;
103 			IHunk[] hunks = this.fDiff.getHunks();
104 			for (IHunk h : hunks) {
105 				Hunk hunk = (Hunk) h;
106 				hunk.setCharset(getCharset());
107 				HunkResult result = getHunkResult(hunk);
108 				result.setMatches(false);
109 			}
110 		} else {
111 			// If this diff has no problems discovered so far, try applying the patch
112 			patch(getLines(content, create), monitor);
113 		}
114 
115 		if (containsProblems()) {
116 			if (this.fMatches) {
117 				// Check to see if we have at least one hunk that matches
118 				this.fMatches = false;
119 				IHunk[] hunks = this.fDiff.getHunks();
120 				for (IHunk h : hunks) {
121 					Hunk hunk = (Hunk) h;
122 					HunkResult result = getHunkResult(hunk);
123 					if (result.isOK()) {
124 						this.fMatches = true;
125 						break;
126 					}
127 				}
128 			}
129 		}
130 	}
131 
canCreateTarget(ReaderCreator content)132 	protected boolean canCreateTarget(ReaderCreator content) {
133 		return true;
134 	}
135 
targetExists(ReaderCreator content)136 	protected boolean targetExists(ReaderCreator content) {
137 		return content != null && content.canCreateReader();
138 	}
139 
getLines(ReaderCreator content, boolean create)140 	protected List<String> getLines(ReaderCreator content, boolean create) {
141 		List<String> lines = LineReader.load(content, create);
142 		return lines;
143 	}
144 
isEmpty(ReaderCreator content)145 	protected boolean isEmpty(ReaderCreator content) {
146 		if (content == null)
147 			return true;
148 		return LineReader.load(content, false).isEmpty();
149 	}
150 
151 	/*
152 	 * Tries to patch the given lines with the specified Diff.
153 	 * Any hunk that couldn't be applied is returned in the list failedHunks.
154 	 */
patch(List<String> lines, IProgressMonitor monitor)155 	public void patch(List<String> lines, IProgressMonitor monitor) {
156 		this.fBeforeLines = new ArrayList<>();
157 		this.fBeforeLines.addAll(lines);
158 		if (getConfiguration().getFuzz() != 0) {
159 			calculateFuzz(this.fBeforeLines, monitor);
160 		}
161 		int shift= 0;
162 		IHunk[] hunks = this.fDiff.getHunks();
163 		for (IHunk h : hunks) {
164 			Hunk hunk = (Hunk) h;
165 			hunk.setCharset(getCharset());
166 			HunkResult result = getHunkResult(hunk);
167 			result.setShift(shift);
168 			if (result.patch(lines)) {
169 				shift = result.getShift();
170 			}
171 		}
172 		this.fAfterLines = lines;
173 	}
174 
getDiffProblem()175 	public boolean getDiffProblem() {
176 		return this.fDiffProblem;
177 	}
178 
179 	/**
180 	 * Returns whether this Diff has any problems
181 	 * @return true if this Diff or any of its children Hunks have a problem, false if it doesn't
182 	 */
containsProblems()183 	public boolean containsProblems() {
184 		if (this.fDiffProblem)
185 			return true;
186 		for (HunkResult result : this.fHunkResults.values()) {
187 			if (!result.isOK())
188 				return true;
189 		}
190 		return false;
191 	}
192 
getLabel()193 	public String getLabel() {
194 		String label= getTargetPath().toString();
195 		if (this.fDiffProblem)
196 			return NLS.bind(Messages.FileDiffResult_2, new String[] {label, this.fErrorMessage});
197 		return label;
198 	}
199 
200 	@Override
hasMatches()201 	public boolean hasMatches() {
202 		return this.fMatches;
203 	}
204 
205 	/**
206 	 * Return the lines of the target file with all matched hunks applied.
207 	 * @return the lines of the target file with all matched hunks applied
208 	 */
getLines()209 	public List<String> getLines() {
210 		return this.fAfterLines;
211 	}
212 
213 	/**
214 	 * Calculate the fuzz factor that will allow the most hunks to be matched.
215 	 * @param lines the lines of the target file
216 	 * @param monitor a progress monitor
217 	 * @return the fuzz factor or <code>-1</code> if no hunks could be matched
218 	 */
calculateFuzz(List<String> lines, IProgressMonitor monitor)219 	public int calculateFuzz(List<String> lines, IProgressMonitor monitor) {
220 		if (monitor == null)
221 			monitor = new NullProgressMonitor();
222 		this.fBeforeLines = new ArrayList<>(lines);
223 		// TODO: What about deletions?
224 		if (this.fDiff.getDiffType(getConfiguration().isReversed()) == FilePatch2.ADDITION) {
225 			// Additions don't need to adjust the fuzz factor
226 			// TODO: What about the after lines?
227 			return -1;
228 		}
229 		int shift= 0;
230 		int highestFuzz = -1; // the maximum fuzz factor for all hunks
231 		String name = getTargetPath() != null ? getTargetPath().lastSegment() : ""; //$NON-NLS-1$
232 		IHunk[] hunks = this.fDiff.getHunks();
233 		for (int j = 0; j < hunks.length; j++) {
234 			Hunk h = (Hunk) hunks[j];
235 			monitor.subTask(NLS.bind(Messages.FileDiffResult_3, new String[] {name, Integer.toString(j + 1)}));
236 			HunkResult result = getHunkResult(h);
237 			result.setShift(shift);
238 			int fuzz = result.calculateFuzz(lines, monitor);
239 			shift = result.getShift();
240 			if (fuzz > highestFuzz)
241 				highestFuzz = fuzz;
242 			monitor.worked(1);
243 		}
244 		this.fAfterLines = lines;
245 		return highestFuzz;
246 	}
247 
getTargetPath()248 	public IPath getTargetPath() {
249 		return this.fDiff.getStrippedPath(getConfiguration().getPrefixSegmentStripCount(), getConfiguration().isReversed());
250 	}
251 
getHunkResult(Hunk hunk)252 	private HunkResult getHunkResult(Hunk hunk) {
253 		HunkResult result = this.fHunkResults.get(hunk);
254 		if (result == null) {
255 			result = new HunkResult(this, hunk);
256 			this.fHunkResults.put(hunk, result);
257 		}
258 		return result;
259 	}
260 
getFailedHunks()261 	public List<Hunk> getFailedHunks() {
262 		List<Hunk> failedHunks = new ArrayList<>();
263 		IHunk[] hunks = this.fDiff.getHunks();
264 		for (IHunk hunk : hunks) {
265 			HunkResult result = this.fHunkResults.get(hunk);
266 			if (result != null && !result.isOK())
267 				failedHunks.add(result.getHunk());
268 		}
269 		return failedHunks;
270 	}
271 
getDiff()272 	public FilePatch2 getDiff() {
273 		return this.fDiff;
274 	}
275 
getBeforeLines()276 	public List<String> getBeforeLines() {
277 		return this.fBeforeLines;
278 	}
279 
getAfterLines()280 	public List<String> getAfterLines() {
281 		return this.fAfterLines;
282 	}
283 
getHunkResults()284 	public HunkResult[] getHunkResults() {
285 		// return hunk results in the same order as hunks are placed in file diff
286 		List<HunkResult> results = new ArrayList<>();
287 		IHunk[] hunks = this.fDiff.getHunks();
288 		for (IHunk hunk : hunks) {
289 			HunkResult result = this.fHunkResults.get(hunk);
290 			if (result != null) {
291 				results.add(result);
292 			}
293 		}
294 		return results.toArray(new HunkResult[results.size()]);
295 	}
296 
297 	@Override
getOriginalContents()298 	public InputStream getOriginalContents() {
299 		String contents = LineReader.createString(isPreserveLineDelimeters(), getBeforeLines());
300 		return asInputStream(contents, getCharset());
301 	}
302 
303 	@Override
getPatchedContents()304 	public InputStream getPatchedContents() {
305 		String contents = LineReader.createString(isPreserveLineDelimeters(), getLines());
306 		return asInputStream(contents, getCharset());
307 	}
308 
309 	@Override
getCharset()310 	public String getCharset() {
311 		return this.charset;
312 	}
313 
isPreserveLineDelimeters()314 	public boolean isPreserveLineDelimeters() {
315 		return false;
316 	}
317 
318 	@Override
getRejects()319 	public IHunk[] getRejects() {
320 		List<Hunk> failedHunks = getFailedHunks();
321 		return failedHunks.toArray(new IHunk[failedHunks.size()]);
322 	}
323 
324 	@Override
hasRejects()325 	public boolean hasRejects() {
326 		return getFailedHunks().size() > 0;
327 	}
328 
asInputStream(String contents, String charSet)329 	public static InputStream asInputStream(String contents, String charSet) {
330 		byte[] bytes = null;
331 		if (charSet != null) {
332 			try {
333 				bytes = contents.getBytes(charSet);
334 			} catch (UnsupportedEncodingException e) {
335 				ComparePlugin.log(e);
336 			}
337 		}
338 		if (bytes == null) {
339 			bytes = contents.getBytes();
340 		}
341 		return new ByteArrayInputStream(bytes);
342 	}
343 
344 }
345