001: package vqwiki.utils;
002:
003: import java.util.List;
004: import java.util.StringTokenizer;
005: import org.apache.log4j.Logger;
006: import org.incava.util.diff.Diff;
007: import org.incava.util.diff.Difference;
008:
009: /**
010: * Utility class for creating either a text of HTML representation of the difference
011: * between two files.
012: *
013: * @author Ryan Holliday
014: */
015: public class DiffUtil {
016:
017: protected static Logger logger = Logger.getLogger(DiffUtil.class);
018: /** The number of lines of unchanged text to display before and after each diff. */
019: // FIXME - make this a property value
020: private static final int DIFF_UNCHANGED_LINE_DISPLAY = 2;
021:
022: /**
023: * Returned an HTML formatted table that displays a diff of two Strings.
024: *
025: * FIXME: return objects and parse to HTML from a JSP tag, not a class file.
026: *
027: * @param newVersion The String that is to be compared to, ie the later version of a topic.
028: * @param oldVersion The String that is to be considered as having changed, ie the earlier
029: * version of a topic.
030: * @param htmlFormat Set to true if the diff should be returned in HTML format. Returns
031: * text otherwise.
032: * @return Returns an HTML-formatted table that displays the diff of the Strings.
033: */
034: public static String diff(String newVersion, String oldVersion,
035: boolean htmlFormat) {
036: if (oldVersion == null)
037: oldVersion = "";
038: if (newVersion == null)
039: newVersion = "";
040: // FIXME: don't hard code
041: if (!htmlFormat && newVersion.equals(oldVersion))
042: return "Files are the same";
043: DiffUtil diffUtil = new DiffUtil();
044: return diffUtil.process(newVersion, oldVersion, htmlFormat);
045: }
046:
047: /**
048: *
049: */
050: private String process(String newVersion, String oldVersion,
051: boolean htmlFormat) {
052: logger.debug("Diffing: " + oldVersion + " against: "
053: + newVersion);
054: DiffHelper diffHelper = new DiffHelper(oldVersion, newVersion,
055: htmlFormat);
056: return diffHelper.diff();
057: }
058:
059: /**
060: * Split up a large String into an array of Strings made up of each line (indicated
061: * by a newline) of the original String.
062: */
063: private static String[] buildArray(String original) {
064: if (original == null)
065: return null;
066: StringTokenizer tokens = new StringTokenizer(original, "\n");
067: int size = tokens.countTokens();
068: String[] array = new String[size];
069: int count = 0;
070: while (tokens.hasMoreTokens()) {
071: array[count] = tokens.nextToken();
072: count++;
073: }
074: return array;
075: }
076:
077: /**
078: *
079: */
080: public static String convertToHTML(String input) {
081: StringBuffer output = new StringBuffer(input);
082: int pos = -1;
083: // FIXME - need a general String.replace() method
084: // for obvious reasons, ampersands must be escaped first
085: while ((pos = output.indexOf("&", (pos + 1))) != -1) {
086: output.replace(pos, pos + 1, "&");
087: }
088: while ((pos = output.indexOf("<", (pos + 1))) != -1) {
089: output.replace(pos, pos + 1, "<");
090: }
091: while ((pos = output.indexOf(">", (pos + 1))) != -1) {
092: output.replace(pos, pos + 1, ">");
093: }
094: return output.toString();
095: }
096:
097: /**
098: *
099: */
100: class DiffHelper {
101:
102: String[] oldArray = null;
103: String[] newArray = null;
104: StringBuffer output = new StringBuffer();
105: int oldCurrentLine = 0;
106: int newCurrentLine = 0;
107: int delStart, delEnd, addStart, addEnd, replacements;
108: boolean lineNumberDisplayed = false;
109: boolean htmlFormat = true;
110:
111: /**
112: *
113: */
114: DiffHelper(String oldVersion, String newVersion,
115: boolean htmlFormat) {
116: this .oldArray = buildArray(oldVersion);
117: this .newArray = buildArray(newVersion);
118: this .htmlFormat = htmlFormat;
119: }
120:
121: /**
122: * Generate an HTML row indicating the diff of two lines of the versioned
123: * files.
124: *
125: * @param oldChange A boolean flag indicating whether the old line has been deleted
126: * or changed.
127: * @param oldLine A line from the file that has changed.
128: * @param newChange A boolean flag indicating whether the new line has been added.
129: * @param newLine A line from the later version of the file.
130: */
131: private String buildRow(boolean oldChange, String oldLine,
132: boolean newChange, String newLine) {
133: StringBuffer output = new StringBuffer();
134: // escape HTML if needed
135: if (this .htmlFormat) {
136: if (oldLine.trim().length() == 0) {
137: oldLine += " ";
138: } else {
139: oldLine = convertToHTML(oldLine);
140: }
141: if (newLine.trim().length() == 0) {
142: newLine += " ";
143: } else {
144: newLine = convertToHTML(newLine);
145: }
146: }
147: // build table row
148: if (this .htmlFormat)
149: output.append("<tr>");
150: if (this .htmlFormat) {
151: if (oldChange) {
152: output
153: .append("<td class=\"diff-indicator\">-</td>");
154: output.append("<td class=\"diff-delete\">"
155: + oldLine + "</td>");
156: } else {
157: output
158: .append("<td class=\"diff-no-indicator\"> </td>");
159: output.append("<td class=\"diff-unchanged\">"
160: + oldLine + "</td>");
161: }
162: if (newChange) {
163: output
164: .append("<td class=\"diff-indicator\">+</td>");
165: output.append("<td class=\"diff-add\">" + newLine
166: + "</td>");
167: } else {
168: output
169: .append("<td class=\"diff-no-indicator\"> </td>");
170: output.append("<td class=\"diff-unchanged\">"
171: + newLine + "</td>");
172: }
173: } else {
174: if (oldChange) {
175: output.append("- >").append(oldLine).append("\n");
176: } else {
177: output.append(" >").append(oldLine).append("\n");
178: }
179: if (newChange) {
180: output.append("+ <").append(newLine).append("\n");
181: } else {
182: output.append(" <").append(newLine).append("\n");
183: }
184: }
185: if (this .htmlFormat)
186: output.append("</tr>");
187: return output.toString();
188: }
189:
190: /**
191: *
192: */
193: private boolean canDisplay(int changeStart, int changeEnd,
194: int currentLine) {
195: // only display if current line is plus or minus a specified number of lines
196: // from the change area
197: int earliest = (this .htmlFormat) ? (changeStart - DIFF_UNCHANGED_LINE_DISPLAY)
198: : changeStart;
199: int latest = (this .htmlFormat) ? (changeEnd + DIFF_UNCHANGED_LINE_DISPLAY)
200: : changeEnd;
201: if (currentLine >= earliest && currentLine <= latest)
202: return true;
203: return false;
204: }
205:
206: /**
207: *
208: */
209: String diff() {
210: Diff diffObject = new Diff(this .oldArray, this .newArray);
211: List diffs = diffObject.diff();
212: Difference diff;
213: if (this .htmlFormat)
214: this .output.append("<table class=\"diff\">");
215: for (int i = 0; i < diffs.size(); i++) {
216: diff = (Difference) diffs.get(i);
217: this .lineNumberDisplayed = false;
218: this .delStart = diff.getDeletedStart();
219: this .delEnd = diff.getDeletedEnd();
220: this .addStart = diff.getAddedStart();
221: this .addEnd = diff.getAddedEnd();
222: // add lines up to first change point
223: displayUnchanged(this .delStart, this .addStart);
224: // add changed lines
225: displayChanged();
226: }
227: // if lines at the end of the original Strings haven't changed display them
228: displayUnchanged(oldArray.length, newArray.length);
229: if (this .htmlFormat)
230: output.append("</table>");
231: return output.toString();
232: }
233:
234: /**
235: *
236: */
237: private void displayUnchanged(int delMax, int addMax) {
238: replacements = ((delMax - this .oldCurrentLine) > (addMax - this .newCurrentLine)) ? (delMax - this .oldCurrentLine)
239: : (addMax - this .newCurrentLine);
240: String oldLine, newLine;
241: for (int j = 0; j < replacements; j++) {
242: oldLine = "";
243: if (this .oldCurrentLine < this .oldArray.length
244: && this .oldCurrentLine < delMax) {
245: oldLine = this .oldArray[this .oldCurrentLine];
246: this .oldCurrentLine++;
247: }
248: newLine = "";
249: if (this .newCurrentLine < this .newArray.length
250: && this .newCurrentLine < addMax) {
251: newLine = this .newArray[this .newCurrentLine];
252: this .newCurrentLine++;
253: }
254: // only display if within specified number of lines of a change. subtract
255: // one from current line since that value was incremented above
256: if (canDisplay(delMax, this .delEnd,
257: (this .oldCurrentLine - 1))
258: || canDisplay(addMax, this .addEnd,
259: (this .newCurrentLine - 1))) {
260: displayLineNumber();
261: this .output.append(buildRow(false, oldLine, false,
262: newLine));
263: }
264: }
265: }
266:
267: /**
268: *
269: */
270: private void displayChanged() {
271: replacements = ((this .delEnd - this .delStart) > (this .addEnd - this .addStart)) ? (this .delEnd - this .delStart)
272: : (this .addEnd - this .addStart);
273: String oldLine, newLine;
274: boolean oldChange, newChange;
275: for (int j = 0; j <= replacements; j++) {
276: oldLine = "";
277: oldChange = false;
278: if (this .oldCurrentLine < this .oldArray.length
279: && this .delStart <= this .delEnd) {
280: oldLine = this .oldArray[this .oldCurrentLine];
281: oldChange = true;
282: this .oldCurrentLine++;
283: this .delStart++;
284: }
285: newLine = "";
286: newChange = false;
287: if (this .newCurrentLine < this .newArray.length
288: && this .addStart <= this .addEnd) {
289: newLine = this .newArray[this .newCurrentLine];
290: newChange = true;
291: this .newCurrentLine++;
292: this .addStart++;
293: }
294: displayLineNumber();
295: output.append(buildRow(oldChange, oldLine, newChange,
296: newLine));
297: }
298: }
299:
300: /**
301: *
302: */
303: private void displayLineNumber() {
304: if (this .lineNumberDisplayed)
305: return;
306: int lineNumber = oldCurrentLine;
307: this .lineNumberDisplayed = true;
308: if (this .htmlFormat) {
309: output
310: .append("<tr><td colspan=\"4\" class=\"diff-line\">Line "
311: + lineNumber + ":</td></tr>");
312: } else {
313: output.append("Line " + lineNumber + ":\n");
314: }
315: }
316: }
317: }
|