package jdiff;
|
|
import java.io.*;
|
import java.util.*;
|
import javax.xml.parsers.ParserConfigurationException;
|
|
/* 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 {
|
parser = javax.xml.parsers.SAXParserFactory.newInstance().newSAXParser().getXMLReader();
|
} catch (SAXException saxe) {
|
System.out.println("SAXException: " + saxe);
|
saxe.printStackTrace();
|
System.exit(1);
|
} catch (ParserConfigurationException pce) {
|
System.out.println("ParserConfigurationException: " + pce);
|
pce.printStackTrace();
|
System.exit(1);
|
}
|
|
if (XMLToAPI.validateXML) {
|
parser.setFeature("https://xml.org/sax/features/namespaces", true);
|
parser.setFeature("https://xml.org/sax/features/validation", true);
|
parser.setFeature("https://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=\"https://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;
|
|
}
|