Home | History | Annotate | Download | only in jdiff
      1 package jdiff;
      2 
      3 import 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 (at) pobox.com
     11  */
     12 public 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