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