Home | History | Annotate | Download | only in jdiff
      1 package jdiff;
      2 
      3 import java.io.*;
      4 import java.util.*;
      5 
      6 /* For SAX XML parsing */
      7 import org.xml.sax.Attributes;
      8 import org.xml.sax.SAXException;
      9 import org.xml.sax.SAXParseException;
     10 import org.xml.sax.XMLReader;
     11 import org.xml.sax.InputSource;
     12 import org.xml.sax.helpers.*;
     13 
     14 /**
     15  * Creates a Comments from an XML file. The Comments object is the internal
     16  * representation of the comments for the changes.
     17  * All methods in this class for populating a Comments object are static.
     18  *
     19  * See the file LICENSE.txt for copyright details.
     20  * @author Matthew Doar, mdoar (at) pobox.com
     21  */
     22 public class Comments {
     23 
     24     /**
     25      * All the possible comments known about, accessible by the commentID.
     26      */
     27     public static Hashtable allPossibleComments = new Hashtable();
     28 
     29     /** The old Comments object which is populated from the file read in. */
     30     private static Comments oldComments_ = null;
     31 
     32     /** Default constructor. */
     33     public Comments() {
     34         commentsList_ = new ArrayList(); // SingleComment[]
     35     }
     36 
     37     // The list of comments elements associated with this objects
     38     public List commentsList_ = null; // SingleComment[]
     39 
     40     /**
     41      * Read the file where the XML for comments about the changes between
     42      * the old API and new API is stored and create a Comments object for
     43      * it. The Comments object may be null if no file exists.
     44      */
     45     public static Comments readFile(String filename) {
     46         // If validation is desired, write out the appropriate comments.xsd
     47         // file in the same directory as the comments XML file.
     48         if (XMLToAPI.validateXML) {
     49             writeXSD(filename);
     50         }
     51 
     52         // If the file does not exist, return null
     53         File f = new File(filename);
     54         if (!f.exists())
     55             return null;
     56 
     57         // The instance of the Comments object which is populated from the file.
     58         oldComments_ = new Comments();
     59         try {
     60             DefaultHandler handler = new CommentsHandler(oldComments_);
     61             XMLReader parser = null;
     62             try {
     63                 String parserName = System.getProperty("org.xml.sax.driver");
     64                 if (parserName == null) {
     65                     parser = org.xml.sax.helpers.XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");
     66                 } else {
     67                     // Let the underlying mechanisms try to work out which
     68                     // class to instantiate
     69                     parser = org.xml.sax.helpers.XMLReaderFactory.createXMLReader();
     70                 }
     71             } catch (SAXException saxe) {
     72                 System.out.println("SAXException: " + saxe);
     73                 saxe.printStackTrace();
     74                 System.exit(1);
     75             }
     76 
     77             if (XMLToAPI.validateXML) {
     78                 parser.setFeature("http://xml.org/sax/features/namespaces", true);
     79                 parser.setFeature("http://xml.org/sax/features/validation", true);
     80                 parser.setFeature("http://apache.org/xml/features/validation/schema", true);
     81             }
     82             parser.setContentHandler(handler);
     83             parser.setErrorHandler(handler);
     84             parser.parse(new InputSource(new FileInputStream(new File(filename))));
     85         } catch(org.xml.sax.SAXNotRecognizedException snre) {
     86             System.out.println("SAX Parser does not recognize feature: " + snre);
     87             snre.printStackTrace();
     88             System.exit(1);
     89         } catch(org.xml.sax.SAXNotSupportedException snse) {
     90             System.out.println("SAX Parser feature is not supported: " + snse);
     91             snse.printStackTrace();
     92             System.exit(1);
     93         } catch(org.xml.sax.SAXException saxe) {
     94             System.out.println("SAX Exception parsing file '" + filename + "' : " + saxe);
     95             saxe.printStackTrace();
     96             System.exit(1);
     97         } catch(java.io.IOException ioe) {
     98             System.out.println("IOException parsing file '" + filename + "' : " + ioe);
     99             ioe.printStackTrace();
    100             System.exit(1);
    101         }
    102 
    103         Collections.sort(oldComments_.commentsList_);
    104         return oldComments_;
    105     } //readFile()
    106 
    107     /**
    108      * Write the XML Schema file used for validation.
    109      */
    110     public static void writeXSD(String filename) {
    111         String xsdFileName = filename;
    112         int idx = xsdFileName.lastIndexOf('\\');
    113         int idx2 = xsdFileName.lastIndexOf('/');
    114         if (idx == -1 && idx2 == -1) {
    115             xsdFileName = "";
    116         } else if (idx == -1 && idx2 != -1) {
    117             xsdFileName = xsdFileName.substring(0, idx2+1);
    118         } else if (idx != -1  && idx2 == -1) {
    119             xsdFileName = xsdFileName.substring(0, idx+1);
    120         } else if (idx != -1  && idx2 != -1) {
    121             int max = idx2 > idx ? idx2 : idx;
    122             xsdFileName = xsdFileName.substring(0, max+1);
    123         }
    124         xsdFileName += "comments.xsd";
    125         try {
    126             FileOutputStream fos = new FileOutputStream(xsdFileName);
    127             PrintWriter xsdFile = new PrintWriter(fos);
    128             // The contents of the comments.xsd file
    129             xsdFile.println("<?xml version=\"1.0\" encoding=\"iso-8859-1\" standalone=\"no\"?>");
    130             xsdFile.println("<xsd:schema xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">");
    131             xsdFile.println();
    132             xsdFile.println("<xsd:annotation>");
    133             xsdFile.println("  <xsd:documentation>");
    134             xsdFile.println("  Schema for JDiff comments.");
    135             xsdFile.println("  </xsd:documentation>");
    136             xsdFile.println("</xsd:annotation>");
    137             xsdFile.println();
    138             xsdFile.println("<xsd:element name=\"comments\" type=\"commentsType\"/>");
    139             xsdFile.println();
    140             xsdFile.println("<xsd:complexType name=\"commentsType\">");
    141             xsdFile.println("  <xsd:sequence>");
    142             xsdFile.println("    <xsd:element name=\"comment\" type=\"commentType\" minOccurs='0' maxOccurs='unbounded'/>");
    143             xsdFile.println("  </xsd:sequence>");
    144             xsdFile.println("  <xsd:attribute name=\"name\" type=\"xsd:string\"/>");
    145             xsdFile.println("  <xsd:attribute name=\"jdversion\" type=\"xsd:string\"/>");
    146             xsdFile.println("</xsd:complexType>");
    147             xsdFile.println();
    148             xsdFile.println("<xsd:complexType name=\"commentType\">");
    149             xsdFile.println("  <xsd:sequence>");
    150             xsdFile.println("    <xsd:element name=\"identifier\" type=\"identifierType\" minOccurs='1' maxOccurs='unbounded'/>");
    151             xsdFile.println("    <xsd:element name=\"text\" type=\"xsd:string\" minOccurs='1' maxOccurs='1'/>");
    152             xsdFile.println("  </xsd:sequence>");
    153             xsdFile.println("</xsd:complexType>");
    154             xsdFile.println();
    155             xsdFile.println("<xsd:complexType name=\"identifierType\">");
    156             xsdFile.println("  <xsd:attribute name=\"id\" type=\"xsd:string\"/>");
    157             xsdFile.println("</xsd:complexType>");
    158             xsdFile.println();
    159             xsdFile.println("</xsd:schema>");
    160             xsdFile.close();
    161         } catch(IOException e) {
    162             System.out.println("IO Error while attempting to create " + xsdFileName);
    163             System.out.println("Error: " +  e.getMessage());
    164             System.exit(1);
    165         }
    166     }
    167 
    168 //
    169 // Methods to add data to a Comments object. Called by the XML parser and the
    170 // report generator.
    171 //
    172 
    173     /**
    174      * Add the SingleComment object to the list of comments kept by this
    175      * object.
    176      */
    177     public void addComment(SingleComment comment) {
    178         commentsList_.add(comment);
    179     }
    180 
    181 //
    182 // Methods to get data from a Comments object. Called by the report generator
    183 //
    184 
    185     /**
    186      * The text placed into XML comments file where there is no comment yet.
    187      * It never appears in reports.
    188      */
    189     public static final String placeHolderText = "InsertCommentsHere";
    190 
    191     /**
    192      * Return the comment associated with the given id in the Comment object.
    193      * If there is no such comment, return the placeHolderText.
    194      */
    195     public static String getComment(Comments comments, String id) {
    196         if (comments == null)
    197             return placeHolderText;
    198         SingleComment key = new SingleComment(id, null);
    199         int idx = Collections.binarySearch(comments.commentsList_, key);
    200         if (idx < 0) {
    201             return placeHolderText;
    202         } else {
    203             int startIdx = comments.commentsList_.indexOf(key);
    204             int endIdx = comments.commentsList_.indexOf(key);
    205             int numIdx = endIdx - startIdx + 1;
    206             if (numIdx != 1) {
    207                 System.out.println("Warning: " + numIdx + " identical ids in the existing comments file. Using the first instance.");
    208             }
    209             SingleComment singleComment = (SingleComment)(comments.commentsList_.get(idx));
    210             // Convert @link tags to links
    211             return singleComment.text_;
    212         }
    213     }
    214 
    215     /**
    216      * Convert @link tags to HTML links.
    217      */
    218     public static String convertAtLinks(String text, String currentElement,
    219                                         PackageAPI pkg, ClassAPI cls) {
    220         if (text == null)
    221             return null;
    222 
    223         StringBuffer result = new StringBuffer();
    224 
    225         int state = -1;
    226 
    227         final int NORMAL_TEXT = -1;
    228         final int IN_LINK = 1;
    229         final int IN_LINK_IDENTIFIER = 2;
    230         final int IN_LINK_IDENTIFIER_REFERENCE = 3;
    231         final int IN_LINK_IDENTIFIER_REFERENCE_PARAMS = 6;
    232         final int IN_LINK_LINKTEXT = 4;
    233         final int END_OF_LINK = 5;
    234 
    235         StringBuffer identifier = null;
    236         StringBuffer identifierReference = null;
    237         StringBuffer linkText = null;
    238 
    239         // Figure out relative reference if required.
    240         String ref = "";
    241         if (currentElement.compareTo("class") == 0 ||
    242             currentElement.compareTo("interface") == 0) {
    243 	    ref = pkg.name_ + "." + cls.name_ + ".";
    244         } else if (currentElement.compareTo("package") == 0) {
    245 	    ref = pkg.name_ + ".";
    246         }
    247         ref = ref.replace('.', '/');
    248 
    249         for (int i=0; i < text.length(); i++) {
    250 	    char c = text.charAt( i);
    251 	    char nextChar = i < text.length()-1 ? text.charAt( i+1) : (char)-1;
    252 	    int remainingChars = text.length() - i;
    253 
    254 	    switch (state) {
    255 	    case NORMAL_TEXT:
    256 		if (c == '{' && remainingChars >= 5) {
    257 		    if ("{@link".equals(text.substring(i, i + 6))) {
    258 			state = IN_LINK;
    259 			identifier = null;
    260 			identifierReference = null;
    261 			linkText = null;
    262 			i += 5;
    263 			continue;
    264 		    }
    265 		}
    266 		result.append( c);
    267 		break;
    268 	    case IN_LINK:
    269 		if (Character.isWhitespace(nextChar)) continue;
    270 		if (nextChar == '}') {
    271 		    // End of the link
    272 		    state = END_OF_LINK;
    273 		} else if (!Character.isWhitespace(nextChar)) {
    274 		    state = IN_LINK_IDENTIFIER;
    275 		}
    276 		break;
    277             case IN_LINK_IDENTIFIER:
    278 		if (identifier == null) {
    279 		    identifier = new StringBuffer();
    280 		}
    281 
    282 		if (c == '#') {
    283 		    // We have a reference.
    284 		    state = IN_LINK_IDENTIFIER_REFERENCE;
    285 		    // Don't append #
    286 		    continue;
    287 		} else if (Character.isWhitespace(c)) {
    288 		    // We hit some whitespace: the next character is the beginning
    289 		    // of the link text.
    290 		    state = IN_LINK_LINKTEXT;
    291 		    continue;
    292 		}
    293 		identifier.append(c);
    294 		// Check for a } that ends the link.
    295 		if (nextChar == '}') {
    296 		    state = END_OF_LINK;
    297 		}
    298 		break;
    299             case IN_LINK_IDENTIFIER_REFERENCE:
    300 		if (identifierReference == null) {
    301 		    identifierReference = new StringBuffer();
    302 		}
    303 		if (Character.isWhitespace(c)) {
    304 		    state = IN_LINK_LINKTEXT;
    305 		    continue;
    306 		}
    307 		identifierReference.append(c);
    308 
    309 		if (c == '(') {
    310 		    state = IN_LINK_IDENTIFIER_REFERENCE_PARAMS;
    311 		}
    312 
    313 		if (nextChar == '}') {
    314 		    state = END_OF_LINK;
    315 		}
    316 		break;
    317             case IN_LINK_IDENTIFIER_REFERENCE_PARAMS:
    318 		// We're inside the parameters of a reference. Spaces are allowed.
    319 		if (c == ')') {
    320 		    state = IN_LINK_IDENTIFIER_REFERENCE;
    321 		}
    322 		identifierReference.append(c);
    323 		if (nextChar == '}') {
    324 		    state = END_OF_LINK;
    325 		}
    326 		break;
    327             case IN_LINK_LINKTEXT:
    328 		if (linkText == null) linkText = new StringBuffer();
    329 
    330 		linkText.append(c);
    331 
    332 		if (nextChar == '}') {
    333 		    state = END_OF_LINK;
    334 		}
    335 		break;
    336             case END_OF_LINK:
    337 		if (identifier != null) {
    338 		    result.append("<A HREF=\"");
    339 		    result.append(HTMLReportGenerator.newDocPrefix);
    340 		    result.append(ref);
    341 		    result.append(identifier.toString().replace('.', '/'));
    342 		    result.append(".html");
    343 		    if (identifierReference != null) {
    344 			result.append("#");
    345 			result.append(identifierReference);
    346 		    }
    347 		    result.append("\">");   // target=_top?
    348 
    349 		    result.append("<TT>");
    350 		    if (linkText != null) {
    351 			result.append(linkText);
    352 		    } else {
    353 			result.append(identifier);
    354 			if (identifierReference != null) {
    355 			    result.append(".");
    356 			    result.append(identifierReference);
    357 			}
    358 		    }
    359 		    result.append("</TT>");
    360 		    result.append("</A>");
    361 		}
    362 		state = NORMAL_TEXT;
    363 		break;
    364 	    }
    365         }
    366         return result.toString();
    367     }
    368 
    369 //
    370 // Methods to write a Comments object out to a file.
    371 //
    372 
    373     /**
    374      * Write the XML representation of comments to a file.
    375      *
    376      * @param outputFileName The name of the comments file.
    377      * @param oldComments The old comments on the changed APIs.
    378      * @param newComments The new comments on the changed APIs.
    379      * @return true if no problems encountered
    380      */
    381     public static boolean writeFile(String outputFileName,
    382                                     Comments newComments) {
    383         try {
    384             FileOutputStream fos = new FileOutputStream(outputFileName);
    385             outputFile = new PrintWriter(fos);
    386             newComments.emitXMLHeader(outputFileName);
    387             newComments.emitComments();
    388             newComments.emitXMLFooter();
    389             outputFile.close();
    390         } catch(IOException e) {
    391             System.out.println("IO Error while attempting to create " + outputFileName);
    392             System.out.println("Error: "+ e.getMessage());
    393             System.exit(1);
    394         }
    395         return true;
    396     }
    397 
    398     /**
    399      * Write the Comments object out in XML.
    400      */
    401     public void emitComments() {
    402         Iterator iter = commentsList_.iterator();
    403         while (iter.hasNext()) {
    404             SingleComment currComment = (SingleComment)(iter.next());
    405             if (!currComment.isUsed_)
    406                 outputFile.println("<!-- This comment is no longer used ");
    407             outputFile.println("<comment>");
    408             outputFile.println("  <identifier id=\"" + currComment.id_ + "\"/>");
    409             outputFile.println("  <text>");
    410             outputFile.println("    " + currComment.text_);
    411             outputFile.println("  </text>");
    412             outputFile.println("</comment>");
    413             if (!currComment.isUsed_)
    414                 outputFile.println("-->");
    415         }
    416     }
    417 
    418     /**
    419      * Dump the contents of a Comments object out for inspection.
    420      */
    421     public void dump() {
    422         Iterator iter = commentsList_.iterator();
    423         int i = 0;
    424         while (iter.hasNext()) {
    425             i++;
    426             SingleComment currComment = (SingleComment)(iter.next());
    427             System.out.println("Comment " + i);
    428             System.out.println("id = " + currComment.id_);
    429             System.out.println("text = \"" + currComment.text_ + "\"");
    430             System.out.println("isUsed = " + currComment.isUsed_);
    431         }
    432     }
    433 
    434     /**
    435      * Emit messages about which comments are now unused and which are new.
    436      */
    437     public static void noteDifferences(Comments oldComments, Comments newComments) {
    438         if (oldComments == null) {
    439             System.out.println("Note: all the comments have been newly generated");
    440             return;
    441         }
    442 
    443         // See which comment ids are no longer used and add those entries to
    444         // the new comments, marking them as unused.
    445         Iterator iter = oldComments.commentsList_.iterator();
    446         while (iter.hasNext()) {
    447             SingleComment oldComment = (SingleComment)(iter.next());
    448             int idx = Collections.binarySearch(newComments.commentsList_, oldComment);
    449             if (idx < 0) {
    450                 System.out.println("Warning: comment \"" + oldComment.id_ + "\" is no longer used.");
    451                 oldComment.isUsed_ = false;
    452                 newComments.commentsList_.add(oldComment);
    453             }
    454         }
    455 
    456     }
    457 
    458     /**
    459      * Emit the XML header.
    460      */
    461     public void emitXMLHeader(String filename) {
    462         outputFile.println("<?xml version=\"1.0\" encoding=\"iso-8859-1\" standalone=\"no\"?>");
    463         outputFile.println("<comments");
    464         outputFile.println("  xmlns:xsi='" + RootDocToXML.baseURI + "/2001/XMLSchema-instance'");
    465         outputFile.println("  xsi:noNamespaceSchemaLocation='comments.xsd'");
    466         // Extract the identifier from the filename by removing the suffix
    467         int idx = filename.lastIndexOf('.');
    468         String apiIdentifier = filename.substring(0, idx);
    469         // Also remove the output directory and directory separator if present
    470         if (HTMLReportGenerator.commentsDir != null)
    471 	    apiIdentifier = apiIdentifier.substring(HTMLReportGenerator.commentsDir.length()+1);
    472         else if (HTMLReportGenerator.outputDir != null)
    473             apiIdentifier = apiIdentifier.substring(HTMLReportGenerator.outputDir.length()+1);
    474         // Also remove "user_comments_for_"
    475         apiIdentifier = apiIdentifier.substring(18);
    476         outputFile.println("  name=\"" + apiIdentifier + "\"");
    477         outputFile.println("  jdversion=\"" + JDiff.version + "\">");
    478         outputFile.println();
    479         outputFile.println("<!-- Use this file to enter an API change description. For example, when you remove a class, ");
    480         outputFile.println("     you can enter a comment for that class that points developers to the replacement class. ");
    481         outputFile.println("     You can also provide a change summary for modified API, to give an overview of the changes ");
    482         outputFile.println("     why they were made, workarounds, etc.  -->");
    483         outputFile.println();
    484         outputFile.println("<!-- When the API diffs report is generated, the comments in this file get added to the tables of ");
    485         outputFile.println("     removed, added, and modified packages, classes, methods, and fields. This file does not ship ");
    486         outputFile.println("     with the final report. -->");
    487         outputFile.println();
    488         outputFile.println("<!-- The id attribute in an identifier element identifies the change as noted in the report. ");
    489         outputFile.println("     An id has the form package[.class[.[ctor|method|field].signature]], where [] indicates optional ");
    490         outputFile.println("     text. A comment element can have multiple identifier elements, which will will cause the same ");
    491         outputFile.println("     text to appear at each place in the report, but will be converted to separate comments when the ");
    492         outputFile.println("     comments file is used. -->");
    493         outputFile.println();
    494         outputFile.println("<!-- HTML tags in the text field will appear in the report. You also need to close p HTML elements, ");
    495         outputFile.println("     used for paragraphs - see the top-level documentation. -->");
    496         outputFile.println();
    497         outputFile.println("<!-- You can include standard javadoc links in your change descriptions. You can use the @first command  ");
    498         outputFile.println("     to cause jdiff to include the first line of the API documentation. You also need to close p HTML ");
    499         outputFile.println("     elements, used for paragraphs - see the top-level documentation. -->");
    500         outputFile.println();
    501     }
    502 
    503     /**
    504      * Emit the XML footer.
    505      */
    506     public void emitXMLFooter() {
    507         outputFile.println();
    508         outputFile.println("</comments>");
    509     }
    510 
    511     private static List oldAPIList = null;
    512     private static List newAPIList = null;
    513 
    514     /**
    515      * Return true if the given HTML tag has no separate </tag> end element.
    516      *
    517      * If you want to be able to use sloppy HTML in your comments, then you can
    518      * add the element, e.g. li back into the condition here. However, if you
    519      * then become more careful and do provide the closing tag, the output is
    520      * generally just the closing tag, which is incorrect.
    521      *
    522      * tag.equalsIgnoreCase("tr") || // Is sometimes minimized
    523      * tag.equalsIgnoreCase("th") || // Is sometimes minimized
    524      * tag.equalsIgnoreCase("td") || // Is sometimes minimized
    525      * tag.equalsIgnoreCase("dt") || // Is sometimes minimized
    526      * tag.equalsIgnoreCase("dd") || // Is sometimes minimized
    527      * tag.equalsIgnoreCase("img") || // Is sometimes minimized
    528      * tag.equalsIgnoreCase("code") || // Is sometimes minimized (error)
    529      * tag.equalsIgnoreCase("font") || // Is sometimes minimized (error)
    530      * tag.equalsIgnoreCase("ul") || // Is sometimes minimized
    531      * tag.equalsIgnoreCase("ol") || // Is sometimes minimized
    532      * tag.equalsIgnoreCase("li") // Is sometimes minimized
    533      */
    534     public static boolean isMinimizedTag(String tag) {
    535         if (tag.equalsIgnoreCase("p") ||
    536             tag.equalsIgnoreCase("br") ||
    537             tag.equalsIgnoreCase("hr")
    538             ) {
    539             return true;
    540 	}
    541         return false;
    542     }
    543 
    544     /**
    545      * The file where the XML representing the new Comments object is stored.
    546      */
    547     private static PrintWriter outputFile = null;
    548 
    549 }
    550 
    551 
    552