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