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