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