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