1 /*
   2  * $Id$
   3  *
   4  * Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved.
   5  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   6  *
   7  * This code is free software; you can redistribute it and/or modify it
   8  * under the terms of the GNU General Public License version 2 only, as
   9  * published by the Free Software Foundation.  Oracle designates this
  10  * particular file as subject to the "Classpath" exception as provided
  11  * by Oracle in the LICENSE file that accompanied this code.
  12  *
  13  * This code is distributed in the hope that it will be useful, but WITHOUT
  14  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  15  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  16  * version 2 for more details (a copy is included in the LICENSE file that
  17  * accompanied this code).
  18  *
  19  * You should have received a copy of the GNU General Public License version
  20  * 2 along with this work; if not, write to the Free Software Foundation,
  21  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  22  *
  23  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  24  * or visit www.oracle.com if you need additional information or have any
  25  * questions.
  26  */
  27 package com.sun.javatest;
  28 
  29 import com.sun.javatest.util.DynamicArray;
  30 
  31 import java.io.*;
  32 import java.nio.charset.StandardCharsets;
  33 import java.util.*;
  34 
  35 import com.sun.javatest.util.I18NResourceBundle;
  36 
  37 /**
  38  * Support class to read and process a list of tests and test cases which are
  39  * known to fail during execution.  The intent is to allow better post-run
  40  * analysis of repetitive test runs, making is easier to find out what has
  41  * "changed" since the list was made.  This class is loosely based on the
  42  * exclude list, making it easy to interchange the files and tools.
  43  *
  44  * File format:
  45  * Test_URL[Test_Cases] BugID_List
  46  * The test URL rules are defined elsewhere, but it is critical that the test
  47  * names do not contain spaces and nothing before the BugID_List has any
  48  * whitespace.  The exact format of the BugID_List must simply conform to being
  49  * comma separated values, no whitespace or non-printable characters.
  50  * @since 4.4
  51  */
  52 public class KnownFailuresList
  53 {
  54 
  55     public void addEntry(Entry e) throws Fault {
  56         synchronized (table) {
  57             Key key = new Key(e.relativeURL);
  58             Object o = table.get(key);
  59             if (o == null) {
  60                 // easy case: nothing already exists in the table, so just
  61                 // add this one
  62                 table.put(key, e);
  63             }
  64             else if (o instanceof Entry) {
  65                 // a single entry exists in the table, so need to check for
  66                 // invalid combinations of test cases and tests
  67                 Entry curr = (Entry)o;
  68                 if (curr.testCase == null) {
  69                     if (e.testCase == null)
  70                         // overwrite existing entry for entire test
  71                         table.put(key, e);
  72                     else {
  73                         if (strict) {
  74                             // can't record test case when entire test already listed
  75                             throw new Fault(i18n, "kfl.cantListCase", e.relativeURL);
  76                         }
  77                         // else ignore new entry since entire test is already listed
  78                     }
  79                 }
  80                 else {
  81                     if (e.testCase == null) {
  82                         if (strict) {
  83                             // can't record entire test when test case already listed
  84                             throw new Fault(i18n, "kfl.cantListTest", e.relativeURL);
  85                         }
  86                         else {
  87                             // overwrite existing entry for a test case with
  88                             // new entry for entire test
  89                             table.put(key, e);
  90                         }
  91                     }
  92                     else if (curr.testCase.equals(e.testCase)) {
  93                         // overwrite existing entry for the same test case
  94                         table.put(key, e);
  95                     }
  96                     else {
  97                         // already excluded one test case, now we need to exclude
  98                         // another; make an array to hold both entries against the
  99                         // one key
 100                         table.put(key, new Entry[] {curr, e});
 101                     }
 102                 }
 103             }
 104             else {
 105                 // if there is an array, it must be for unique test cases
 106                 if (e.testCase == null) {
 107                     if (strict) {
 108                         // can't exclude entire test when selected test cases already excluded
 109                         throw new Fault(i18n, "kfl.cantListTest", e.relativeURL);
 110                     }
 111                     else {
 112                         // overwrite existing entry for list of test cases with
 113                         // new entry for entire test
 114                         table.put(key, e);
 115                     }
 116                 }
 117                 else {
 118                     Entry[] curr = (Entry[])o;
 119                     for (int i = 0; i < curr.length; i++) {
 120                         if (curr[i].testCase.equals(e.testCase)) {
 121                             curr[i] = e;
 122                             return;
 123                         }
 124                     }
 125                     // must be a new test case, add it into the array
 126                     table.put(key, DynamicArray.append(curr, e));
 127                 }
 128             }
 129 
 130         }
 131     }
 132 
 133     /**
 134      * This exception is used to report problems manipulating an exclude list.
 135      */
 136     public static class Fault extends Exception
 137     {
 138         Fault(I18NResourceBundle i18n, String s, Object o) {
 139             super(i18n.getString(s, o));
 140         }
 141     }
 142 
 143     /**
 144      * Test if a file appears to be for an exclude list, by checking the extension.
 145      * @param f The file to be tested.
 146      * @return <code>true</code> if the file appears to be a known failures list.
 147      */
 148     public static boolean isKflFile(File f) {
 149         return f.getPath().endsWith(KFLFILE_EXTN);
 150     }
 151 
 152     /**
 153      * Create a new, empty KFL object.
 154      */
 155     public KnownFailuresList() {
 156     }
 157 
 158     /**
 159      * Create an KnownFailuresList from the data contained in a file.
 160      * @param f The file to be read.
 161      * @throws FileNotFoundException if the file cannot be found
 162      * @throws IOException if any problems occur while reading the file
 163      * @throws KnownFailuresList.Fault if the data in the file is inconsistent
 164      * @see #KnownFailuresList(File[])
 165      */
 166     public KnownFailuresList(File f)
 167         throws FileNotFoundException, IOException, Fault
 168     {
 169         this(f, false);
 170     }
 171 
 172     /**
 173      * Create an KnownFailuresList from the data contained in a file.
 174      * @param f The file to be read.
 175      * @param strict Indicate if strict data checking rules should be used.
 176      * @throws FileNotFoundException if the file cannot be found
 177      * @throws IOException if any problems occur while reading the file
 178      * @throws KnownFailuresList.Fault if the data in the file is inconsistent
 179      * @see #KnownFailuresList(File[])
 180      * @see #setStrictModeEnabled(boolean)
 181      */
 182     public KnownFailuresList(File f, boolean strict)
 183         throws FileNotFoundException, IOException, Fault
 184     {
 185         setStrictModeEnabled(strict);
 186         if (f != null) {
 187             BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8));
 188 
 189             Parser p = new Parser(in);
 190             try {
 191                 Entry e;
 192                 while ((e = p.readEntry()) != null)
 193                     addEntry(e);
 194             }
 195             finally {
 196                 in.close();
 197             }
 198 
 199             title = p.getTitle();
 200         }
 201     }
 202 
 203 
 204     /**
 205      * Create a KnownFailuresList from the data contained in a series of files.
 206      * @param files The file to be read.
 207      * @throws FileNotFoundException if any of the files cannot be found
 208      * @throws IOException if any problems occur while reading the files.
 209      * @throws KnownFailuresList.Fault if the data in the files is inconsistent
 210      * @see #KnownFailuresList(File)
 211      */
 212     public KnownFailuresList(File[] files)
 213         throws FileNotFoundException, IOException, Fault
 214     {
 215         this(files, false);
 216     }
 217 
 218     /**
 219      * Create a KnownFailuresList from the data contained in a series of files.
 220      * @param files The file to be read.
 221      * @param strict Indicate if strict data checking rules should be used.
 222      * @throws FileNotFoundException if any of the files cannot be found
 223      * @throws IOException if any problems occur while reading the files.
 224      * @throws KnownFailuresList.Fault if the data in the files is inconsistent
 225      * @see #KnownFailuresList(File)
 226      * @see #setStrictModeEnabled(boolean)
 227      */
 228     public KnownFailuresList(File[] files, boolean strict)
 229         throws FileNotFoundException, IOException, Fault
 230     {
 231         setStrictModeEnabled(strict);
 232         for (int i = 0; i < files.length; i++) {
 233             KnownFailuresList kfl = new KnownFailuresList(files[i], strict);
 234             merge(kfl);
 235         }
 236     }
 237 
 238     /**
 239      * Specify whether strict mode is on or not. In strict mode, calls to addEntry
 240      * may generate an exception in the case of conflicts, such as adding an entry
 241      * to exclude a specific test case when the entire test is already excluded.
 242      * @param on true if strict mode should be enabled, and false otherwise
 243      * @see #isStrictModeEnabled
 244      */
 245     public void setStrictModeEnabled(boolean on) {
 246         //System.err.println("EL.setStrictModeEnabled " + on);
 247         strict = on;
 248     }
 249 
 250     /**
 251      * Check whether strict mode is enabled or not. In strict mode, calls to addEntry
 252      * may generate an exception in the case of conflicts, such as adding an entry
 253      * to exclude a specific test case when the entire test is already excluded.
 254      * @return true if strict mode is enabled, and false otherwise
 255      * @see #setStrictModeEnabled
 256      */
 257     public boolean isStrictModeEnabled() {
 258         return strict;
 259     }
 260 
 261     /**
 262      * Iterate over the contents of the table.
 263      * @param group if <code>true</code>, entries for the same relative
 264      * URL are grouped together, and if more than one, returned in an
 265      * array; if <code>false</code>, the iterator always returns
 266      * separate entries.
 267      * @see Entry
 268      * @return an iterator for the table: the entries are either
 269      * single instances of @link(Entry) or a mixture of @link(Entry)
 270      * and @link(Entry)[], depending on the <code>group</code>
 271      * parameter.
 272      */
 273     public Iterator<Entry> getIterator(boolean group) {
 274         if (group)
 275             return table.values().iterator();
 276         else {
 277             // flatten the enumeration into a vector, then
 278             // enumerate that
 279             List<Entry> v = new ArrayList<>(table.size());
 280             for (Iterator<Entry> iter = table.values().iterator(); iter.hasNext(); ) {
 281                 Object o = iter.next();
 282                 if (o instanceof Entry)
 283                     v.add((Entry)o);
 284                 else {
 285                     Entry[] entries = (Entry[])o;
 286                     for (int i = 0; i < entries.length; i++)
 287                         v.add(entries[i]);
 288                 }
 289             }
 290             return v.iterator();
 291         }
 292 
 293     }
 294 
 295 
 296     /**
 297      * Merge the contents of another exclude list into this one.
 298      * The individual entries are merged;  The title of the exclude list
 299      * being merged is ignored.
 300      * @param other the exclude list to be merged with this one.
 301      *
 302      */
 303     public void merge(KnownFailuresList other) {
 304         synchronized (table) {
 305             for (Iterator iter = other.getIterator(false); iter.hasNext(); ) {
 306                 Entry otherEntry = (Entry) (iter.next());
 307                 Key key = new Key(otherEntry.relativeURL);
 308                 Object o = table.get(key);
 309                 if (o == null) {
 310                     // Easy case: nothing already exists in the table, so just
 311                     // add this one
 312                     table.put(key, otherEntry);
 313                 }
 314                 else if (o instanceof Entry) {
 315                     // A single entry exists in the table
 316                     Entry curr = (Entry)o;
 317                     if (curr.testCase == null || otherEntry.testCase == null) {
 318                         table.put(key, new Entry(curr.relativeURL, null,
 319                                             ExcludeList.mergeBugIds(curr.bugIdStrings, otherEntry.bugIdStrings),
 320                                             ExcludeList.mergeSynopsis(curr.notes, otherEntry.notes)));
 321                     }
 322                     else
 323                         table.put(key, new Entry[] {curr, otherEntry});
 324                 }
 325                 else if (otherEntry.testCase == null) {
 326                     // An array of test cases exist in the table, but we're merging
 327                     // an entry for the complete test, so flatten down to a single entry
 328                     // for the whole test
 329                     String[] bugIdStrings = otherEntry.bugIdStrings;
 330                     String notes = otherEntry.notes;
 331                     Entry[] curr = (Entry[])o;
 332                     for (int i = 0; i < curr.length; i++) {
 333                         bugIdStrings = ExcludeList.mergeBugIds(bugIdStrings, curr[i].bugIdStrings);
 334                         notes = ExcludeList.mergeSynopsis(notes, curr[i].notes);
 335                     }
 336                     table.put(key, new Entry(otherEntry.relativeURL, null,
 337                                              bugIdStrings, notes));
 338                 }
 339                 else {
 340                     // An array of test cases exist in the table, and we're merging
 341                     // an entry with another set of test cases.
 342                     // For now, concatenate the arrays.
 343                     // RFE: Replace Entry[] with Set and merge the sets.
 344                     table.put(key, DynamicArray.append((Entry[]) o, otherEntry));
 345                 }
 346             }
 347         }
 348     }
 349 
 350     public Entry[] find(String url) {
 351         Object o = table.get(new Key(url));
 352         if (o == null) {
 353             return null;
 354         }
 355         if (o instanceof Entry[]) {
 356             return (Entry[])o;
 357         }
 358         else {
 359             return new Entry[] {(Entry)o};
 360         }
 361     }
 362 
 363     public Entry find(String url, String tc) {
 364         Entry[] entries = find(url);
 365 
 366         if (entries == null || entries.length == 0)
 367             return null;
 368 
 369         for(Entry e: entries) {
 370             if (e.containsTestCase(tc))
 371                 return e;
 372         }
 373 
 374         return null;
 375     }
 376 
 377     /**
 378      * Test if a specific test is completely excluded according to the table.
 379      * It is completely excluded if there is an entry, and the test case field is null.
 380      * @param td A test description for the test being checked.
 381      * @return <code>true</code> if the table contains an entry for this test.
 382      */
 383     public boolean listsAllOf(TestDescription td) {
 384         return listsAllOf(td.getRootRelativeURL());
 385     }
 386 
 387     /**
 388      * Test if a specific test is completely excluded according to the table.
 389      * It is completely excluded if there is an entry, and the test case field is null.
 390      * @param url The test-suite root-relative URL for the test.
 391      * @return <code>true</code> if the table contains an entry for this test.
 392      */
 393     public boolean listsAllOf(String url) {
 394         Object o = table.get(new Key(url));
 395         return (o != null && o instanceof Entry && ((Entry)o).testCase == null);
 396     }
 397 
 398     /**
 399      * Test if a specific test is partially or completely excluded according to the table.
 400      * It is so excluded if there is any entry in the table for the test.
 401      * @param td A test description for the test being checked.
 402      * @return <code>true</code> if the table contains an entry for this test.
 403      */
 404     public boolean listsAnyOf(TestDescription td) {
 405         return listsAnyOf(td.getRootRelativeURL());
 406     }
 407 
 408     /**
 409      * Test if a specific test is partially or completely excluded according to the table.
 410      * It is so excluded if there is any entry in the table for the test.
 411      * @param url The test-suite root-relative URL for the test.
 412      * @return <code>true</code> if the table contains an entry for this test.
 413      */
 414     public boolean listsAnyOf(String url) {
 415         Object o = table.get(new Key(url));
 416         return (o != null);
 417     }
 418 
 419     /**
 420      * Check whether an exclude list has any entries or not.
 421      * @return true if this exclude list has no entries
 422      * @see #size
 423      */
 424     public boolean isEmpty() {
 425         return table.isEmpty();
 426     }
 427 
 428     /**
 429      * Get the number of entries in the table.
 430      * @return the number of entries in the table
 431      * @see #isEmpty
 432      */
 433     public int size() {
 434         return 0;
 435     }
 436 
 437     /**
 438      * Get the title for this exclude list.
 439      * @return the title for this exclude list
 440      * @see #setTitle
 441      */
 442     public String getTitle() {
 443         return title;
 444     }
 445 
 446     /**
 447      * Set the title for this exclude list.
 448      * @param title the title for this exclude list
 449      * @see #getTitle
 450      */
 451     public void setTitle(String title) {
 452         this.title = title;
 453     }
 454 
 455     /**
 456      * Write the table out to a file.
 457      * @param f The file to which the table should be written.
 458      * @throws IOException is thrown if any problems occur while the
 459      * file is being written.
 460      */
 461     public void write(File f) throws IOException {
 462         BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8));
 463         out.write("### KFL/");
 464         out.write(KFL_FILE_VERSION);
 465         out.newLine();
 466         out.write("### Known Failures List");
 467         out.newLine();
 468         if (title != null) {
 469             out.write("### title " + title);
 470             out.newLine();
 471         }
 472 
 473         // write
 474 
 475         out.close();
 476     }
 477 
 478     private void write(Writer out, String s, int width) throws IOException {
 479         out.write(s);
 480         for (int i = s.length(); i < width; i++)
 481             out.write(' ');
 482     }
 483 
 484 
 485     private static boolean equals(String s1, String s2) {
 486         return (s1 == null && s2 == null
 487                 || s1 != null && s2 != null && s1.equals(s2));
 488     }
 489 
 490 
 491     /**
 492      * @param obj - object to compare
 493      * @return returns true if two entry tables are equal
 494      */
 495     @Override
 496     public boolean equals(Object obj) {
 497         if (obj == null) {
 498             return false;
 499         }
 500         if (getClass() != obj.getClass()) {
 501             return false;
 502         }
 503         final KnownFailuresList other = (KnownFailuresList) obj;
 504         if (this.table != other.table && (this.table == null ||
 505                 !this.table.equals(other.table))) {
 506             return false;
 507         }
 508         return true;
 509     }
 510 
 511     @Override
 512     public int hashCode() {
 513         int hash = 3;
 514         hash = 71 * hash + (this.table != null ? this.table.hashCode() : 0);
 515         return hash;
 516     }
 517 
 518     // --------- Inner classes -----------
 519     private static final class Parser {
 520         Parser(Reader in) throws IOException {
 521             this.in = in;
 522             ch = in.read();
 523         }
 524 
 525         String getTitle() {
 526             return title;
 527         }
 528 
 529         Entry readEntry() throws IOException, Fault {
 530             String url = readURL(); // includes optional test case
 531             if (url == null)
 532                 return null;
 533             String testCase = null; // for now
 534             if (url.endsWith("]")) {
 535                 int i = url.lastIndexOf("[");
 536                 if (i != -1) {
 537                     testCase = url.substring(i+1, url.length()-1);
 538                     url = url.substring(0, i);
 539                 }
 540             }
 541             String[] bugIdStrings = readBugIds();
 542             String note = readRest();
 543             return new Entry(url, testCase, bugIdStrings, note);
 544         }
 545 
 546         private boolean isEndOfLine(int ch) {
 547             return (ch == -1 || ch == '\n' || ch == '\r');
 548         }
 549 
 550         private boolean isWhitespace(int ch) {
 551             return (ch == ' ' || ch == '\t');
 552         }
 553 
 554         private String readURL() throws IOException, Fault {
 555             // skip white space, comments and blank lines until a word is found
 556             for (;;) {
 557                 skipWhite();
 558                 switch (ch) {
 559                 case -1:
 560                     // end of file
 561                     return null;
 562                 case '#':
 563                     // comment
 564                     skipComment();
 565                     break;
 566                 case '\r':
 567                 case '\n':
 568                     // blank line (or end of comment)
 569                     ch = in.read();
 570                     break;
 571                 default:
 572                     return readWord();
 573                 }
 574             }
 575         }
 576 
 577         private String[] readBugIds() throws IOException {
 578             // skip white space, then read and sort a list of comma-separated
 579             // numbers with no embedded white-space
 580             skipWhite();
 581             Set<String> s = new TreeSet<>();
 582             StringBuilder sb = new StringBuilder();
 583             for ( ; !isEndOfLine(ch) && !isWhitespace(ch); ch = in.read()) {
 584                 if (ch == ',') {
 585                     if (sb.length() > 0) {
 586                         s.add(sb.toString());
 587                         sb.setLength(0);
 588                     }
 589                 }
 590                 else
 591                     sb.append((char) ch);
 592             }
 593 
 594             if (sb.length() > 0)
 595                 s.add(sb.toString());
 596 
 597             if (s.isEmpty())
 598                 s.add("0");  // backwards compatibility
 599 
 600             return s.toArray(new String[s.size()]);
 601         }
 602 
 603         private String readRest() throws IOException {
 604             // skip white space, then read up to the end of the line
 605             skipWhite();
 606             StringBuilder word = new StringBuilder(80);
 607             for ( ; !isEndOfLine(ch); ch = in.read())
 608                 word.append((char)ch);
 609             // skip over terminating character
 610             ch = in.read();
 611             return word.toString();
 612         }
 613 
 614         private String readWord() throws IOException {
 615             // read characters up to the next white space
 616             StringBuilder word = new StringBuilder(32);
 617             for ( ; !isEndOfLine(ch) && !isWhitespace(ch); ch = in.read())
 618                 word.append((char)ch);
 619             return word.toString();
 620         }
 621 
 622         private void skipComment() throws IOException, Fault {
 623             ch = in.read();
 624             // first # has already been read
 625             if (ch == '#') {
 626                 ch = in.read();
 627                 if (ch == '#') {
 628                     ch = in.read();
 629                     skipWhite();
 630                     String s = readWord();
 631                     if (s.equals("title")) {
 632                         skipWhite();
 633                         title = readRest();
 634                         return;
 635                     }
 636                 }
 637             }
 638             while (!isEndOfLine(ch))
 639                 ch = in.read();
 640         }
 641 
 642         private void skipWhite() throws IOException {
 643             // skip horizontal white space
 644             // input is line-oriented, so do not skip over end of line
 645             while (ch != -1 && isWhitespace(ch))
 646                 ch = in.read();
 647         }
 648 
 649         private Reader in;      // source stream being read
 650         private int ch;         // current character
 651         private String title;
 652     };
 653 
 654     private static class Key {
 655         Key(String url) {
 656             relativeURL = url;
 657         }
 658 
 659         @Override
 660         public int hashCode() {
 661             // the hashCode for a key is the hashcode of the normalized URL.
 662             // The normalized URL is url.replace(File.separatorChar, '/').toLowerCase();
 663             int h = hash;
 664             if (h == 0) {
 665                 int len = relativeURL.length();
 666 
 667                 for (int i = 0; i < len; i++) {
 668                     char c = Character.toLowerCase(relativeURL.charAt(i));
 669                     if (c == sep)
 670                         c = '/';
 671                     h = 31*h + c;
 672                 }
 673                 hash = h;
 674             }
 675             return h;
 676         }
 677 
 678         @Override
 679         public boolean equals(Object o) {
 680             // Two keys are equal if their normalized URLs are equal.
 681             // The normalized URL is url.replace(File.separatorChar, '/').toLowerCase();
 682             if (o == null || !(o instanceof Key))
 683                 return false;
 684             String u1 = relativeURL;
 685             String u2 = ((Key) o).relativeURL;
 686             int len = u1.length();
 687             if (len != u2.length())
 688                 return false;
 689             for (int i = 0; i < len; i++) {
 690                 char c1 = Character.toLowerCase(u1.charAt(i));
 691                 if (c1 == sep)
 692                     c1 = '/';
 693                 char c2 = Character.toLowerCase(u2.charAt(i));
 694                 if (c2 == sep)
 695                     c2 = '/';
 696                 if (c1 != c2)
 697                     return false;
 698             }
 699             return true;
 700         }
 701 
 702         private static final char sep = File.separatorChar;
 703         private String relativeURL;
 704         private int hash;
 705     }
 706 
 707     /**
 708      * An entry in the exclude list.
 709      */
 710     public static final class Entry implements Comparable {
 711         /**
 712          * Create an ExcludeList entry.
 713          * @param u The URL for the test, specified relative to the test suite root.
 714          * @param tc One or more test cases within the test to be excluded.
 715          * @param b An array of bug identifiers, justifying why the test is excluded.
 716 
 717          * @param s A short synopsis of the reasons why the test is excluded.
 718          */
 719         public Entry(String u, String tc, String[] b, String s) {
 720             if (b == null)
 721                 throw new NullPointerException();
 722 
 723             // The file format cannot support platforms but no bugids,
 724             // so fault that; other combinations (bugs, no platforms;
 725             // no bugs, no platforms etc) are acceptable.
 726             if (b.length == 0)
 727                 throw new IllegalArgumentException();
 728 
 729             relativeURL = u;
 730             testCase = tc;
 731             bugIdStrings = b;
 732             notes = s;
 733         }
 734 
 735         public int compareTo(Object o) {
 736             Entry e = (Entry) o;
 737             int n = relativeURL.compareTo(e.relativeURL);
 738             if (n == 0) {
 739                 if (testCase == null && e.testCase == null)
 740                     return 0;
 741                 else if (testCase == null)
 742                     return -1;
 743                 else if (e.testCase == null)
 744                     return +1;
 745                 else
 746                     return testCase.compareTo(e.testCase);
 747             }
 748             else
 749                 return n;
 750         }
 751 
 752         public boolean containsTestCase(String s) {
 753             String[] tcs = getTestCaseList();
 754 
 755             if (tcs == null || tcs.length == 0)
 756                 return false;
 757 
 758             for (int i = 0; i < tcs.length; i++) {
 759                 if (tcs[i].equals(s))
 760                     return true;
 761             }   // for
 762 
 763             return false;
 764         }
 765 
 766         /**
 767          * Get the relative URL identifying the test referenced by this entry.
 768          * @return the relative URL identifying the test referenced by this entry
 769          */
 770         public String getRelativeURL() {
 771             return relativeURL;
 772         }
 773 
 774         /**
 775          * Get the (possibly empty) list of test cases for this entry.
 776          * An entry can have zero, one, or a comma separated list of TCs.
 777          *
 778          * @return List, or null if there are no test cases.
 779          */
 780         public String getTestCases() {
 781             return testCase;
 782         }
 783 
 784         /**
 785          * Get the same data as getTestCases(), but split into many Strings
 786          * This method is costly, so use with care.
 787          *
 788          * @return The parsed comma list, or null if there are no test cases.
 789          */
 790         public String[] getTestCaseList() {
 791             // code borrowed from StringArray
 792             // it is a little wasteful to recalc everytime but saves space
 793             if (testCase == null)
 794                 return null;
 795 
 796             List<String> v = new ArrayList<>();
 797             int start = -1;
 798             for (int i = 0; i < testCase.length(); i++) {
 799                 if (testCase.charAt(i) == ',') {
 800                     if (start != -1)
 801                         v.add(testCase.substring(start, i));
 802                     start = -1;
 803                 } else
 804                     if (start == -1)
 805                         start = i;
 806             }
 807             if (start != -1)
 808                 v.add(testCase.substring(start));
 809 
 810             if (v.isEmpty())
 811                 return null;
 812 
 813             String[] a = new String[v.size()];
 814             v.toArray(a);
 815             return a;
 816         }
 817 
 818         /**
 819          * Get the set of bug IDs referenced by this entry.
 820          * @return the bugs referenced by the entry
 821          */
 822         public String[] getBugIdStrings() {
 823             return bugIdStrings;
 824         }
 825 
 826         /**
 827          * Get a short description associated with this entry.
 828          * This should normally give details about why the test has been
 829          * excluded.
 830          * @return a short description associated with this entry
 831          */
 832         public String getNotes() {
 833             return notes;
 834         }
 835 
 836         /**
 837          * Create an entry from a string. The string should be formatted
 838          * as though it were a line of text in an exclude file.
 839          * @param text The text to be read
 840          * @return the first entry read from the supplied text
 841          * @throws ExcludeList.Fault if there is a problem reading the entry.
 842          */
 843         public static Entry read(String text) throws Fault {
 844             try {
 845                 return new Parser(new StringReader(text)).readEntry();
 846             }
 847             catch (IOException e) {
 848                 throw new Fault(i18n, "kfl.badEntry", e);
 849             }
 850         }
 851 
 852         /**
 853          * Compare this entry against another.
 854          * @param o the object to compare against
 855          * @return true is the objects are bothe ExcludeList.Entries containing
 856          *    the same details
 857          */
 858         @Override
 859         public boolean equals(Object o) {
 860             if (o instanceof Entry) {
 861                 Entry e = (Entry)o;
 862                 return equals(relativeURL, e.relativeURL)
 863                     && equals(testCase, e.testCase)
 864                     && equals(bugIdStrings, e.bugIdStrings)
 865                     && equals(notes, e.notes);
 866             }
 867             else
 868                 return false;
 869         }
 870 
 871         @Override
 872         public int hashCode() {
 873             return relativeURL.hashCode();
 874         }
 875 
 876         @Override
 877         public String toString() {
 878             StringBuffer sb = new StringBuffer(64);
 879             sb.append(relativeURL);
 880             if (testCase != null) {
 881                 sb.append('[');
 882                 sb.append(testCase);
 883                 sb.append(']');
 884             }
 885             if (bugIdStrings != null) {
 886                 for (int i = 0; i<bugIdStrings.length; i++) {
 887                     sb.append(i == 0 ? ' ' : ',');
 888                     sb.append(bugIdStrings[i]);
 889                 }
 890             }
 891             if (notes != null) {
 892                 sb.append(' ');
 893                 sb.append(notes);
 894             }
 895             return new String(sb);
 896         }
 897 
 898         private static boolean equals(int[] i1, int[] i2) {
 899             if (i1 == null || i2 == null)
 900                 return (i1 == null && i2 == null);
 901 
 902             if (i1.length != i2.length)
 903                 return false;
 904 
 905             for (int x = 0; x < i1.length; x++)
 906                 if (i1[x] != i2[x])
 907                     return false;
 908 
 909             return true;
 910         }
 911 
 912         private static boolean equals(String[] s1, String[] s2) {
 913             if (s1 == null || s2 == null)
 914                 return (s1 == null && s2 == null);
 915 
 916             if (s1.length != s2.length)
 917                 return false;
 918 
 919             for (int x = 0; x < s1.length; x++) {
 920                 if (!equals(s1[x], s2[x]))
 921                     return false;
 922             }
 923 
 924             return true;
 925         }
 926 
 927         private static boolean equals(String s1, String s2) {
 928             return (s1 == null && s2 == null
 929                     || s1 != null && s2 != null && s1.equals(s2));
 930         }
 931 
 932 
 933         private String relativeURL;
 934         private String testCase;
 935         private String[] bugIdStrings;
 936         private int[] bugIds; // null, unless required
 937         private String notes;
 938     }
 939 
 940     private Map table = new HashMap();
 941     private String title;
 942     private boolean strict;
 943     private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(KnownFailuresList.class);
 944 
 945     /**
 946      * The standard extension for KFL files. (".kfl")
 947      */
 948     public static final String KFLFILE_EXTN = ".kfl";
 949     public static final String KFL_FILE_VERSION = "1.0";
 950 }