1package jdiff; 2 3import java.io.*; 4import java.util.*; 5 6/* For SAX XML parsing */ 7import org.xml.sax.Attributes; 8import org.xml.sax.SAXException; 9import org.xml.sax.SAXParseException; 10import org.xml.sax.XMLReader; 11import org.xml.sax.InputSource; 12import 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@pobox.com 21 */ 22public 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