package jdiff; import java.io.*; import java.util.*; /** * Class to generate colored differences between two sections of HTML text. * * See the file LICENSE.txt for copyright details. * @author Matthew Doar, mdoar@pobox.com */ class Diff { /** * Save the differences between the two strings in a DiffOutput object * for later use. * * @param id A per-package unique identifier for each documentation * change. */ static String saveDocDiffs(String pkgName, String className, String oldDoc, String newDoc, String id, String title) { // Generate the string which will link to this set of diffs if (noDocDiffs) return "Documentation changed from "; if (oldDoc == null || newDoc == null) { return "Documentation changed from "; } // Generate the differences. generateDiffs(pkgName, className, oldDoc, newDoc, id, title); return "Documentation changed from "; } /** * Generate the differences. */ static void generateDiffs(String pkgName, String className, String oldDoc, String newDoc, String id, String title) { String[] oldDocWords = parseDoc(oldDoc); String[] newDocWords = parseDoc(newDoc); DiffMyers diff = new DiffMyers(oldDocWords, newDocWords); DiffMyers.change script = diff.diff_2(false); script = mergeDiffs(oldDocWords, newDocWords, script); String text = "" + title + "

"; // Generate the differences in blockquotes to cope with unterminated // HTML tags text += "
"; text = addDiffs(oldDocWords, newDocWords, script, text); text += "
"; docDiffs.add(new DiffOutput(pkgName, className, id, title, text)); } /** * Convert the string to an array of strings, but don't break HTML tags up. */ static String[] parseDoc(String doc) { String delimiters = " .,;:?!(){}[]\"'~@#$%^&*+=_-|\\<>/"; StringTokenizer st = new StringTokenizer(doc, delimiters, true); List docList = new ArrayList(); boolean inTag = false; String tag = null; while (st.hasMoreTokens()) { String tok = st.nextToken(); if (!inTag) { if (tok.compareTo("<") == 0) { tag = tok; if (st.hasMoreTokens()) { // See if this really is a tag tok = st.nextToken(); char ch = tok.charAt(0); if (Character.isLetter(ch) || ch == '/') { inTag = true; tag += tok; } } if (!inTag) docList.add(tag); } else { docList.add(tok); } } else { // Add all tokens to the tag until the closing > is seen if (tok.compareTo(">") == 0) { inTag = false; tag += tok; docList.add(tag); } else { tag += tok; } } } if (inTag) { // An unterminated tag, or more likely, < used instead of < // There are no nested tags such as > in HTML docList.add(tag); } String[] docWords = new String[docList.size()]; docWords = (String[])docList.toArray(docWords); return docWords; } /** * For improved readability, merge changes of the form * "delete 1, insert 1, space, delete 1, insert 1" * to * "delete 3, insert 3" (including the space). * * @param oldDocWords The original documentation as a String array * @param newDocWords The new documentation as a String array */ static DiffMyers.change mergeDiffs(String[] oldDocWords, String[] newDocWords, DiffMyers.change script) { if (script.link == null) return script; // Only one change DiffMyers.change hunk = script; DiffMyers.change lasthunk = null; // Set to the last potential hunk int startOld = 0; for (; hunk != null; hunk = hunk.link) { int deletes = hunk.deleted; int inserts = hunk.inserted; if (lasthunk == null) { if (deletes == 1 && inserts == 1) { // This is the start of a potential merge lasthunk = hunk; } continue; } else { int first0 = hunk.line0; // Index of first deleted word int first1 = hunk.line1; // Index of first inserted word if (deletes == 1 && inserts == 1 && oldDocWords[first0 - 1].compareTo(" ") == 0 && newDocWords[first1 - 1].compareTo(" ") == 0 && first0 == lasthunk.line0 + lasthunk.deleted + 1 && first1 == lasthunk.line1 + lasthunk.inserted + 1) { // Merge this change into the last change lasthunk.deleted += 2; lasthunk.inserted += 2; lasthunk.link = hunk.link; } else { lasthunk = null; } } } return script; } /** * Add the differences to the text passed in. The old documentation is * edited using the edit script provided by the DiffMyers object. * Do not display diffs in HTML tags. * * @param oldDocWords The original documentation as a String array * @param newDocWords The new documentation as a String array * @return The text for this documentation difference */ static String addDiffs(String[] oldDocWords, String[] newDocWords, DiffMyers.change script, String text) { String res = text; DiffMyers.change hunk = script; int startOld = 0; if (trace) { System.out.println("Old Text:"); for (int i = 0; i < oldDocWords.length; i++) { System.out.print(oldDocWords[i]); } System.out.println(":END"); System.out.println("New Text:"); for (int i = 0; i < newDocWords.length; i++) { System.out.print(newDocWords[i]); } System.out.println(":END"); } for (; hunk != null; hunk = hunk.link) { int deletes = hunk.deleted; int inserts = hunk.inserted; if (deletes == 0 && inserts == 0) { continue; // Not clear how this would occur, but handle it } // Determine the range of word and delimiter numbers involved // in each file. int first0 = hunk.line0; // Index of first deleted word // Index of last deleted word, invalid if deletes == 0 int last0 = hunk.line0 + hunk.deleted - 1; int first1 = hunk.line1; // Index of first inserted word // Index of last inserted word, invalid if inserts == 0 int last1 = hunk.line1 + hunk.inserted - 1; if (trace) { System.out.println("HUNK: "); System.out.println("inserts: " + inserts); System.out.println("deletes: " + deletes); System.out.println("first0: " + first0); System.out.println("last0: " + last0); System.out.println("first1: " + first1); System.out.println("last1: " + last1); } // Emit the original document up to this change for (int i = startOld; i < first0; i++) { res += oldDocWords[i]; } // Record where to start the next hunk from startOld = last0 + 1; // Emit the deleted words, but struck through // but do not emit deleted HTML tags if (deletes != 0) { boolean inStrike = false; for (int i = first0; i <= last0; i++) { if (!oldDocWords[i].startsWith("<") && !oldDocWords[i].endsWith(">")) { if (!inStrike) { if (deleteEffect == 0) res += ""; else if (deleteEffect == 1) res += ""; inStrike = true; } res += oldDocWords[i]; } } if (inStrike) { if (deleteEffect == 0) res += ""; else if (deleteEffect == 1) res += ""; } } // Emit the inserted words, but do not emphasise new HTML tags if (inserts != 0) { boolean inEmph = false; for (int i = first1; i <= last1; i++) { if (!newDocWords[i].startsWith("<") && !newDocWords[i].endsWith(">")) { if (!inEmph) { if (insertEffect == 0) res += ""; else if (insertEffect == 1) res += ""; inEmph = true; } } res += newDocWords[i]; } if (inEmph) { if (insertEffect == 0) res += ""; else if (insertEffect == 1) res += ""; } } } //for (; hunk != null; hunk = hunk.link) // Print out the remaining part of the old text for (int i = startOld; i < oldDocWords.length; i++) { res += oldDocWords[i]; } return res; } /** * Emit all the documentation differences into one file per package. */ static void emitDocDiffs(String fullReportFileName) { Collections.sort(docDiffs); DiffOutput[] docDiffsArr = new DiffOutput[docDiffs.size()]; docDiffsArr = (DiffOutput[])docDiffs.toArray(docDiffsArr); for (int i = 0; i < docDiffsArr.length; i++) { DiffOutput diffOutput = docDiffsArr[i]; if (currPkgName == null || currPkgName.compareTo(diffOutput.pkgName_) != 0) { // Open a different file for each package, add the HTML header, // the navigation bar and some preamble. if (currPkgName != null) closeDiffFile(); // Close the existing file // Create the HTML link to the previous package String prevPkgName = currPkgName; if (currPkgName != null) { prevPkgName = diffFileName + docDiffsArr[i-1].pkgName_ + HTMLReportGenerator.reportFileExt; } // Set the current package name currPkgName = diffOutput.pkgName_; // Create the HTML link to the next package String nextPkgName = null; for (int j = i; j < docDiffsArr.length; j++) { if (currPkgName.compareTo(docDiffsArr[j].pkgName_) != 0) { nextPkgName = diffFileName + docDiffsArr[j].pkgName_ + HTMLReportGenerator.reportFileExt; break; } } String fullDiffFileName = fullReportFileName + JDiff.DIR_SEP + diffFileName + currPkgName + HTMLReportGenerator.reportFileExt; // Create the output file try { FileOutputStream fos = new FileOutputStream(fullDiffFileName); diffFile = new PrintWriter(fos); // Write the HTML header diffFile.println(""); diffFile.println(""); diffFile.println(""); diffFile.println(""); diffFile.println(""); diffFile.println(""); // diffFile.println(""); diffFile.println(""); diffFile.println(""); diffFile.println(""); diffFile.println(""); diffFile.println(currPkgName + " Documentation Differences"); diffFile.println(""); diffFile.println(""); diffFile.println(""); // Write the navigation bar diffFile.println(""); diffFile.println(""); diffFile.println(""); diffFile.println(""); // The right hand side title diffFile.println(""); diffFile.println(""); // Links for previous and next, and frames and no frames diffFile.println(""); diffFile.println(" "); diffFile.println(" "); diffFile.println(""); diffFile.println("
"); diffFile.println(" "); diffFile.println(" "); // Always have a link to the Javadoc files String pkgRef = currPkgName; pkgRef = pkgRef.replace('.', '/'); pkgRef = HTMLReportGenerator.newDocPrefix + pkgRef + "/package-summary"; diffFile.println(" "); diffFile.println(" "); diffFile.println(" "); diffFile.println(" "); if (!Diff.noDocDiffs) { diffFile.println(" "); } if (HTMLReportGenerator.doStats) { diffFile.println(" "); } diffFile.println(" "); diffFile.println(" "); diffFile.println("
" + APIDiff.newAPIName_ + "  Overview   Package   Class  Text Changes  Statistics  Help 
"); diffFile.println("
Generated by
JDiff
"); if (prevPkgName != null) diffFile.println(" PREV PACKAGE  "); else diffFile.println(" PREV PACKAGE  "); if (nextPkgName != null) diffFile.println("  NEXT PACKAGE"); else diffFile.println("  NEXT PACKAGE"); diffFile.println("        "); diffFile.println(" FRAMES  "); diffFile.println("  NO FRAMES 
"); diffFile.println("
"); diffFile.println(""); diffFile.println("

"); diffFile.println(currPkgName + " Documentation Differences"); diffFile.println("

"); diffFile.println(); diffFile.println("
"); diffFile.println("This file contains all the changes in documentation in the package " + currPkgName + " as colored differences."); if (deleteEffect == 0) diffFile.println("Deletions are shown like this, and"); else if (deleteEffect == 1) diffFile.println("Deletions are shown like this, and"); if (insertEffect == 0) diffFile.println("additions are shown in red like this."); else if (insertEffect == 1) diffFile.println("additions are shown like this."); diffFile.println("
"); diffFile.println("
"); diffFile.println("If no deletions or additions are shown in an entry, the HTML tags will be what has changed. The new HTML tags are shown in the differences. "); diffFile.println("If no documentation existed, and then some was added in a later version, this change is noted in the appropriate class pages of differences, but the change is not shown on this page. Only changes in existing text are shown here. "); diffFile.println("Similarly, documentation which was inherited from another class or interface is not shown here."); diffFile.println("
"); diffFile.println("
"); diffFile.println(" Note that an HTML error in the new documentation may cause the display of other documentation changes to be presented incorrectly. For instance, failure to close a <code> tag will cause all subsequent paragraphs to be displayed differently."); diffFile.println("
"); diffFile.println("
"); diffFile.println(); } catch(IOException e) { System.out.println("IO Error while attempting to create " + fullDiffFileName); System.out.println("Error: " + e.getMessage()); System.exit(1); } } // if (currPkgName == null || currPkgName.compareTo(diffOutput.pkgName_) != 0) // Now add the documentation difference text diffFile.println(diffOutput.text_); // Separate with a horizontal line if (i != docDiffsArr.length - 1 && diffOutput.className_ != null && docDiffsArr[i+1].className_ != null && diffOutput.className_.compareTo(docDiffsArr[i+1].className_) != 0) diffFile.println("
"); // else // diffFile.println("
"); } // for (i = 0; if (currPkgName != null) closeDiffFile(); // Close the existing file // Emit the single file which is the index to all documentation changes emitDocDiffIndex(fullReportFileName, docDiffsArr); } /** * Emit the single file which is the index to all documentation changes. */ public static void emitDocDiffIndex(String fullReportFileName, DiffOutput[] docDiffsArr) { String fullDiffFileName = fullReportFileName + JDiff.DIR_SEP + diffFileName + "index" + HTMLReportGenerator.reportFileExt; // Create the output file try { FileOutputStream fos = new FileOutputStream(fullDiffFileName); diffFile = new PrintWriter(fos); // Write the HTML header diffFile.println(""); diffFile.println(""); diffFile.println(""); diffFile.println(""); diffFile.println(""); diffFile.println(""); // diffFile.println(""); diffFile.println(""); diffFile.println(""); diffFile.println(""); diffFile.println(""); diffFile.println("All Documentation Differences"); diffFile.println(""); diffFile.println(""); diffFile.println(""); // Write the navigation bar diffFile.println(""); diffFile.println(""); diffFile.println(""); diffFile.println(""); // The right hand side title diffFile.println(""); diffFile.println(""); // Links for frames and no frames diffFile.println(""); diffFile.println(" "); diffFile.println(" "); diffFile.println(""); diffFile.println("
"); diffFile.println(" "); diffFile.println(" "); // Always have a link to the Javadoc files diffFile.println(" "); diffFile.println(" "); diffFile.println(" "); diffFile.println(" "); if (!Diff.noDocDiffs) { diffFile.println(" "); } if (HTMLReportGenerator.doStats) { diffFile.println(" "); } diffFile.println(" "); diffFile.println(" "); diffFile.println("
" + APIDiff.newAPIName_ + "  Overview   Package   Class  Text Changes  Statistics  Help 
"); diffFile.println("
Generated by
JDiff
"); diffFile.println(" FRAMES  "); diffFile.println("  NO FRAMES 
"); diffFile.println("
"); diffFile.println(""); diffFile.println("

"); diffFile.println("All Documentation Differences"); diffFile.println("

"); diffFile.println(); // For each package and class, add the first DiffOutput to // the hash table. Used when generating navigation bars. boolean firstPackage = true; // Set for the first package boolean firstClass = true; // Set for first class in a package boolean firstCtor = true; // Set for first ctor in a class boolean firstMethod = true; // Set for first method in a class boolean firstField = true; // Set for first field in a class for (int i = 0; i < docDiffsArr.length; i++) { DiffOutput diffOutput = docDiffsArr[i]; String link = "
"; // See if the package name changed if (firstPackage || diffOutput.pkgName_.compareTo(docDiffsArr[i-1].pkgName_) != 0) { if (firstPackage) { firstPackage = false; } else { diffFile.println("
"); } firstClass = true; firstCtor = true; firstMethod = true; firstField = true; String id = diffOutput.pkgName_ + "!package"; firstDiffOutput.put(id, id); if (diffOutput.className_ == null) { diffFile.println("
" + link + "Package " + diffOutput.pkgName_ + "
"); } else { diffFile.println("" + "Package " + diffOutput.pkgName_ + "
"); } } // See if the class name changed if (diffOutput.className_ != null && (firstClass || diffOutput.className_.compareTo(docDiffsArr[i-1].className_) != 0)) { if (firstClass) { firstClass = false; } else { diffFile.println("
"); } firstCtor = true; firstMethod = true; firstField = true; String id = diffOutput.pkgName_ + "." + diffOutput.className_ + "!class"; firstDiffOutput.put(id, id); if (diffOutput.id_.endsWith("!class")) { diffFile.println("  Class " + link + diffOutput.className_ + "
"); } else { diffFile.println("  Class " + diffOutput.className_ + "
"); } } // Work out what kind of member this is, and // display it appropriately if (diffOutput.className_ != null && !diffOutput.id_.endsWith("!class")) { int ctorIdx = diffOutput.id_.indexOf(".ctor"); if (ctorIdx != -1) { diffFile.println("    " + link + diffOutput.className_ + diffOutput.id_.substring(ctorIdx + 5) + "
"); } else { int methodIdx = diffOutput.id_.indexOf(".dmethod."); if (methodIdx != -1) { diffFile.println("    " + "Method " + link + diffOutput.id_.substring(methodIdx + 9) + "
"); } else { int fieldIdx = diffOutput.id_.indexOf(".field."); if (fieldIdx != -1) { diffFile.println("    " + "Field " + link + diffOutput.id_.substring(fieldIdx + 7) + "
"); } } //if (methodIdx != -1) } //if (ctorIdx != -1) } //diffOutput.className_ != null } } catch(IOException e) { System.out.println("IO Error while attempting to create " + fullDiffFileName); System.out.println("Error: " + e.getMessage()); System.exit(1); } closeDiffFile(); } /** * Emit the HTML footer and close the diff file. */ public static void closeDiffFile() { if (diffFile != null) { // Write the HTML footer diffFile.println(); diffFile.println(""); diffFile.println(""); diffFile.close(); } } /** * Current file where documentation differences are written as colored * differences. */ public static PrintWriter diffFile = null; /** * Base name of the current file where documentation differences are * written as colored differences. */ public static String diffFileName = "docdiffs_"; /** * The name of the current package, used to create diffFileName. */ private static String currPkgName = null; /** * If set, then do not generate colored diffs for documentation. * Default is true. */ public static boolean noDocDiffs = true; /** * Define the type of emphasis for deleted words. * 0 strikes the words through. * 1 outlines the words in light grey. */ public static int deleteEffect = 0; /** * Define the type of emphasis for inserted words. * 0 colors the words red. * 1 outlines the words in yellow, like a highlighter. */ public static int insertEffect = 1; /** * For each package and class, the first DiffOutput is added to * this hash table. Used when generating navigation bars. */ public static Hashtable firstDiffOutput = new Hashtable(); /** * If set, then show changes in implementation-related modifiers such as * native and synchronized. For more information, see * http://java.sun.com/j2se/1.4.1/docs/tooldocs/solaris/javadoc.html#generatedapideclarations */ public static boolean showAllChanges = false; /** The list of documentation differences. */ private static List docDiffs = new ArrayList(); // DiffOutput[] /** Set to enable increased logging verbosity for debugging. */ private static boolean trace = false; }