1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package com.finalist.util;
19
20 import com.finalist.jaggenerator.HtmlContentPopUp;
21
22 import java.io.*;
23 import java.util.*;
24
25 /***
26 * This class performs a 'diff' on two files: i.e. compares the two files and returns a result
27 * containing information about the lines that differ.
28 * <p>
29 * <b>NOTE:</b> This diff tool does not use the same algorithm as the familiar command-line diff,
30 * so the results will not always be identical.
31 *
32 * @todo The output of the diff report if hardcoded to HTML, for the time being.
33 * @todo It would be nice if performDiff() generated XML and createReport() used XSLT..
34 *
35 * @author Michael O'Connor - Finalist IT Group.
36 */
37 public class Diff {
38 private File file1;
39 private File file2;
40 private BufferedReader in1;
41 private BufferedReader in2;
42 private HashMap lineCounter = new HashMap(2);
43
44 private static final String REPORT_HEADER =
45 "<html><head><style type='text/css'>" +
46 "body, table { font-size: 9px; font-family: Verdana, Arial, Helvetica, sans-serif }" +
47 "font.file1 {color:#ff0000;}" +
48 "font.file1-code {color:#ff0000; font-family: 'courier new',monospace;}" +
49 "font.file2 {color:#0000ff;}" +
50 "font.file2-code {color:#0000ff; font-family: 'courier new',monospace;}" +
51 "</style></head><body>" +
52 "<table><tr><td width='30'><font class='file1'><</td><td>";
53
54 /***
55 * Creates a new Diff, given two files that will be compared.
56 *
57 * @param file1 the first file.
58 * @param file2 the second file.
59 * @throws IOException if either of the files doesn't exist.
60 */
61 public Diff(File file1, File file2) throws IOException {
62 this.file1 = file1;
63 this.file2 = file2;
64 if (!file1.exists()) {
65 throw new IOException("File " + file1 + " doesn't exist!");
66 }
67 if (!file2.exists()) {
68 throw new IOException("File " + file2 + " doesn't exist!");
69 }
70 in1 = new BufferedReader(new FileReader(file1));
71 in2 = new BufferedReader(new FileReader(file2));
72 lineCounter.put(in1, new Integer(1));
73 lineCounter.put(in2, new Integer(1));
74 }
75
76 /***
77 * Performs the diff on the two files specified in the constructor, returning a
78 * formatted human-readable report (HTML, in this case).
79 *
80 * @return a String representation of the diff report, or <code>null</code> if the
81 * two files were identical (excluding whitespace differences).
82 * @throws IOException if the files couldn't be read.
83 */
84 public String performDiff() throws IOException {
85 List diffLines = getDiffLines();
86 return createReport(diffLines);
87 }
88
89 /***
90 * Performs the diff and returns the result as a List of DiffConflictLine objects.
91 *
92 * @return
93 * @throws IOException if the files couldn't be read.
94 */
95 public List getDiffLines() throws IOException {
96 ArrayList diffLines = new ArrayList();
97 try {
98 DiffConflictLine line1 = nextLine(in1);
99 DiffConflictLine line2 = nextLine(in2);
100 while (!(line1.isEof() && line2.isEof())) {
101 if (line1.isEof()) {
102 diffLines.add(line2);
103 line2 = nextLine(in2);
104 continue;
105 } else if (line2.isEof()) {
106 diffLines.add(line1);
107 line1 = nextLine(in1);
108 continue;
109 }
110 if (line1.lineEquals(line2)) {
111
112 line1 = nextLine(in1);
113 line2 = nextLine(in2);
114 } else {
115 DiffConflictLine conflictLine = null;
116 if (containsLine(file2, line1.getLine(), line2.getLineNumber())) {
117
118 MatchResult betterMatch = bestMatch(line1.getLineNumber(), line2.getLineNumber());
119
120 if (betterMatch != null) {
121 diffLines.add(line1);
122 while(line1.getLineNumber() < betterMatch.endFile1Sequence) {
123 line1 = nextLine(in1);
124
125 diffLines.add(line1);
126 }
127 line1 = nextLine(in1);
128 continue;
129 } else {
130 conflictLine = line2;
131 line2 = nextLine(in2);
132 }
133 } else {
134
135 conflictLine = line1;
136 line1 = nextLine(in1);
137 }
138 diffLines.add(conflictLine);
139 }
140 }
141
142
143 } finally {
144 if (in1 != null) try { in1.close(); } catch (IOException _) {}
145 if (in2 != null) try { in2.close(); } catch (IOException _) {}
146 }
147 return diffLines;
148 }
149
150 /***
151 * Finds the longest sequence of lines in file#1 starting with line# 'count1',
152 * which matches a sequence in file#2 starting with line# 'count2'.
153 * A match is deemed valid if the distance from count1 to the start of the match is less than
154 * the length of the sequence.
155 * @param count1
156 * @param count2
157 * @return
158 */
159 private MatchResult bestMatch(int count1, int count2) {
160 int matchDistance = count1;
161 int matchLength = 0;
162 BufferedReader in1 = null;
163 BufferedReader in2 = null;
164 try {
165 in1 = new BufferedReader(new FileReader(file1));
166 in2 = new BufferedReader(new FileReader(file2));
167
168
169 for (int i = 1; i < count1; i++) {
170 in1.readLine();
171 }
172 for (int i = 1; i < count2; i++) {
173 in2.readLine();
174 }
175
176
177 String s1 = in1.readLine();
178 String s2 = in2.readLine();
179 while (s1 != null) {
180 if (s1.trim().equals(s2.trim())) {
181 break;
182 }
183 s1 = in1.readLine();
184 count1++;
185 }
186 if (s1 == null) {
187
188 return null;
189 }
190 matchDistance = count1 - matchDistance;
191
192
193 while (!(s1 == null || s2 == null) &&
194 s1.trim().equals(s2.trim())) {
195 matchLength++;
196 s1 = in1.readLine();
197 s2 = in2.readLine();
198 }
199 } catch (FileNotFoundException e) {
200
201 } catch (IOException e) {
202 e.printStackTrace();
203 } finally {
204 if (in1 != null) try { in1.close(); } catch (IOException _) {}
205 if (in2 != null) try { in2.close(); } catch (IOException _) {}
206 }
207 if (matchLength < matchDistance) {
208 return null;
209 }
210
211 return new MatchResult(matchLength, count1 - 1);
212 }
213
214 private boolean containsLine(File file, String line, int startPos) {
215 int lineCount = 0;
216 BufferedReader in = null;
217 try {
218 in = new BufferedReader(new FileReader(file));
219 String s = in.readLine();
220 lineCount++;
221 while (s != null) {
222 if (lineCount > startPos && s != null && line.trim().equals(s.trim())) {
223 return true;
224 }
225 s = in.readLine();
226 lineCount++;
227 }
228 } catch (FileNotFoundException e) {
229
230 } catch (IOException e) {
231 e.printStackTrace();
232 } finally {
233 if (in != null) try { in.close(); } catch (IOException _) {}
234 }
235 return false;
236 }
237
238 private DiffConflictLine nextLine(BufferedReader reader) throws IOException {
239 String line = "";
240 int lineNumber = currentLineNumber(reader);
241 while (line != null && "".equals(line.trim())) {
242 line = reader.readLine();
243 lineNumber++;
244 }
245 lineCounter.put(reader, new Integer(lineNumber));
246 return line == null ? DiffConflictLine.EOF : new DiffConflictLine(reader == in1, lineNumber - 1, line);
247 }
248
249 private int currentLineNumber(BufferedReader reader) {
250 return ((Integer) lineCounter.get(reader)).intValue();
251 }
252
253 private String createReport(List diffLines) throws IOException {
254 if (diffLines.isEmpty()) {
255 return null;
256 }
257 StringBuffer report = new StringBuffer(REPORT_HEADER);
258 report.append(file1.getCanonicalPath());
259 report.append("</font></td></tr><tr><td width='30'><font class='file2'>></td><td>");
260 report.append(file2.getCanonicalPath()).append("</font></td></tr></table><br>");
261 ArrayList temp = new ArrayList();
262 for (int i = 0; i < diffLines.size(); i++) {
263 DiffConflictLine line = (DiffConflictLine) diffLines.get(i);
264 DiffConflictLine next = (i == diffLines.size() - 1) ? null : (DiffConflictLine) diffLines.get(i + 1);
265 if (line.precedes(next)) {
266 DiffConflictLine last = lastLine(temp);
267 if (last != null && !last.precedes(line)) {
268 reportLineGroup(report, temp);
269 }
270 temp.add(line);
271 continue;
272 }
273 if (temp.isEmpty()) {
274 reportSingleLine(report, line);
275 } else {
276 DiffConflictLine last = lastLine(temp);
277 if (last.precedes(line)) {
278 temp.add(line);
279 } else {
280 reportLineGroup(report, temp);
281 temp.add(line);
282 }
283 }
284 }
285 if (temp.size() > 1) {
286 reportLineGroup(report, temp);
287 } else if (temp.size() == 1) {
288 reportSingleLine(report, (DiffConflictLine) temp.get(0));
289 }
290
291 report.append("</body></html>");
292 return report.toString();
293 }
294
295 private void reportSingleLine(StringBuffer report, DiffConflictLine line) {
296 report.append("<font class=");
297 report.append(line.isFirstFile() ? "'file1'" : "'file2'");
298 report.append(">").append(line.getLineNumber()).append("</font><br>");
299 report.append(line.toString());
300 }
301
302 private void reportLineGroup(StringBuffer report, ArrayList temp) {
303 DiffConflictLine last = lastLine(temp);
304 DiffConflictLine first = (DiffConflictLine) temp.get(0);
305 report.append("<font class=");
306 report.append(first.isFirstFile() ? "'file1'" : "'file2'");
307 report.append(">").append(first.getLineNumber()).append(',').append(last.getLineNumber());
308 report.append("</font><br>");
309 Iterator j = temp.iterator();
310 while (j.hasNext()) {
311 DiffConflictLine groupedLine = (DiffConflictLine) j.next();
312 report.append(groupedLine.toString());
313 }
314 temp.clear();
315 }
316
317 private DiffConflictLine lastLine(List group) {
318 return (group == null || group.isEmpty()) ? null : (DiffConflictLine) group.get(group.size() - 1);
319 }
320
321 /***
322 * For testing purposes - I distance off with JUnit, honestly - but it's too slow!
323 *
324 * @param args [1] file#1, [2] file#2.
325 */
326 public static void main(String[] args) {
327 try {
328 Diff diff = new Diff(new File(args[0]), new File(args[1]));
329 System.out.println(diff.performDiff());
330 } catch (IOException e) {
331 e.printStackTrace();
332 }
333 }
334
335 }
336
337 class MatchResult {
338 public MatchResult(int length, int endFile1Sequence) {
339 this.length = length;
340 this.endFile1Sequence = endFile1Sequence;
341 }
342
343 int endFile1Sequence;
344 int length;
345
346 public String toString() {
347 return "Match: end#1=" + endFile1Sequence + ", length=" + length;
348 }
349
350 }