diff options
Diffstat (limited to 'src/jdiff/Comments.java')
-rwxr-xr-x | src/jdiff/Comments.java | 551 |
1 files changed, 551 insertions, 0 deletions
diff --git a/src/jdiff/Comments.java b/src/jdiff/Comments.java new file mode 100755 index 0000000..066e4f0 --- /dev/null +++ b/src/jdiff/Comments.java @@ -0,0 +1,551 @@ +package jdiff; + +import java.io.*; +import java.util.*; + +/* For SAX XML parsing */ +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.XMLReader; +import org.xml.sax.InputSource; +import org.xml.sax.helpers.*; + +/** + * Creates a Comments from an XML file. The Comments object is the internal + * representation of the comments for the changes. + * All methods in this class for populating a Comments object are static. + * + * See the file LICENSE.txt for copyright details. + * @author Matthew Doar, mdoar@pobox.com + */ +public class Comments { + + /** + * All the possible comments known about, accessible by the commentID. + */ + public static Hashtable allPossibleComments = new Hashtable(); + + /** The old Comments object which is populated from the file read in. */ + private static Comments oldComments_ = null; + + /** Default constructor. */ + public Comments() { + commentsList_ = new ArrayList(); // SingleComment[] + } + + // The list of comments elements associated with this objects + public List commentsList_ = null; // SingleComment[] + + /** + * Read the file where the XML for comments about the changes between + * the old API and new API is stored and create a Comments object for + * it. The Comments object may be null if no file exists. + */ + public static Comments readFile(String filename) { + // If validation is desired, write out the appropriate comments.xsd + // file in the same directory as the comments XML file. + if (XMLToAPI.validateXML) { + writeXSD(filename); + } + + // If the file does not exist, return null + File f = new File(filename); + if (!f.exists()) + return null; + + // The instance of the Comments object which is populated from the file. + oldComments_ = new Comments(); + try { + DefaultHandler handler = new CommentsHandler(oldComments_); + XMLReader parser = null; + try { + String parserName = System.getProperty("org.xml.sax.driver"); + if (parserName == null) { + parser = org.xml.sax.helpers.XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser"); + } else { + // Let the underlying mechanisms try to work out which + // class to instantiate + parser = org.xml.sax.helpers.XMLReaderFactory.createXMLReader(); + } + } catch (SAXException saxe) { + System.out.println("SAXException: " + saxe); + saxe.printStackTrace(); + System.exit(1); + } + + if (XMLToAPI.validateXML) { + parser.setFeature("http://xml.org/sax/features/namespaces", true); + parser.setFeature("http://xml.org/sax/features/validation", true); + parser.setFeature("http://apache.org/xml/features/validation/schema", true); + } + parser.setContentHandler(handler); + parser.setErrorHandler(handler); + parser.parse(new InputSource(new FileInputStream(new File(filename)))); + } catch(org.xml.sax.SAXNotRecognizedException snre) { + System.out.println("SAX Parser does not recognize feature: " + snre); + snre.printStackTrace(); + System.exit(1); + } catch(org.xml.sax.SAXNotSupportedException snse) { + System.out.println("SAX Parser feature is not supported: " + snse); + snse.printStackTrace(); + System.exit(1); + } catch(org.xml.sax.SAXException saxe) { + System.out.println("SAX Exception parsing file '" + filename + "' : " + saxe); + saxe.printStackTrace(); + System.exit(1); + } catch(java.io.IOException ioe) { + System.out.println("IOException parsing file '" + filename + "' : " + ioe); + ioe.printStackTrace(); + System.exit(1); + } + + Collections.sort(oldComments_.commentsList_); + return oldComments_; + } //readFile() + + /** + * Write the XML Schema file used for validation. + */ + public static void writeXSD(String filename) { + String xsdFileName = filename; + int idx = xsdFileName.lastIndexOf('\\'); + int idx2 = xsdFileName.lastIndexOf('/'); + if (idx == -1 && idx2 == -1) { + xsdFileName = ""; + } else if (idx == -1 && idx2 != -1) { + xsdFileName = xsdFileName.substring(0, idx2+1); + } else if (idx != -1 && idx2 == -1) { + xsdFileName = xsdFileName.substring(0, idx+1); + } else if (idx != -1 && idx2 != -1) { + int max = idx2 > idx ? idx2 : idx; + xsdFileName = xsdFileName.substring(0, max+1); + } + xsdFileName += "comments.xsd"; + try { + FileOutputStream fos = new FileOutputStream(xsdFileName); + PrintWriter xsdFile = new PrintWriter(fos); + // The contents of the comments.xsd file + xsdFile.println("<?xml version=\"1.0\" encoding=\"iso-8859-1\" standalone=\"no\"?>"); + xsdFile.println("<xsd:schema xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"); + xsdFile.println(); + xsdFile.println("<xsd:annotation>"); + xsdFile.println(" <xsd:documentation>"); + xsdFile.println(" Schema for JDiff comments."); + xsdFile.println(" </xsd:documentation>"); + xsdFile.println("</xsd:annotation>"); + xsdFile.println(); + xsdFile.println("<xsd:element name=\"comments\" type=\"commentsType\"/>"); + xsdFile.println(); + xsdFile.println("<xsd:complexType name=\"commentsType\">"); + xsdFile.println(" <xsd:sequence>"); + xsdFile.println(" <xsd:element name=\"comment\" type=\"commentType\" minOccurs='0' maxOccurs='unbounded'/>"); + xsdFile.println(" </xsd:sequence>"); + xsdFile.println(" <xsd:attribute name=\"name\" type=\"xsd:string\"/>"); + xsdFile.println(" <xsd:attribute name=\"jdversion\" type=\"xsd:string\"/>"); + xsdFile.println("</xsd:complexType>"); + xsdFile.println(); + xsdFile.println("<xsd:complexType name=\"commentType\">"); + xsdFile.println(" <xsd:sequence>"); + xsdFile.println(" <xsd:element name=\"identifier\" type=\"identifierType\" minOccurs='1' maxOccurs='unbounded'/>"); + xsdFile.println(" <xsd:element name=\"text\" type=\"xsd:string\" minOccurs='1' maxOccurs='1'/>"); + xsdFile.println(" </xsd:sequence>"); + xsdFile.println("</xsd:complexType>"); + xsdFile.println(); + xsdFile.println("<xsd:complexType name=\"identifierType\">"); + xsdFile.println(" <xsd:attribute name=\"id\" type=\"xsd:string\"/>"); + xsdFile.println("</xsd:complexType>"); + xsdFile.println(); + xsdFile.println("</xsd:schema>"); + xsdFile.close(); + } catch(IOException e) { + System.out.println("IO Error while attempting to create " + xsdFileName); + System.out.println("Error: " + e.getMessage()); + System.exit(1); + } + } + +// +// Methods to add data to a Comments object. Called by the XML parser and the +// report generator. +// + + /** + * Add the SingleComment object to the list of comments kept by this + * object. + */ + public void addComment(SingleComment comment) { + commentsList_.add(comment); + } + +// +// Methods to get data from a Comments object. Called by the report generator +// + + /** + * The text placed into XML comments file where there is no comment yet. + * It never appears in reports. + */ + public static final String placeHolderText = "InsertCommentsHere"; + + /** + * Return the comment associated with the given id in the Comment object. + * If there is no such comment, return the placeHolderText. + */ + public static String getComment(Comments comments, String id) { + if (comments == null) + return placeHolderText; + SingleComment key = new SingleComment(id, null); + int idx = Collections.binarySearch(comments.commentsList_, key); + if (idx < 0) { + return placeHolderText; + } else { + int startIdx = comments.commentsList_.indexOf(key); + int endIdx = comments.commentsList_.indexOf(key); + int numIdx = endIdx - startIdx + 1; + if (numIdx != 1) { + System.out.println("Warning: " + numIdx + " identical ids in the existing comments file. Using the first instance."); + } + SingleComment singleComment = (SingleComment)(comments.commentsList_.get(idx)); + // Convert @link tags to links + return singleComment.text_; + } + } + + /** + * Convert @link tags to HTML links. + */ + public static String convertAtLinks(String text, String currentElement, + PackageAPI pkg, ClassAPI cls) { + if (text == null) + return null; + + StringBuffer result = new StringBuffer(); + + int state = -1; + + final int NORMAL_TEXT = -1; + final int IN_LINK = 1; + final int IN_LINK_IDENTIFIER = 2; + final int IN_LINK_IDENTIFIER_REFERENCE = 3; + final int IN_LINK_IDENTIFIER_REFERENCE_PARAMS = 6; + final int IN_LINK_LINKTEXT = 4; + final int END_OF_LINK = 5; + + StringBuffer identifier = null; + StringBuffer identifierReference = null; + StringBuffer linkText = null; + + // Figure out relative reference if required. + String ref = ""; + if (currentElement.compareTo("class") == 0 || + currentElement.compareTo("interface") == 0) { + ref = pkg.name_ + "." + cls.name_ + "."; + } else if (currentElement.compareTo("package") == 0) { + ref = pkg.name_ + "."; + } + ref = ref.replace('.', '/'); + + for (int i=0; i < text.length(); i++) { + char c = text.charAt( i); + char nextChar = i < text.length()-1 ? text.charAt( i+1) : (char)-1; + int remainingChars = text.length() - i; + + switch (state) { + case NORMAL_TEXT: + if (c == '{' && remainingChars >= 5) { + if ("{@link".equals(text.substring(i, i + 6))) { + state = IN_LINK; + identifier = null; + identifierReference = null; + linkText = null; + i += 5; + continue; + } + } + result.append( c); + break; + case IN_LINK: + if (Character.isWhitespace(nextChar)) continue; + if (nextChar == '}') { + // End of the link + state = END_OF_LINK; + } else if (!Character.isWhitespace(nextChar)) { + state = IN_LINK_IDENTIFIER; + } + break; + case IN_LINK_IDENTIFIER: + if (identifier == null) { + identifier = new StringBuffer(); + } + + if (c == '#') { + // We have a reference. + state = IN_LINK_IDENTIFIER_REFERENCE; + // Don't append # + continue; + } else if (Character.isWhitespace(c)) { + // We hit some whitespace: the next character is the beginning + // of the link text. + state = IN_LINK_LINKTEXT; + continue; + } + identifier.append(c); + // Check for a } that ends the link. + if (nextChar == '}') { + state = END_OF_LINK; + } + break; + case IN_LINK_IDENTIFIER_REFERENCE: + if (identifierReference == null) { + identifierReference = new StringBuffer(); + } + if (Character.isWhitespace(c)) { + state = IN_LINK_LINKTEXT; + continue; + } + identifierReference.append(c); + + if (c == '(') { + state = IN_LINK_IDENTIFIER_REFERENCE_PARAMS; + } + + if (nextChar == '}') { + state = END_OF_LINK; + } + break; + case IN_LINK_IDENTIFIER_REFERENCE_PARAMS: + // We're inside the parameters of a reference. Spaces are allowed. + if (c == ')') { + state = IN_LINK_IDENTIFIER_REFERENCE; + } + identifierReference.append(c); + if (nextChar == '}') { + state = END_OF_LINK; + } + break; + case IN_LINK_LINKTEXT: + if (linkText == null) linkText = new StringBuffer(); + + linkText.append(c); + + if (nextChar == '}') { + state = END_OF_LINK; + } + break; + case END_OF_LINK: + if (identifier != null) { + result.append("<A HREF=\""); + result.append(HTMLReportGenerator.newDocPrefix); + result.append(ref); + result.append(identifier.toString().replace('.', '/')); + result.append(".html"); + if (identifierReference != null) { + result.append("#"); + result.append(identifierReference); + } + result.append("\">"); // target=_top? + + result.append("<TT>"); + if (linkText != null) { + result.append(linkText); + } else { + result.append(identifier); + if (identifierReference != null) { + result.append("."); + result.append(identifierReference); + } + } + result.append("</TT>"); + result.append("</A>"); + } + state = NORMAL_TEXT; + break; + } + } + return result.toString(); + } + +// +// Methods to write a Comments object out to a file. +// + + /** + * Write the XML representation of comments to a file. + * + * @param outputFileName The name of the comments file. + * @param oldComments The old comments on the changed APIs. + * @param newComments The new comments on the changed APIs. + * @return true if no problems encountered + */ + public static boolean writeFile(String outputFileName, + Comments newComments) { + try { + FileOutputStream fos = new FileOutputStream(outputFileName); + outputFile = new PrintWriter(fos); + newComments.emitXMLHeader(outputFileName); + newComments.emitComments(); + newComments.emitXMLFooter(); + outputFile.close(); + } catch(IOException e) { + System.out.println("IO Error while attempting to create " + outputFileName); + System.out.println("Error: "+ e.getMessage()); + System.exit(1); + } + return true; + } + + /** + * Write the Comments object out in XML. + */ + public void emitComments() { + Iterator iter = commentsList_.iterator(); + while (iter.hasNext()) { + SingleComment currComment = (SingleComment)(iter.next()); + if (!currComment.isUsed_) + outputFile.println("<!-- This comment is no longer used "); + outputFile.println("<comment>"); + outputFile.println(" <identifier id=\"" + currComment.id_ + "\"/>"); + outputFile.println(" <text>"); + outputFile.println(" " + currComment.text_); + outputFile.println(" </text>"); + outputFile.println("</comment>"); + if (!currComment.isUsed_) + outputFile.println("-->"); + } + } + + /** + * Dump the contents of a Comments object out for inspection. + */ + public void dump() { + Iterator iter = commentsList_.iterator(); + int i = 0; + while (iter.hasNext()) { + i++; + SingleComment currComment = (SingleComment)(iter.next()); + System.out.println("Comment " + i); + System.out.println("id = " + currComment.id_); + System.out.println("text = \"" + currComment.text_ + "\""); + System.out.println("isUsed = " + currComment.isUsed_); + } + } + + /** + * Emit messages about which comments are now unused and which are new. + */ + public static void noteDifferences(Comments oldComments, Comments newComments) { + if (oldComments == null) { + System.out.println("Note: all the comments have been newly generated"); + return; + } + + // See which comment ids are no longer used and add those entries to + // the new comments, marking them as unused. + Iterator iter = oldComments.commentsList_.iterator(); + while (iter.hasNext()) { + SingleComment oldComment = (SingleComment)(iter.next()); + int idx = Collections.binarySearch(newComments.commentsList_, oldComment); + if (idx < 0) { + System.out.println("Warning: comment \"" + oldComment.id_ + "\" is no longer used."); + oldComment.isUsed_ = false; + newComments.commentsList_.add(oldComment); + } + } + + } + + /** + * Emit the XML header. + */ + public void emitXMLHeader(String filename) { + outputFile.println("<?xml version=\"1.0\" encoding=\"iso-8859-1\" standalone=\"no\"?>"); + outputFile.println("<comments"); + outputFile.println(" xmlns:xsi='" + RootDocToXML.baseURI + "/2001/XMLSchema-instance'"); + outputFile.println(" xsi:noNamespaceSchemaLocation='comments.xsd'"); + // Extract the identifier from the filename by removing the suffix + int idx = filename.lastIndexOf('.'); + String apiIdentifier = filename.substring(0, idx); + // Also remove the output directory and directory separator if present + if (HTMLReportGenerator.commentsDir != null) + apiIdentifier = apiIdentifier.substring(HTMLReportGenerator.commentsDir.length()+1); + else if (HTMLReportGenerator.outputDir != null) + apiIdentifier = apiIdentifier.substring(HTMLReportGenerator.outputDir.length()+1); + // Also remove "user_comments_for_" + apiIdentifier = apiIdentifier.substring(18); + outputFile.println(" name=\"" + apiIdentifier + "\""); + outputFile.println(" jdversion=\"" + JDiff.version + "\">"); + outputFile.println(); + outputFile.println("<!-- Use this file to enter an API change description. For example, when you remove a class, "); + outputFile.println(" you can enter a comment for that class that points developers to the replacement class. "); + outputFile.println(" You can also provide a change summary for modified API, to give an overview of the changes "); + outputFile.println(" why they were made, workarounds, etc. -->"); + outputFile.println(); + outputFile.println("<!-- When the API diffs report is generated, the comments in this file get added to the tables of "); + outputFile.println(" removed, added, and modified packages, classes, methods, and fields. This file does not ship "); + outputFile.println(" with the final report. -->"); + outputFile.println(); + outputFile.println("<!-- The id attribute in an identifier element identifies the change as noted in the report. "); + outputFile.println(" An id has the form package[.class[.[ctor|method|field].signature]], where [] indicates optional "); + outputFile.println(" text. A comment element can have multiple identifier elements, which will will cause the same "); + outputFile.println(" text to appear at each place in the report, but will be converted to separate comments when the "); + outputFile.println(" comments file is used. -->"); + outputFile.println(); + outputFile.println("<!-- HTML tags in the text field will appear in the report. You also need to close p HTML elements, "); + outputFile.println(" used for paragraphs - see the top-level documentation. -->"); + outputFile.println(); + outputFile.println("<!-- You can include standard javadoc links in your change descriptions. You can use the @first command "); + outputFile.println(" to cause jdiff to include the first line of the API documentation. You also need to close p HTML "); + outputFile.println(" elements, used for paragraphs - see the top-level documentation. -->"); + outputFile.println(); + } + + /** + * Emit the XML footer. + */ + public void emitXMLFooter() { + outputFile.println(); + outputFile.println("</comments>"); + } + + private static List oldAPIList = null; + private static List newAPIList = null; + + /** + * Return true if the given HTML tag has no separate </tag> end element. + * + * If you want to be able to use sloppy HTML in your comments, then you can + * add the element, e.g. li back into the condition here. However, if you + * then become more careful and do provide the closing tag, the output is + * generally just the closing tag, which is incorrect. + * + * tag.equalsIgnoreCase("tr") || // Is sometimes minimized + * tag.equalsIgnoreCase("th") || // Is sometimes minimized + * tag.equalsIgnoreCase("td") || // Is sometimes minimized + * tag.equalsIgnoreCase("dt") || // Is sometimes minimized + * tag.equalsIgnoreCase("dd") || // Is sometimes minimized + * tag.equalsIgnoreCase("img") || // Is sometimes minimized + * tag.equalsIgnoreCase("code") || // Is sometimes minimized (error) + * tag.equalsIgnoreCase("font") || // Is sometimes minimized (error) + * tag.equalsIgnoreCase("ul") || // Is sometimes minimized + * tag.equalsIgnoreCase("ol") || // Is sometimes minimized + * tag.equalsIgnoreCase("li") // Is sometimes minimized + */ + public static boolean isMinimizedTag(String tag) { + if (tag.equalsIgnoreCase("p") || + tag.equalsIgnoreCase("br") || + tag.equalsIgnoreCase("hr") + ) { + return true; + } + return false; + } + + /** + * The file where the XML representing the new Comments object is stored. + */ + private static PrintWriter outputFile = null; + +} + + |