001: /* Copyright (C) 2003 Finalist IT Group
002: *
003: * This file is part of JAG - the Java J2EE Application Generator
004: *
005: * JAG is free software; you can redistribute it and/or modify
006: * it under the terms of the GNU General Public License as published by
007: * the Free Software Foundation; either version 2 of the License, or
008: * (at your option) any later version.
009: * JAG is distributed in the hope that it will be useful,
010: * but WITHOUT ANY WARRANTY; without even the implied warranty of
011: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
012: * GNU General Public License for more details.
013: * You should have received a copy of the GNU General Public License
014: * along with JAG; if not, write to the Free Software
015: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
016: */
017:
018: package com.finalist.util;
019:
020: import com.finalist.jaggenerator.HtmlContentPopUp;
021:
022: import java.io.*;
023: import java.util.*;
024:
025: /**
026: * This class performs a 'diff' on two files: i.e. compares the two files and returns a result
027: * containing information about the lines that differ.
028: * <p>
029: * <b>NOTE:</b> This diff tool does not use the same algorithm as the familiar command-line diff,
030: * so the results will not always be identical.
031: *
032: * @todo The output of the diff report if hardcoded to HTML, for the time being.
033: * @todo It would be nice if performDiff() generated XML and createReport() used XSLT..
034: *
035: * @author Michael O'Connor - Finalist IT Group.
036: */
037: public class Diff {
038: private File file1;
039: private File file2;
040: private BufferedReader in1;
041: private BufferedReader in2;
042: private HashMap lineCounter = new HashMap(2);
043:
044: private static final String REPORT_HEADER = "<html><head><style type='text/css'>"
045: + "body, table { font-size: 9px; font-family: Verdana, Arial, Helvetica, sans-serif }"
046: + "font.file1 {color:#ff0000;}"
047: + "font.file1-code {color:#ff0000; font-family: 'courier new',monospace;}"
048: + "font.file2 {color:#0000ff;}"
049: + "font.file2-code {color:#0000ff; font-family: 'courier new',monospace;}"
050: + "</style></head><body>"
051: + "<table><tr><td width='30'><font class='file1'><</td><td>";
052:
053: /**
054: * Creates a new Diff, given two files that will be compared.
055: *
056: * @param file1 the first file.
057: * @param file2 the second file.
058: * @throws IOException if either of the files doesn't exist.
059: */
060: public Diff(File file1, File file2) throws IOException {
061: this .file1 = file1;
062: this .file2 = file2;
063: if (!file1.exists()) {
064: throw new IOException("File " + file1 + " doesn't exist!");
065: }
066: if (!file2.exists()) {
067: throw new IOException("File " + file2 + " doesn't exist!");
068: }
069: in1 = new BufferedReader(new FileReader(file1));
070: in2 = new BufferedReader(new FileReader(file2));
071: lineCounter.put(in1, new Integer(1));
072: lineCounter.put(in2, new Integer(1));
073: }
074:
075: /**
076: * Performs the diff on the two files specified in the constructor, returning a
077: * formatted human-readable report (HTML, in this case).
078: *
079: * @return a String representation of the diff report, or <code>null</code> if the
080: * two files were identical (excluding whitespace differences).
081: * @throws IOException if the files couldn't be read.
082: */
083: public String performDiff() throws IOException {
084: List diffLines = getDiffLines();
085: return createReport(diffLines);
086: }
087:
088: /**
089: * Performs the diff and returns the result as a List of DiffConflictLine objects.
090: *
091: * @return
092: * @throws IOException if the files couldn't be read.
093: */
094: public List getDiffLines() throws IOException {
095: ArrayList diffLines = new ArrayList();
096: try {
097: DiffConflictLine line1 = nextLine(in1);
098: DiffConflictLine line2 = nextLine(in2);
099: while (!(line1.isEof() && line2.isEof())) {
100: if (line1.isEof()) {
101: diffLines.add(line2);
102: line2 = nextLine(in2);
103: continue;
104: } else if (line2.isEof()) {
105: diffLines.add(line1);
106: line1 = nextLine(in1);
107: continue;
108: }
109: if (line1.lineEquals(line2)) {
110: //System.out.println("equals: current=" + line1.getLine() + "-x-" + line2.getLine());
111: line1 = nextLine(in1);
112: line2 = nextLine(in2);
113: } else {
114: DiffConflictLine conflictLine = null;
115: if (containsLine(file2, line1.getLine(), line2
116: .getLineNumber())) {
117: //System.out.println("a is in #2: current=" + line1.getLine() + "-x-" + line2.getLine());
118: MatchResult betterMatch = bestMatch(line1
119: .getLineNumber(), line2.getLineNumber());
120: //System.out.println("current=" + line1.getLine() + "-x-" + line2.getLine() + "\tbetterMatch? " + betterMatch);
121: if (betterMatch != null) {
122: diffLines.add(line1);
123: while (line1.getLineNumber() < betterMatch.endFile1Sequence) {
124: line1 = nextLine(in1);
125: //System.out.println(">>adding '" + line1 + "' from file#1 to conflicts");
126: diffLines.add(line1);
127: }
128: line1 = nextLine(in1);
129: continue;
130: } else {
131: conflictLine = line2;
132: line2 = nextLine(in2);
133: }
134: } else {
135: //System.out.println("a isn't in #2: current=" + line1.getLine() + "-x-" + line2.getLine());
136: conflictLine = line1;
137: line1 = nextLine(in1);
138: }
139: diffLines.add(conflictLine);
140: }
141: }
142: //new HtmlContentPopUp(null, "Diff report:", true, createReport(diffLines)).show();
143:
144: } finally {
145: if (in1 != null)
146: try {
147: in1.close();
148: } catch (IOException _) {
149: }
150: if (in2 != null)
151: try {
152: in2.close();
153: } catch (IOException _) {
154: }
155: }
156: return diffLines;
157: }
158:
159: /**
160: * Finds the longest sequence of lines in file#1 starting with line# 'count1',
161: * which matches a sequence in file#2 starting with line# 'count2'.
162: * A match is deemed valid if the distance from count1 to the start of the match is less than
163: * the length of the sequence.
164: * @param count1
165: * @param count2
166: * @return
167: */
168: private MatchResult bestMatch(int count1, int count2) {
169: int matchDistance = count1;
170: int matchLength = 0;
171: BufferedReader in1 = null;
172: BufferedReader in2 = null;
173: try {
174: in1 = new BufferedReader(new FileReader(file1));
175: in2 = new BufferedReader(new FileReader(file2));
176:
177: //1: read through upto the count1 and count2 starting points:
178: for (int i = 1; i < count1; i++) {
179: in1.readLine();
180: }
181: for (int i = 1; i < count2; i++) {
182: in2.readLine();
183: }
184:
185: //2: read through file#1 until a match is found with the line from file#2:
186: String s1 = in1.readLine();
187: String s2 = in2.readLine();
188: while (s1 != null) {
189: if (s1.trim().equals(s2.trim())) {
190: break;
191: }
192: s1 = in1.readLine();
193: count1++;
194: }
195: if (s1 == null) {
196: //no match was found!
197: return null;
198: }
199: matchDistance = count1 - matchDistance;
200:
201: //3: follow through the match until it stops:
202: while (!(s1 == null || s2 == null)
203: && s1.trim().equals(s2.trim())) {
204: matchLength++;
205: s1 = in1.readLine();
206: s2 = in2.readLine();
207: }
208: } catch (FileNotFoundException e) {
209: //this won't happen!
210: } catch (IOException e) {
211: e.printStackTrace();
212: } finally {
213: if (in1 != null)
214: try {
215: in1.close();
216: } catch (IOException _) {
217: }
218: if (in2 != null)
219: try {
220: in2.close();
221: } catch (IOException _) {
222: }
223: }
224: if (matchLength < matchDistance) {
225: return null;
226: }
227:
228: return new MatchResult(matchLength, count1 - 1);
229: }
230:
231: private boolean containsLine(File file, String line, int startPos) {
232: int lineCount = 0;
233: BufferedReader in = null;
234: try {
235: in = new BufferedReader(new FileReader(file));
236: String s = in.readLine();
237: lineCount++;
238: while (s != null) {
239: if (lineCount > startPos && s != null
240: && line.trim().equals(s.trim())) {
241: return true;
242: }
243: s = in.readLine();
244: lineCount++;
245: }
246: } catch (FileNotFoundException e) {
247: //this won't happen!
248: } catch (IOException e) {
249: e.printStackTrace();
250: } finally {
251: if (in != null)
252: try {
253: in.close();
254: } catch (IOException _) {
255: }
256: }
257: return false;
258: }
259:
260: private DiffConflictLine nextLine(BufferedReader reader)
261: throws IOException {
262: String line = "";
263: int lineNumber = currentLineNumber(reader);
264: while (line != null && "".equals(line.trim())) {
265: line = reader.readLine();
266: lineNumber++;
267: }
268: lineCounter.put(reader, new Integer(lineNumber));
269: return line == null ? DiffConflictLine.EOF
270: : new DiffConflictLine(reader == in1, lineNumber - 1,
271: line);
272: }
273:
274: private int currentLineNumber(BufferedReader reader) {
275: return ((Integer) lineCounter.get(reader)).intValue();
276: }
277:
278: private String createReport(List diffLines) throws IOException {
279: if (diffLines.isEmpty()) {
280: return null;
281: }
282: StringBuffer report = new StringBuffer(REPORT_HEADER);
283: report.append(file1.getCanonicalPath());
284: report
285: .append("</font></td></tr><tr><td width='30'><font class='file2'>></td><td>");
286: report.append(file2.getCanonicalPath()).append(
287: "</font></td></tr></table><br>");
288: ArrayList temp = new ArrayList();
289: for (int i = 0; i < diffLines.size(); i++) {
290: DiffConflictLine line = (DiffConflictLine) diffLines.get(i);
291: DiffConflictLine next = (i == diffLines.size() - 1) ? null
292: : (DiffConflictLine) diffLines.get(i + 1);
293: if (line.precedes(next)) {
294: DiffConflictLine last = lastLine(temp);
295: if (last != null && !last.precedes(line)) {
296: reportLineGroup(report, temp);
297: }
298: temp.add(line);
299: continue;
300: }
301: if (temp.isEmpty()) {
302: reportSingleLine(report, line);
303: } else {
304: DiffConflictLine last = lastLine(temp);
305: if (last.precedes(line)) {
306: temp.add(line);
307: } else {
308: reportLineGroup(report, temp);
309: temp.add(line);
310: }
311: }
312: }
313: if (temp.size() > 1) {
314: reportLineGroup(report, temp);
315: } else if (temp.size() == 1) {
316: reportSingleLine(report, (DiffConflictLine) temp.get(0));
317: }
318:
319: report.append("</body></html>");
320: return report.toString();
321: }
322:
323: private void reportSingleLine(StringBuffer report,
324: DiffConflictLine line) {
325: report.append("<font class=");
326: report.append(line.isFirstFile() ? "'file1'" : "'file2'");
327: report.append(">").append(line.getLineNumber()).append(
328: "</font><br>");
329: report.append(line.toString());
330: }
331:
332: private void reportLineGroup(StringBuffer report, ArrayList temp) {
333: DiffConflictLine last = lastLine(temp);
334: DiffConflictLine first = (DiffConflictLine) temp.get(0);
335: report.append("<font class=");
336: report.append(first.isFirstFile() ? "'file1'" : "'file2'");
337: report.append(">").append(first.getLineNumber()).append(',')
338: .append(last.getLineNumber());
339: report.append("</font><br>");
340: Iterator j = temp.iterator();
341: while (j.hasNext()) {
342: DiffConflictLine groupedLine = (DiffConflictLine) j.next();
343: report.append(groupedLine.toString());
344: }
345: temp.clear();
346: }
347:
348: private DiffConflictLine lastLine(List group) {
349: return (group == null || group.isEmpty()) ? null
350: : (DiffConflictLine) group.get(group.size() - 1);
351: }
352:
353: /**
354: * For testing purposes - I distance off with JUnit, honestly - but it's too slow!
355: *
356: * @param args [1] file#1, [2] file#2.
357: */
358: public static void main(String[] args) {
359: try {
360: Diff diff = new Diff(new File(args[0]), new File(args[1]));
361: System.out.println(diff.performDiff());
362: } catch (IOException e) {
363: e.printStackTrace();
364: }
365: }
366:
367: }
368:
369: class MatchResult {
370: public MatchResult(int length, int endFile1Sequence) {
371: this .length = length;
372: this .endFile1Sequence = endFile1Sequence;
373: }
374:
375: int endFile1Sequence;
376: int length;
377:
378: public String toString() {
379: return "Match: end#1=" + endFile1Sequence + ", length="
380: + length;
381: }
382:
383: }
|