1package jdiff;
2
3import java.util.*;
4
5/**
6 * This class contains method to compare two API objects.
7 * The differences are stored in an APIDiff object.
8 *
9 * See the file LICENSE.txt for copyright details.
10 * @author Matthew Doar, mdoar@pobox.com
11 */
12public class APIComparator {
13
14    /**
15     * Top-level object representing the differences between two APIs.
16     * It is this object which is used to generate the report later on.
17     */
18    public APIDiff apiDiff;
19
20    /**
21     * Package-level object representing the differences between two packages.
22     * This object is also used to determine which file to write documentation
23     * differences into.
24     */
25    public PackageDiff pkgDiff;
26
27    /** Default constructor. */
28    public APIComparator() {
29        apiDiff = new APIDiff();
30    }
31
32    /** For easy local access to the old API object. */
33    private static API oldAPI_;
34    /** For easy local access to the new API object. */
35    private static API newAPI_;
36
37    /**
38     * Compare two APIs.
39     */
40    public void compareAPIs(API oldAPI, API newAPI) {
41        System.out.println("JDiff: comparing the old and new APIs ...");
42        oldAPI_ = oldAPI;
43        newAPI_ = newAPI;
44
45        double differs = 0.0;
46
47        apiDiff.oldAPIName_ = oldAPI.name_;
48        apiDiff.newAPIName_ = newAPI.name_;
49
50        Collections.sort(oldAPI.packages_);
51        Collections.sort(newAPI.packages_);
52
53        // Find packages which were removed in the new API
54        Iterator iter = oldAPI.packages_.iterator();
55        while (iter.hasNext()) {
56            PackageAPI oldPkg = (PackageAPI)(iter.next());
57            // This search is looking for an *exact* match. This is true in
58            // all the *API classes.
59            int idx = Collections.binarySearch(newAPI.packages_, oldPkg);
60            if (idx < 0) {
61                // If there an instance of a package with the same name
62                // in both the old and new API, then treat it as changed,
63                // rather than removed and added. There will never be more than
64                // one instance of a package with the same name in an API.
65                int existsNew = newAPI.packages_.indexOf(oldPkg);
66                if (existsNew != -1) {
67                    // Package by the same name exists in both APIs
68                    // but there has been some or other change.
69                    differs += 2.0 * comparePackages(oldPkg, (PackageAPI)(newAPI.packages_.get(existsNew)));
70                }  else {
71                    if (trace)
72                        System.out.println("Package " + oldPkg.name_ + " was removed");
73                    apiDiff.packagesRemoved.add(oldPkg);
74                    differs += 1.0;
75                }
76            } else {
77                // The package exists unchanged in name or doc, but may
78                // differ in classes and their members, so it still needs to
79                // be compared.
80                differs += 2.0 * comparePackages(oldPkg, (PackageAPI)(newAPI.packages_.get(idx)));
81            }
82        } // while (iter.hasNext())
83
84        // Find packages which were added or changed in the new API
85        iter = newAPI.packages_.iterator();
86        while (iter.hasNext()) {
87            PackageAPI newPkg = (PackageAPI)(iter.next());
88            int idx = Collections.binarySearch(oldAPI.packages_, newPkg);
89            if (idx < 0) {
90                // See comments above
91                int existsOld = oldAPI.packages_.indexOf(newPkg);
92                if (existsOld != -1) {
93                    // Don't mark a package as added or compare it
94                    // if it was already marked as changed
95                } else {
96                    if (trace)
97                        System.out.println("Package " + newPkg.name_ + " was added");
98                    apiDiff.packagesAdded.add(newPkg);
99                    differs += 1.0;
100                }
101            } else {
102                // It will already have been compared above.
103            }
104        } // while (iter.hasNext())
105
106        // Now that the numbers of members removed and added are known
107        // we can deduce more information about changes.
108        MergeChanges.mergeRemoveAdd(apiDiff);
109
110// The percent change statistic reported for all elements in each API is
111// defined recursively as follows:
112//
113// %age change = 100 * (added + removed + 2*changed)
114//               -----------------------------------
115//               sum of public elements in BOTH APIs
116//
117// The definition ensures that if all classes are removed and all new classes
118// added, the change will be 100%.
119// Evaluation of the visibility of elements has already been done when the
120// XML was written out.
121// Note that this doesn't count changes in the modifiers of classes and
122// packages. Other changes in members are counted.
123        Long denom = new Long(oldAPI.packages_.size() + newAPI.packages_.size());
124        // This should never be zero because an API always has packages?
125        if (denom.intValue() == 0) {
126            System.out.println("Error: no packages found in the APIs.");
127            return;
128        }
129        if (trace)
130            System.out.println("Top level changes: " + differs + "/" + denom.intValue());
131        differs = (100.0 * differs)/denom.doubleValue();
132
133        // Some differences such as documentation changes are not tracked in
134        // the difference statistic, so a value of 0.0 does not mean that there
135        // were no differences between the APIs.
136        apiDiff.pdiff = differs;
137        Double percentage = new Double(differs);
138        int approxPercentage = percentage.intValue();
139        if (approxPercentage == 0)
140            System.out.println(" Approximately " + percentage + "% difference between the APIs");
141        else
142            System.out.println(" Approximately " + approxPercentage + "% difference between the APIs");
143
144        Diff.closeDiffFile();
145    }
146
147    /**
148     * Compare two packages.
149     */
150    public double comparePackages(PackageAPI oldPkg, PackageAPI newPkg) {
151        if (trace)
152            System.out.println("Comparing old package " + oldPkg.name_ +
153                               " and new package " + newPkg.name_);
154        pkgDiff = new PackageDiff(oldPkg.name_);
155        double differs = 0.0;
156
157        Collections.sort(oldPkg.classes_);
158        Collections.sort(newPkg.classes_);
159
160        // Find classes which were removed in the new package
161        Iterator iter = oldPkg.classes_.iterator();
162        while (iter.hasNext()) {
163            ClassAPI oldClass = (ClassAPI)(iter.next());
164            // This search is looking for an *exact* match. This is true in
165            // all the *API classes.
166            int idx = Collections.binarySearch(newPkg.classes_, oldClass);
167            if (idx < 0) {
168                // If there an instance of a class with the same name
169                // in both the old and new package, then treat it as changed,
170                // rather than removed and added. There will never be more than
171                // one instance of a class with the same name in a package.
172                int existsNew = newPkg.classes_.indexOf(oldClass);
173                if (existsNew != -1) {
174                    // Class by the same name exists in both packages
175                    // but there has been some or other change.
176                    differs += 2.0 * compareClasses(oldClass, (ClassAPI)(newPkg.classes_.get(existsNew)), pkgDiff);
177                }  else {
178                    if (trace)
179                        System.out.println("  Class " + oldClass.name_ + " was removed");
180                    pkgDiff.classesRemoved.add(oldClass);
181                    differs += 1.0;
182                }
183            } else {
184                // The class exists unchanged in name or modifiers, but may
185                // differ in members, so it still needs to be compared.
186                differs += 2.0 * compareClasses(oldClass, (ClassAPI)(newPkg.classes_.get(idx)), pkgDiff);
187            }
188        } // while (iter.hasNext())
189
190        // Find classes which were added or changed in the new package
191        iter = newPkg.classes_.iterator();
192        while (iter.hasNext()) {
193            ClassAPI newClass = (ClassAPI)(iter.next());
194            int idx = Collections.binarySearch(oldPkg.classes_, newClass);
195            if (idx < 0) {
196                // See comments above
197                int existsOld = oldPkg.classes_.indexOf(newClass);
198                if (existsOld != -1) {
199                    // Don't mark a class as added or compare it
200                    // if it was already marked as changed
201                } else {
202                    if (trace)
203                        System.out.println("  Class " + newClass.name_ + " was added");
204                    pkgDiff.classesAdded.add(newClass);
205                    differs += 1.0;
206                }
207            } else {
208                // It will already have been compared above.
209            }
210        } // while (iter.hasNext())
211
212        // Check if the only change was in documentation. Bug 472521.
213        boolean differsFlag = false;
214        if (docChanged(oldPkg.doc_, newPkg.doc_)) {
215            String link = "<a href=\"pkg_" + oldPkg.name_ + HTMLReportGenerator.reportFileExt + "\" class=\"hiddenlink\">";
216            String id = oldPkg.name_ + "!package";
217            String title = link + "Package <b>" + oldPkg.name_ + "</b></a>";
218            pkgDiff.documentationChange_ = Diff.saveDocDiffs(pkgDiff.name_, null, oldPkg.doc_, newPkg.doc_, id, title);
219            differsFlag = true;
220        }
221
222        // Only add to the parent Diff object if some difference has been found
223        if (differs != 0.0 || differsFlag)
224            apiDiff.packagesChanged.add(pkgDiff);
225
226        Long denom = new Long(oldPkg.classes_.size() + newPkg.classes_.size());
227        // This should never be zero because a package always has classes?
228        if (denom.intValue() == 0) {
229            System.out.println("Warning: no classes found in the package " + oldPkg.name_);
230            return 0.0;
231        }
232        if (trace)
233            System.out.println("Package " + pkgDiff.name_ + " had a difference of " + differs + "/" + denom.intValue());
234        pkgDiff.pdiff = 100.0 * differs/denom.doubleValue();
235        return differs/denom.doubleValue();
236    } // comparePackages()
237
238    /**
239     * Compare two classes.
240     *
241     * Need to compare constructors, methods and fields.
242     */
243    public double compareClasses(ClassAPI oldClass, ClassAPI newClass, PackageDiff pkgDiff) {
244        if (trace)
245            System.out.println("  Comparing old class " + oldClass.name_ +
246                               " and new class " + newClass.name_);
247        boolean differsFlag = false;
248        double differs = 0.0;
249        ClassDiff classDiff = new ClassDiff(oldClass.name_);
250        classDiff.isInterface_ = newClass.isInterface_; // Used in the report
251
252        // Track changes in modifiers - class or interface
253        if (oldClass.isInterface_ != newClass.isInterface_) {
254            classDiff.modifiersChange_  = "Changed from ";
255            if (oldClass.isInterface_)
256                classDiff.modifiersChange_ += "an interface to a class.";
257            else
258                classDiff.modifiersChange_ += "a class to an interface.";
259            differsFlag = true;
260        }
261        // Track changes in inheritance
262        String inheritanceChange = ClassDiff.diff(oldClass, newClass);
263        if (inheritanceChange != null) {
264            classDiff.inheritanceChange_ = inheritanceChange;
265            differsFlag = true;
266        }
267        // Abstract or not
268        if (oldClass.isAbstract_ != newClass.isAbstract_) {
269            String changeText = "";
270            if (oldClass.isAbstract_)
271                changeText += "Changed from abstract to non-abstract.";
272            else
273                changeText += "Changed from non-abstract to abstract.";
274            classDiff.addModifiersChange(changeText);
275            differsFlag = true;
276        }
277        // Track changes in documentation
278        if (docChanged(oldClass.doc_, newClass.doc_)) {
279            String fqName = pkgDiff.name_ + "." + classDiff.name_;
280            String link = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "\" class=\"hiddenlink\">";
281            String id = pkgDiff.name_ + "." + classDiff.name_ + "!class";
282            String title = link + "Class <b>" + classDiff.name_ + "</b></a>";
283            classDiff.documentationChange_ = Diff.saveDocDiffs(pkgDiff.name_,
284 classDiff.name_, oldClass.doc_, newClass.doc_, id, title);
285            differsFlag = true;
286        }
287        // All other modifiers
288        String modifiersChange = oldClass.modifiers_.diff(newClass.modifiers_);
289        if (modifiersChange != null) {
290            differsFlag = true;
291            if (modifiersChange.indexOf("Change from deprecated to undeprecated") != -1) {
292                System.out.println("JDiff: warning: change from deprecated to undeprecated for class " + pkgDiff.name_ + "." + newClass.name_);
293
294            }
295        }
296        classDiff.addModifiersChange(modifiersChange);
297
298        // Track changes in members
299        boolean differsCtors =
300            compareAllCtors(oldClass, newClass, classDiff);
301        boolean differsMethods =
302            compareAllMethods(oldClass, newClass, classDiff);
303        boolean differsFields =
304            compareAllFields(oldClass, newClass, classDiff);
305        if (differsCtors || differsMethods || differsFields)
306            differsFlag = true;
307
308        if (trace) {
309            System.out.println("  Ctors differ? " + differsCtors +
310                ", Methods differ? " + differsMethods +
311                ", Fields differ? " + differsFields);
312        }
313
314        // Only add to the parent if some difference has been found
315        if (differsFlag)
316            pkgDiff.classesChanged.add(classDiff);
317
318        // Get the numbers of affected elements from the classDiff object
319         differs =
320            classDiff.ctorsRemoved.size() + classDiff.ctorsAdded.size() +
321            classDiff.ctorsChanged.size() +
322            classDiff.methodsRemoved.size() + classDiff.methodsAdded.size() +
323            classDiff.methodsChanged.size() +
324            classDiff.fieldsRemoved.size() + classDiff.fieldsAdded.size() +
325            classDiff.fieldsChanged.size();
326         Long denom = new Long(
327             oldClass.ctors_.size() +
328             numLocalMethods(oldClass.methods_) +
329             numLocalFields(oldClass.fields_) +
330             newClass.ctors_.size() +
331             numLocalMethods(newClass.methods_) +
332             numLocalFields(newClass.fields_));
333         if (denom.intValue() == 0) {
334             // This is probably a placeholder interface, but documentation
335             // or modifiers etc may have changed
336             if (differsFlag) {
337                 classDiff.pdiff = 0.0; // 100.0 is too much
338                 return 1.0;
339             } else {
340                 return 0.0;
341             }
342         }
343         // Handle the case where the only change is in documentation or
344         // the modifiers
345         if (differsFlag && differs == 0.0) {
346             differs = 1.0;
347         }
348         if (trace)
349             System.out.println("  Class " + classDiff.name_ + " had a difference of " + differs + "/" + denom.intValue());
350         classDiff.pdiff = 100.0 * differs/denom.doubleValue();
351         return differs/denom.doubleValue();
352    } // compareClasses()
353
354    /**
355     * Compare all the constructors in two classes.
356     *
357     * The compareTo method in the ConstructorAPI class acts only upon the type.
358     */
359    public boolean compareAllCtors(ClassAPI oldClass, ClassAPI newClass,
360                                   ClassDiff classDiff) {
361        if (trace)
362            System.out.println("    Comparing constructors: #old " +
363              oldClass.ctors_.size() + ", #new " + newClass.ctors_.size());
364        boolean differs = false;
365        boolean singleCtor = false; // Set if there is only one ctor
366
367        Collections.sort(oldClass.ctors_);
368        Collections.sort(newClass.ctors_);
369
370        // Find ctors which were removed in the new class
371        Iterator iter = oldClass.ctors_.iterator();
372        while (iter.hasNext()) {
373            ConstructorAPI oldCtor = (ConstructorAPI)(iter.next());
374            int idx = Collections.binarySearch(newClass.ctors_, oldCtor);
375            if (idx < 0) {
376                int oldSize = oldClass.ctors_.size();
377                int newSize = newClass.ctors_.size();
378                if (oldSize == 1 && oldSize == newSize) {
379                    // If there is one constructor in the oldClass and one
380                    // constructor in the new class, then mark it as changed
381                    MemberDiff memberDiff = new MemberDiff(oldClass.name_);
382                    memberDiff.oldType_ = oldCtor.type_;
383                    memberDiff.oldExceptions_ = oldCtor.exceptions_;
384                    ConstructorAPI newCtor  = (ConstructorAPI)(newClass.ctors_.get(0));
385                    memberDiff.newType_ = newCtor.type_;
386                    memberDiff.newExceptions_ = newCtor.exceptions_;
387                    // Track changes in documentation
388                    if (docChanged(oldCtor.doc_, newCtor.doc_)) {
389                        String type = memberDiff.newType_;
390                        if (type.compareTo("void") == 0)
391                            type = "";
392                        String fqName = pkgDiff.name_ + "." + classDiff.name_;
393                        String link1 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "\" class=\"hiddenlink\">";
394                        String link2 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "#" + fqName + ".ctor_changed(" + type + ")\" class=\"hiddenlink\">";
395                        String id = pkgDiff.name_ + "." + classDiff.name_ + ".ctor(" + HTMLReportGenerator.simpleName(type) + ")";
396                        String title = link1 + "Class <b>" + classDiff.name_ +
397                            "</b></a>, " + link2 + "constructor <b>" + classDiff.name_ + "(" + HTMLReportGenerator.simpleName(type) + ")</b></a>";
398                        memberDiff.documentationChange_ = Diff.saveDocDiffs(
399                            pkgDiff.name_, classDiff.name_, oldCtor.doc_, newCtor.doc_, id, title);
400                    }
401                    String modifiersChange = oldCtor.modifiers_.diff(newCtor.modifiers_);
402                    if (modifiersChange != null && modifiersChange.indexOf("Change from deprecated to undeprecated") != -1) {
403                        System.out.println("JDiff: warning: change from deprecated to undeprecated for a constructor in class" + newClass.name_);
404                    }
405                    memberDiff.addModifiersChange(modifiersChange);
406                    if (trace)
407                        System.out.println("    The single constructor was changed");
408                    classDiff.ctorsChanged.add(memberDiff);
409                    singleCtor = true;
410                } else {
411                    if (trace)
412                        System.out.println("    Constructor " + oldClass.name_ + " was removed");
413                    classDiff.ctorsRemoved.add(oldCtor);
414                }
415                differs = true;
416            }
417        } // while (iter.hasNext())
418
419        // Find ctors which were added in the new class
420        iter = newClass.ctors_.iterator();
421        while (iter.hasNext()) {
422            ConstructorAPI newCtor = (ConstructorAPI)(iter.next());
423            int idx = Collections.binarySearch(oldClass.ctors_, newCtor);
424            if (idx < 0) {
425                if (!singleCtor) {
426                    if (trace)
427                        System.out.println("    Constructor " + oldClass.name_ + " was added");
428                    classDiff.ctorsAdded.add(newCtor);
429                    differs = true;
430                }
431            }
432        } // while (iter.hasNext())
433
434        return differs;
435    } // compareAllCtors()
436
437    /**
438     * Compare all the methods in two classes.
439     *
440     * We have to deal with the cases where:
441     *  - there is only one method with a given name, but its signature changes
442     *  - there is more than one method with the same name, and some of them
443     *    may have signature changes
444     * The simplest way to deal with this is to make the MethodAPI comparator
445     * check the params and return type, as well as the name. This means that
446     * changing a parameter's type would cause the method to be seen as
447     * removed and added. To avoid this for the simple case, check for before
448     * recording a method as removed or added.
449     */
450    public boolean compareAllMethods(ClassAPI oldClass, ClassAPI newClass, ClassDiff classDiff) {
451        if (trace)
452            System.out.println("    Comparing methods: #old " +
453                               oldClass.methods_.size() + ", #new " +
454                               newClass.methods_.size());
455        boolean differs = false;
456
457        Collections.sort(oldClass.methods_);
458        Collections.sort(newClass.methods_);
459
460        // Find methods which were removed in the new class
461        Iterator iter = oldClass.methods_.iterator();
462        while (iter.hasNext()) {
463            MethodAPI oldMethod = (MethodAPI)(iter.next());
464            int idx = -1;
465            MethodAPI[] methodArr = new MethodAPI[newClass.methods_.size()];
466            methodArr = (MethodAPI[])newClass.methods_.toArray(methodArr);
467            for (int methodIdx = 0; methodIdx < methodArr.length; methodIdx++) {
468                MethodAPI newMethod = methodArr[methodIdx];
469                if (oldMethod.compareTo(newMethod) == 0) {
470                    idx  = methodIdx;
471                    break;
472                }
473            }
474// NOTE: there was a problem with the binarySearch for
475// java.lang.Byte.toString(byte b) returning -16 when the compareTo method
476// returned 0 on entry 13. Changed to use arrays instead, so maybe it was
477// an issue with methods having another List of params used indirectly by
478// compareTo(), unlike constructors and fields?
479//            int idx = Collections.binarySearch(newClass.methods_, oldMethod);
480            if (idx < 0) {
481                // If there is only one instance of a method with this name
482                // in both the old and new class, then treat it as changed,
483                // rather than removed and added.
484                // Find how many instances of this method name there are in
485                // the old and new class. The equals comparator is just on
486                // the method name.
487                int startOld = oldClass.methods_.indexOf(oldMethod);
488                int endOld = oldClass.methods_.lastIndexOf(oldMethod);
489                int startNew = newClass.methods_.indexOf(oldMethod);
490                int endNew = newClass.methods_.lastIndexOf(oldMethod);
491
492                if (startOld != -1 && startOld == endOld &&
493                    startNew != -1 && startNew == endNew) {
494                    MethodAPI newMethod = (MethodAPI)(newClass.methods_.get(startNew));
495                    // Only one method with that name exists in both packages,
496                    // so it is valid to compare the two methods. We know it
497                    // has changed, because the binarySearch did not find it.
498                    if (oldMethod.inheritedFrom_ == null ||
499                        newMethod.inheritedFrom_ == null) {
500                        // We also know that at least one of the methods is
501                        // locally defined.
502                        compareMethods(oldMethod, newMethod, classDiff);
503                        differs = true;
504                    }
505                } else if (oldMethod.inheritedFrom_ == null) {
506                    // Only concerned with locally defined methods
507                    if (trace)
508                        System.out.println("    Method " + oldMethod.name_ +
509                                           "(" + oldMethod.getSignature() +
510                                           ") was removed");
511                    classDiff.methodsRemoved.add(oldMethod);
512                    differs = true;
513                }
514            }
515        } // while (iter.hasNext())
516
517        // Find methods which were added in the new class
518        iter = newClass.methods_.iterator();
519        while (iter.hasNext()) {
520            MethodAPI newMethod = (MethodAPI)(iter.next());
521            // Only concerned with locally defined methods
522            if (newMethod.inheritedFrom_ != null)
523                continue;
524            int idx = -1;
525            MethodAPI[] methodArr = new MethodAPI[oldClass.methods_.size()];
526            methodArr = (MethodAPI[])oldClass.methods_.toArray(methodArr);
527            for (int methodIdx = 0; methodIdx < methodArr.length; methodIdx++) {
528                MethodAPI oldMethod = methodArr[methodIdx];
529                if (newMethod.compareTo(oldMethod) == 0) {
530                    idx  = methodIdx;
531                    break;
532                }
533            }
534// See note above about searching an array instead of binarySearch
535//            int idx = Collections.binarySearch(oldClass.methods_, newMethod);
536            if (idx < 0) {
537                // See comments above
538                int startOld = oldClass.methods_.indexOf(newMethod);
539                int endOld = oldClass.methods_.lastIndexOf(newMethod);
540                int startNew = newClass.methods_.indexOf(newMethod);
541                int endNew = newClass.methods_.lastIndexOf(newMethod);
542
543                if (startOld != -1 && startOld == endOld &&
544                    startNew != -1 && startNew == endNew) {
545                    // Don't mark a method as added if it was marked as changed
546                    // The comparison will have been done just above here.
547                } else {
548                    if (trace)
549                        System.out.println("    Method " + newMethod.name_ +
550                                           "(" + newMethod.getSignature() + ") was added");
551                    classDiff.methodsAdded.add(newMethod);
552                    differs = true;
553                }
554            }
555        } // while (iter.hasNext())
556
557        return differs;
558    } // compareAllMethods()
559
560    /**
561     * Compare two methods which have the same name.
562     */
563    public boolean compareMethods(MethodAPI oldMethod, MethodAPI newMethod, ClassDiff classDiff) {
564        MemberDiff methodDiff = new MemberDiff(oldMethod.name_);
565        boolean differs = false;
566        // Check changes in return type
567        methodDiff.oldType_ = oldMethod.returnType_;
568        methodDiff.newType_ = newMethod.returnType_;
569        if (oldMethod.returnType_.compareTo(newMethod.returnType_) != 0) {
570            differs = true;
571        }
572        // Check changes in signature
573        String oldSig = oldMethod.getSignature();
574        String newSig = newMethod.getSignature();
575        methodDiff.oldSignature_ = oldSig;
576        methodDiff.newSignature_ = newSig;
577        if (oldSig.compareTo(newSig) != 0) {
578            differs = true;
579        }
580        // Changes in inheritance
581        int inh = changedInheritance(oldMethod.inheritedFrom_, newMethod.inheritedFrom_);
582        if (inh != 0)
583            differs = true;
584        if (inh == 1) {
585            methodDiff.addModifiersChange("Method was locally defined, but is now inherited from " + linkToClass(newMethod, true) + ".");
586            methodDiff.inheritedFrom_ = newMethod.inheritedFrom_;
587        } else if (inh == 2) {
588            methodDiff.addModifiersChange("Method was inherited from " + linkToClass(oldMethod, false) + ", but is now defined locally.");
589        } else if (inh == 3) {
590            methodDiff.addModifiersChange("Method was inherited from " +
591                                          linkToClass(oldMethod, false) + ", and is now inherited from " + linkToClass(newMethod, true) + ".");
592            methodDiff.inheritedFrom_ = newMethod.inheritedFrom_;
593        }
594        // Abstract or not
595        if (oldMethod.isAbstract_ != newMethod.isAbstract_) {
596            String changeText = "";
597            if (oldMethod.isAbstract_)
598                changeText += "Changed from abstract to non-abstract.";
599            else
600                changeText += "Changed from non-abstract to abstract.";
601            methodDiff.addModifiersChange(changeText);
602            differs = true;
603        }
604        // Native or not
605        if (Diff.showAllChanges &&
606	    oldMethod.isNative_ != newMethod.isNative_) {
607            String changeText = "";
608            if (oldMethod.isNative_)
609                changeText += "Changed from native to non-native.";
610            else
611                changeText += "Changed from non-native to native.";
612            methodDiff.addModifiersChange(changeText);
613            differs = true;
614        }
615        // Synchronized or not
616        if (Diff.showAllChanges &&
617	    oldMethod.isSynchronized_ != newMethod.isSynchronized_) {
618            String changeText = "";
619            if (oldMethod.isSynchronized_)
620                changeText += "Changed from synchronized to non-synchronized.";
621            else
622                changeText += "Changed from non-synchronized to synchronized.";
623            methodDiff.addModifiersChange(changeText);
624            differs = true;
625        }
626
627        // Check changes in exceptions thrown
628        methodDiff.oldExceptions_ = oldMethod.exceptions_;
629        methodDiff.newExceptions_ = newMethod.exceptions_;
630        if (oldMethod.exceptions_.compareTo(newMethod.exceptions_) != 0) {
631            differs = true;
632        }
633
634        // Track changes in documentation
635        if (docChanged(oldMethod.doc_, newMethod.doc_)) {
636            String sig = methodDiff.newSignature_;
637            if (sig.compareTo("void") == 0)
638                sig = "";
639            String fqName = pkgDiff.name_ + "." + classDiff.name_;
640            String link1 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "\" class=\"hiddenlink\">";
641            String link2 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "#" + fqName + "." + newMethod.name_ + "_changed(" + sig + ")\" class=\"hiddenlink\">";
642            String id = pkgDiff.name_ + "." + classDiff.name_ + ".dmethod." + newMethod.name_ + "(" + HTMLReportGenerator.simpleName(sig) + ")";
643            String title = link1 + "Class <b>" + classDiff.name_ + "</b></a>, " +
644                link2 + HTMLReportGenerator.simpleName(methodDiff.newType_) + " <b>" + newMethod.name_ + "(" + HTMLReportGenerator.simpleName(sig) + ")</b></a>";
645            methodDiff.documentationChange_ = Diff.saveDocDiffs(pkgDiff.name_, classDiff.name_, oldMethod.doc_, newMethod.doc_, id, title);
646            differs = true;
647        }
648
649        // All other modifiers
650        String modifiersChange = oldMethod.modifiers_.diff(newMethod.modifiers_);
651        if (modifiersChange != null) {
652            differs = true;
653            if (modifiersChange.indexOf("Change from deprecated to undeprecated") != -1) {
654                System.out.println("JDiff: warning: change from deprecated to undeprecated for method " +  classDiff.name_ + "." + newMethod.name_);
655
656            }
657        }
658        methodDiff.addModifiersChange(modifiersChange);
659
660        // Only add to the parent if some difference has been found
661        if (differs) {
662            if (trace) {
663                System.out.println("    Method " + newMethod.name_ +
664                    " was changed: old: " +
665                   oldMethod.returnType_ + "(" + oldSig + "), new: " +
666                   newMethod.returnType_ + "(" + newSig + ")");
667                if (methodDiff.modifiersChange_ != null)
668                    System.out.println("    Modifier change: " + methodDiff.modifiersChange_);
669            }
670            classDiff.methodsChanged.add(methodDiff);
671        }
672
673        return differs;
674    } // compareMethods()
675
676    /**
677     * Compare all the fields in two classes.
678     */
679    public boolean compareAllFields(ClassAPI oldClass, ClassAPI newClass,
680                                    ClassDiff classDiff) {
681        if (trace)
682            System.out.println("    Comparing fields: #old " +
683                               oldClass.fields_.size() + ", #new "
684                               + newClass.fields_.size());
685        boolean differs = false;
686
687        Collections.sort(oldClass.fields_);
688        Collections.sort(newClass.fields_);
689
690        // Find fields which were removed in the new class
691        Iterator iter = oldClass.fields_.iterator();
692        while (iter.hasNext()) {
693            FieldAPI oldField = (FieldAPI)(iter.next());
694            int idx = Collections.binarySearch(newClass.fields_, oldField);
695            if (idx < 0) {
696                // If there an instance of a field with the same name
697                // in both the old and new class, then treat it as changed,
698                // rather than removed and added. There will never be more than
699                // one instance of a field with the same name in a class.
700                int existsNew = newClass.fields_.indexOf(oldField);
701                if (existsNew != -1) {
702                    FieldAPI newField = (FieldAPI)(newClass.fields_.get(existsNew));
703                    if (oldField.inheritedFrom_ == null ||
704                        newField.inheritedFrom_ == null) {
705                        // We also know that one of the fields is locally defined.
706                        MemberDiff memberDiff = new MemberDiff(oldField.name_);
707                        memberDiff.oldType_ = oldField.type_;
708                        memberDiff.newType_ = newField.type_;
709                        // Changes in inheritance
710                        int inh = changedInheritance(oldField.inheritedFrom_, newField.inheritedFrom_);
711                        if (inh != 0)
712                            differs = true;
713                        if (inh == 1) {
714                            memberDiff.addModifiersChange("Field was locally defined, but is now inherited from " + linkToClass(newField, true) + ".");
715                            memberDiff.inheritedFrom_ = newField.inheritedFrom_;
716                        } else if (inh == 2) {
717                            memberDiff.addModifiersChange("Field was inherited from " + linkToClass(oldField, false) + ", but is now defined locally.");
718                        } else if (inh == 3) {
719                            memberDiff.addModifiersChange("Field was inherited from " + linkToClass(oldField, false) + ", and is now inherited from " + linkToClass(newField, true) + ".");
720                            memberDiff.inheritedFrom_ = newField.inheritedFrom_;
721                        }
722                        // Transient or not
723                        if (oldField.isTransient_ != newField.isTransient_) {
724                            String changeText = "";
725                            if (oldField.isTransient_)
726                                changeText += "Changed from transient to non-transient.";
727                            else
728                                changeText += "Changed from non-transient to transient.";
729                            memberDiff.addModifiersChange(changeText);
730                            differs = true;
731                        }
732                        // Volatile or not
733                        if (oldField.isVolatile_ != newField.isVolatile_) {
734                            String changeText = "";
735                            if (oldField.isVolatile_)
736                                changeText += "Changed from volatile to non-volatile.";
737                            else
738                                changeText += "Changed from non-volatile to volatile.";
739                            memberDiff.addModifiersChange(changeText);
740                            differs = true;
741                        }
742                        // Change in value of the field
743                        if (oldField.value_ != null &&
744                            newField.value_ != null &&
745                            oldField.value_.compareTo(newField.value_) != 0) {
746                            String changeText = "Changed in value from " + oldField.value_
747                                + " to " + newField.value_ +".";
748                            memberDiff.addModifiersChange(changeText);
749                            differs = true;
750                        }
751                        // Track changes in documentation
752                        if (docChanged(oldField.doc_, newField.doc_)) {
753                            String fqName = pkgDiff.name_ + "." + classDiff.name_;
754                            String link1 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "\" class=\"hiddenlink\">";
755                            String link2 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "#" + fqName + "." + newField.name_ + "\" class=\"hiddenlink\">";
756                            String id = pkgDiff.name_ + "." + classDiff.name_ + ".field." + newField.name_;
757                            String title = link1 + "Class <b>" + classDiff.name_ + "</b></a>, " +
758                                link2 + HTMLReportGenerator.simpleName(memberDiff.newType_) + " <b>" + newField.name_ + "</b></a>";
759                            memberDiff.documentationChange_ = Diff.saveDocDiffs(pkgDiff.name_, classDiff.name_, oldField.doc_, newField.doc_, id, title);
760                            differs = true;
761                        }
762
763                        // Other differences
764                        String modifiersChange = oldField.modifiers_.diff(newField.modifiers_);
765                        memberDiff.addModifiersChange(modifiersChange);
766                        if (modifiersChange != null && modifiersChange.indexOf("Change from deprecated to undeprecated") != -1) {
767                            System.out.println("JDiff: warning: change from deprecated to undeprecated for class " + newClass.name_ + ", field " + newField.name_);
768                        }
769                        if (trace)
770                            System.out.println("    Field " + newField.name_ + " was changed");
771                        classDiff.fieldsChanged.add(memberDiff);
772                        differs = true;
773                    }
774                } else if (oldField.inheritedFrom_ == null) {
775                    if (trace)
776                        System.out.println("    Field " + oldField.name_ + " was removed");
777                    classDiff.fieldsRemoved.add(oldField);
778                    differs = true;
779                }
780            }
781        } // while (iter.hasNext())
782
783        // Find fields which were added in the new class
784        iter = newClass.fields_.iterator();
785        while (iter.hasNext()) {
786            FieldAPI newField = (FieldAPI)(iter.next());
787            // Only concerned with locally defined fields
788            if (newField.inheritedFrom_ != null)
789                continue;
790            int idx = Collections.binarySearch(oldClass.fields_, newField);
791            if (idx < 0) {
792                // See comments above
793                int existsOld = oldClass.fields_.indexOf(newField);
794                if (existsOld != -1) {
795                    // Don't mark a field as added if it was marked as changed
796                } else {
797                    if (trace)
798                        System.out.println("    Field " + newField.name_ + " was added");
799                    classDiff.fieldsAdded.add(newField);
800                    differs = true;
801                }
802            }
803        } // while (iter.hasNext())
804
805        return differs;
806    } // compareFields()
807
808    /**
809     * Decide if two blocks of documentation changed.
810     *
811     * @return true if both are non-null and differ,
812     *              or if one is null and the other is not.
813     */
814    public static boolean docChanged(String oldDoc, String newDoc) {
815        if (!HTMLReportGenerator.reportDocChanges)
816            return false; // Don't even count doc changes as changes
817        if (oldDoc == null && newDoc != null)
818            return true;
819        if (oldDoc != null && newDoc == null)
820            return true;
821        if (oldDoc != null && newDoc != null && oldDoc.compareTo(newDoc) != 0)
822            return true;
823        return false;
824    }
825
826    /**
827     * Decide if two elements changed where they were defined.
828     *
829     * @return 0 if both are null, or both are non-null and are the same.
830     *         1 if the oldInherit was null and newInherit is non-null.
831     *         2 if the oldInherit was non-null and newInherit is null.
832     *         3 if the oldInherit was non-null and newInherit is non-null
833     *           and they differ.
834     */
835    public static int changedInheritance(String oldInherit, String newInherit) {
836        if (oldInherit == null && newInherit == null)
837            return 0;
838        if (oldInherit == null && newInherit != null)
839            return 1;
840        if (oldInherit != null && newInherit == null)
841            return 2;
842        if (oldInherit.compareTo(newInherit) == 0)
843            return 0;
844        else
845            return 3;
846    }
847
848    /**
849     * Generate a link to the Javadoc page for the given method.
850     */
851    public static String linkToClass(MethodAPI m, boolean useNew) {
852        String sig = m.getSignature();
853        if (sig.compareTo("void") == 0)
854            sig = "";
855        return linkToClass(m.inheritedFrom_, m.name_, sig, useNew);
856    }
857
858    /**
859     * Generate a link to the Javadoc page for the given field.
860     */
861    public static String linkToClass(FieldAPI m, boolean useNew) {
862        return linkToClass(m.inheritedFrom_, m.name_, null, useNew);
863    }
864
865    /**
866     * Given the name of the class, generate a link to a relevant page.
867     * This was originally for inheritance changes, so the JDiff page could
868     * be a class changes page, or a section in a removed or added classes
869     * table. Since there was no easy way to tell which type the link
870     * should be, it is now just a link to the relevant Javadoc page.
871     */
872    public static String linkToClass(String className, String memberName,
873                                     String memberType, boolean useNew) {
874        if (!useNew && HTMLReportGenerator.oldDocPrefix == null) {
875            return "<tt>" + className + "</tt>"; // No link possible
876        }
877        API api = oldAPI_;
878        String prefix = HTMLReportGenerator.oldDocPrefix;
879        if (useNew) {
880            api = newAPI_;
881            prefix = HTMLReportGenerator.newDocPrefix;
882        }
883        ClassAPI cls = (ClassAPI)api.classes_.get(className);
884        if (cls == null) {
885            if (useNew)
886                System.out.println("Warning: class " + className + " not found in the new API when creating Javadoc link");
887            else
888                System.out.println("Warning: class " + className + " not found in the old API when creating Javadoc link");
889            return "<tt>" + className + "</tt>";
890        }
891        int clsIdx = className.indexOf(cls.name_);
892        if (clsIdx != -1) {
893            String pkgRef = className.substring(0, clsIdx);
894            pkgRef = pkgRef.replace('.', '/');
895            String res = "<a href=\"" + prefix + pkgRef + cls.name_ + ".html#" + memberName;
896            if (memberType != null)
897                res += "(" + memberType + ")";
898            res += "\" target=\"_top\">" + "<tt>" + cls.name_ + "</tt></a>";
899            return res;
900        }
901        return "<tt>" + className + "</tt>";
902    }
903
904    /**
905     * Return the number of methods which are locally defined.
906     */
907    public int numLocalMethods(List methods) {
908        int res = 0;
909        Iterator iter = methods.iterator();
910        while (iter.hasNext()) {
911            MethodAPI m = (MethodAPI)(iter.next());
912            if (m.inheritedFrom_ == null)
913                res++;
914        }
915        return res;
916    }
917
918    /**
919     * Return the number of fields which are locally defined.
920     */
921    public int numLocalFields(List fields) {
922        int res = 0;
923        Iterator iter = fields.iterator();
924        while (iter.hasNext()) {
925            FieldAPI f = (FieldAPI)(iter.next());
926            if (f.inheritedFrom_ == null)
927                res++;
928        }
929        return res;
930    }
931
932    /** Set to enable increased logging verbosity for debugging. */
933    private boolean trace = false;
934}
935