1 /*
   2  * $Id$
   3  *
   4  * Copyright (c) 2001, 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 java.io.*;
  30 import java.nio.charset.StandardCharsets;
  31 import java.util.*;
  32 
  33 import com.sun.javatest.util.DynamicArray;
  34 import com.sun.javatest.util.I18NResourceBundle;
  35 
  36 /**
  37  * A set of tests to be excluded from a test run.
  38  */
  39 
  40 public class ExcludeList
  41 {
  42     /**
  43      * This exception is used to report problems manipulating an exclude list.
  44      */
  45     public static class Fault extends Exception
  46     {
  47         Fault(I18NResourceBundle i18n, String s, Object o) {
  48             super(i18n.getString(s, o));
  49         }
  50     }
  51 
  52     /**
  53      * Test if a file appears to be for an exclude list, by checking the extension.
  54      * @param f The file to be tested.
  55      * @return <code>true</code> if the file appears to be an exclude list.
  56      */
  57     public static boolean isExcludeFile(File f) {
  58         return f.getPath().endsWith(EXCLUDEFILE_EXTN);
  59     }
  60 
  61     /**
  62      * Create a new exclude list.
  63      */
  64     public ExcludeList() {
  65     }
  66 
  67     /**
  68      * Create an ExcludeList from the data contained in a file.
  69      * @param f The file to be read.
  70      * @throws FileNotFoundException if the file cannot be found
  71      * @throws IOException if any problems occur while reading the file
  72      * @throws ExcludeList.Fault if the data in the file is ionconsistent
  73      * @see #ExcludeList(File[])
  74      */
  75     public ExcludeList(File f)
  76         throws FileNotFoundException, IOException, Fault
  77     {
  78         this(f, false);
  79     }
  80 
  81     /**
  82      * Create an ExcludeList from the data contained in a file.
  83      * @param f The file to be read.
  84      * @param strict Indicate if strict data checking rules should be used.
  85      * @throws FileNotFoundException if the file cannot be found
  86      * @throws IOException if any problems occur while reading the file
  87      * @throws ExcludeList.Fault if the data in the file is inconsistent
  88      * @see #ExcludeList(File[])
  89      * @see #setStrictModeEnabled(boolean)
  90      */
  91     public ExcludeList(File f, boolean strict)
  92         throws FileNotFoundException, IOException, Fault
  93     {
  94         setStrictModeEnabled(strict);
  95         if (f != null) {
  96             try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) {
  97                 Parser p = new Parser(in);
  98                 Entry e;
  99                 while ((e = p.readEntry()) != null)
 100                     addEntry(e);
 101 
 102                 title = p.getTitle();
 103             }
 104         }
 105     }
 106 
 107 
 108     /**
 109      * Create an ExcludeList from the data contained in a series of files.
 110      * @param files The file to be read.
 111      * @throws FileNotFoundException if any of the files cannot be found
 112      * @throws IOException if any problems occur while reading the files.
 113      * @throws ExcludeList.Fault if the data in the files is inconsistent
 114      * @see #ExcludeList(File)
 115      */
 116     public ExcludeList(File[] files)
 117         throws FileNotFoundException, IOException, Fault
 118     {
 119         this(files, false);
 120     }
 121 
 122     /**
 123      * Create an ExcludeList from the data contained in a series of files.
 124      * @param files The file to be read.
 125      * @param strict Indicate if strict data checking rules should be used.
 126      * @throws FileNotFoundException if any of the files cannot be found
 127      * @throws IOException if any problems occur while reading the files.
 128      * @throws ExcludeList.Fault if the data in the files is inconsistent
 129      * @see #ExcludeList(File)
 130      * @see #setStrictModeEnabled(boolean)
 131      */
 132     public ExcludeList(File[] files, boolean strict)
 133         throws FileNotFoundException, IOException, Fault
 134     {
 135         setStrictModeEnabled(strict);
 136         for (File file : files) {
 137             ExcludeList et = new ExcludeList(file, strict);
 138             merge(et);
 139         }
 140     }
 141 
 142     /**
 143      * Specify whether strict mode is on or not. In strict mode, calls to addEntry
 144      * may generate an exception in the case of conflicts, such as adding an entry
 145      * to exclude a specific test case when the entire test is already excluded.
 146      * @param on true if strict mode should be enabled, and false otherwise
 147      * @see #isStrictModeEnabled
 148      */
 149     public void setStrictModeEnabled(boolean on) {
 150         //System.err.println("EL.setStrictModeEnabled " + on);
 151         strict = on;
 152     }
 153 
 154     /**
 155      * Check whether strict mode is enabled or not. In strict mode, calls to addEntry
 156      * may generate an exception in the case of conflicts, such as adding an entry
 157      * to exclude a specific test case when the entire test is already excluded.
 158      * @return true if strict mode is enabled, and false otherwise
 159      * @see #setStrictModeEnabled
 160      */
 161     public boolean isStrictModeEnabled() {
 162         return strict;
 163     }
 164 
 165     /**
 166      * Test if a specific test is completely excluded according to the table.
 167      * It is completely excluded if there is an entry, and the test case field is null.
 168      * @param td A test description for the test being checked.
 169      * @return <code>true</code> if the table contains an entry for this test.
 170      */
 171     public boolean excludesAllOf(TestDescription td) {
 172         return excludesAllOf(td.getRootRelativeURL());
 173     }
 174 
 175     /**
 176      * Test if a specific test is completely excluded according to the table.
 177      * It is completely excluded if there is an entry, and the test case field is null.
 178      * @param url The test-suite root-relative URL for the test.
 179      * @return <code>true</code> if the table contains an entry for this test.
 180      */
 181     public boolean excludesAllOf(String url) {
 182         Object o = table.get(new Key(url));
 183         return (o != null && o instanceof Entry && ((Entry)o).testCase == null);
 184     }
 185 
 186     /**
 187      * Test if a specific test is partially or completely excluded according to the table.
 188      * It is so excluded if there is any entry in the table for the test.
 189      * @param td A test description for the test being checked.
 190      * @return <code>true</code> if the table contains an entry for this test.
 191      */
 192     public boolean excludesAnyOf(TestDescription td) {
 193         return excludesAnyOf(td.getRootRelativeURL());
 194     }
 195 
 196     /**
 197      * Test if a specific test is partially or completely excluded according to the table.
 198      * It is so excluded if there is any entry in the table for the test.
 199      * @param url The test-suite root-relative URL for the test.
 200      * @return <code>true</code> if the table contains an entry for this test.
 201      */
 202     public boolean excludesAnyOf(String url) {
 203         Object o = table.get(new Key(url));
 204         return (o != null);
 205     }
 206 
 207     /**
 208      * Get the test cases to be excluded for a test.
 209      *
 210      * @param td A test description for the test being checked.
 211      * @return an array of test case names if any test cases are to
 212      * be excluded. The result is null if the test is not found or is
 213      * completely excluded without specifying test cases.  This may be
 214      * a mix of single TC strings or a comma separated list of them.
 215      */
 216     public String[] getTestCases(TestDescription td) {
 217         Key key = new Key(td.getRootRelativeURL());
 218         synchronized (table) {
 219             Object o = table.get(key);
 220             if (o == null)
 221                 // not found
 222                 return null;
 223             else if (o instanceof Entry) {
 224                 Entry e = (Entry)o;
 225                 if (e.testCase == null)
 226                     // entire test excluded
 227                     return null;
 228                 else
 229                     return (new String[] {e.testCase});
 230             }
 231             else {
 232                 Entry[] ee = (Entry[])o;
 233                 String[] testCases = new String[ee.length];
 234                 for (int i = 0; i < ee.length; i++)
 235                     testCases[i] = ee[i].testCase;
 236                 return testCases;
 237             }
 238         }
 239     }
 240 
 241     /**
 242      * Add an entry to the table.
 243      * @param e The entry to be added; if an entry already exists for this test
 244      *  description, it will be replaced.
 245      * @throws ExcludeList.Fault if the entry is for the entire test and
 246      *   there is already an entry for a test case for this test, or vice versa.
 247      */
 248     public void addEntry(Entry e) throws Fault {
 249         synchronized (table) {
 250             Key key = new Key(e.relativeURL);
 251             Object o = table.get(key);
 252 
 253             if (o == null) {
 254                 // easy case: nothing already exists in the table, so just
 255                 // add this one
 256                 table.put(key, e);
 257             }
 258             else if (o instanceof Entry) {
 259                 // a single entry exists in the table, so need to check for
 260                 // invalid combinations of test cases and tests
 261                 Entry curr = (Entry)o;
 262                 if (curr.testCase == null) {
 263                     if (e.testCase == null)
 264                         // overwrite existing entry for entire test
 265                         table.put(key, e);
 266                     else {
 267                         if (strict) {
 268                             // can't exclude test case when entire test already excluded
 269                             throw new Fault(i18n, "excl.cantExcludeCase", e.relativeURL);
 270                         }
 271                         // else ignore new entry since entire test is already excluded
 272                     }
 273                 }
 274                 else {
 275                     if (e.testCase == null) {
 276                         if (strict) {
 277                             // can't exclude entire test when test case already excluded
 278                             throw new Fault(i18n, "excl.cantExcludeTest", e.relativeURL);
 279                         }
 280                         else {
 281                             // overwrite existing entry for a test case with
 282                             // new entry for entire test
 283                             table.put(key, e);
 284                         }
 285                     }
 286                     else if (curr.testCase.equals(e.testCase)) {
 287                         // overwrite existing entry for the same test case
 288                         table.put(key, e);
 289                     }
 290                     else {
 291                         // already excluded one test case, now we need to exclude
 292                         // another; make an array to hold both entries against the
 293                         // one key
 294                         table.put(key, new Entry[] {curr, e});
 295                     }
 296                 }
 297             }
 298             else {
 299                 // if there is an array, it must be for unique test cases
 300                 if (e.testCase == null) {
 301                     if (strict) {
 302                         // can't exclude entire test when selected test cases already excluded
 303                         throw new Fault(i18n, "excl.cantExcludeTest", e.relativeURL);
 304                     }
 305                     else {
 306                         // overwrite existing entry for list of test cases with
 307                         // new entry for entire test
 308                         table.put(key, e);
 309                     }
 310                 }
 311                 else {
 312                     Entry[] curr = (Entry[])o;
 313                     for (int i = 0; i < curr.length; i++) {
 314                         if (curr[i].testCase.equals(e.testCase)) {
 315                             curr[i] = e;
 316                             return;
 317                         }
 318                     }
 319                     // must be a new test case, add it into the array
 320                     table.put(key, DynamicArray.append(curr, e));
 321                 }
 322             }
 323 
 324         }
 325     }
 326 
 327     /**
 328      * Locate an entry for a test.
 329      * @param url The root relative URL for the test; the URL may include
 330      * a test case if necessary included in square brackets after the URL proper.
 331      * @return The entry for the test, or null if there is none.
 332      */
 333     public Entry getEntry(String url) {
 334         String testCase = null;
 335         if (url.endsWith("]")) {
 336             int i = url.lastIndexOf("[");
 337             if (i != -1) {
 338                 testCase = url.substring(i+1, url.length()-1);
 339                 url = url.substring(0, i);
 340             }
 341         }
 342         return getEntry(url, testCase);
 343     }
 344 
 345     /**
 346      * Locate an entry for a test.
 347      *
 348      * @param url The root relative URL for the test.
 349      * @param testCase An optional test case to be taken into account.  This cannot
 350      *  be a comma separated list.  A value of null will match any entry with the given
 351      *  url.
 352      * @return The entry for the test, or null if the URL cannot be found.
 353      */
 354     public Entry getEntry(String url, String testCase) {
 355         // XXX what if multiple entries?
 356         Key key = new Key(url);
 357         Object o = table.get(key);
 358         if (o == null)
 359             return null;
 360         else if (o instanceof Entry) {
 361             Entry e = (Entry)o;
 362             if (testCase == null)
 363                 return e;
 364             else
 365                 return (isInList(e.testCase, testCase) ? e : null);
 366         }
 367         else {
 368             Entry[] entries = (Entry[])o;
 369             for (Entry e : entries) {
 370                 if (isInList(e.testCase, testCase))
 371                     return e;
 372             }
 373             return null;
 374         }
 375     }
 376 
 377     /**
 378      * Merge the contents of another exclude list into this one.
 379      * The individual entries are merged;  The title of the exclude list
 380      * being merged is ignored.
 381      * @param other the exclude list to be merged with this one.
 382      *
 383      */
 384     public void merge(ExcludeList other) {
 385         synchronized (table) {
 386             for (Iterator iter = other.getIterator(false); iter.hasNext(); ) {
 387                 Entry otherEntry = (Entry) (iter.next());
 388                 Key key = new Key(otherEntry.relativeURL);
 389                 Object o = table.get(key);
 390                 if (o == null) {
 391                     // Easy case: nothing already exists in the table, so just
 392                     // add this one
 393                     table.put(key, otherEntry);
 394                 }
 395                 else if (o instanceof Entry) {
 396                     // A single entry exists in the table
 397                     Entry curr = (Entry)o;
 398                     if (curr.testCase == null || otherEntry.testCase == null) {
 399                         table.put(key, new Entry(curr.relativeURL, null,
 400                                             mergeBugIds(curr.bugIdStrings, otherEntry.bugIdStrings),
 401                                             mergePlatforms(curr.platforms, otherEntry.platforms),
 402                                             mergeSynopsis(curr.synopsis, otherEntry.synopsis)));
 403                     }
 404                     else
 405                         table.put(key, new Entry[] {curr, otherEntry});
 406                 }
 407                 else if (otherEntry.testCase == null) {
 408                     // An array of test cases exist in the table, but we're merging
 409                     // an entry for the complete test, so flatten down to a single entry
 410                     // for the whole test
 411                     String[] bugIdStrings = otherEntry.bugIdStrings;
 412                     String[] platforms = otherEntry.platforms;
 413                     String synopsis = otherEntry.synopsis;
 414                     for (Entry entry : (Entry[])o) {
 415                         bugIdStrings = mergeBugIds(bugIdStrings, entry.bugIdStrings);
 416                         platforms = mergePlatforms(platforms, entry.platforms);
 417                         synopsis = mergeSynopsis(synopsis, entry.synopsis);
 418                     }
 419                     table.put(key, new Entry(otherEntry.relativeURL, null,
 420                                              bugIdStrings, platforms, synopsis));
 421                 }
 422                 else {
 423                     // An array of test cases exist in the table, and we're merging
 424                     // an entry with another set of test cases.
 425                     // For now, concatenate the arrays.
 426                     // RFE: Replace Entry[] with Set and merge the sets.
 427                     table.put(key, DynamicArray.append((Entry[]) o, otherEntry));
 428                 }
 429             }
 430         }
 431     }
 432 
 433     static String[] mergeBugIds(String[] a, String[] b) {
 434         return merge(a, b);
 435     }
 436 
 437     static String[] mergePlatforms(String[] a, String[] b) {
 438         return merge(a, b);
 439     }
 440 
 441     static String[] merge(String[] a, String[] b) {
 442         SortedSet<String> s = new TreeSet<>();
 443         s.addAll(Arrays.asList(a));
 444         s.addAll(Arrays.asList(b));
 445         return s.toArray(new String[s.size()]);
 446     }
 447 
 448     static String mergeSynopsis(String a, String b) {
 449         if (a == null || a.trim().length() == 0)
 450             return b;
 451         else if (b == null || b.trim().length() == 0)
 452             return a;
 453         else if (a.indexOf(b) != -1)
 454             return a;
 455         else if (b.indexOf(a) != -1)
 456             return b;
 457         else
 458             return a + "; " + b;
 459     }
 460 
 461 
 462     /**
 463      * Remove an entry from the table.
 464      * @param e the entry to be removed
 465      */
 466     public void removeEntry(Entry e) {
 467         synchronized (table) {
 468             Key key = new Key(e.relativeURL);
 469             Object o = table.get(key);
 470             if (o == null)
 471                 // no such entry
 472                 return;
 473             else if (o instanceof Entry) {
 474                 if (o == e)
 475                     table.remove(key);
 476             }
 477             else {
 478                 Entry[] o2 = DynamicArray.remove((Entry[])o, e);
 479                 if (o2 == o)
 480                     // not found
 481                     return;
 482                 else {
 483                     if (o2.length == 1)
 484                         table.put(key, o2[0]);
 485                     else
 486                         table.put(key, o2);
 487                 }
 488             }
 489         }
 490     }
 491 
 492     /**
 493      * Check whether an exclude list has any entries or not.
 494      * @return true if this exclude list has no entries
 495      * @see #size
 496      */
 497     public boolean isEmpty() {
 498         return table.isEmpty();
 499     }
 500 
 501     /**
 502      * Get the number of entries in the table.
 503      * @return the number of entries in the table
 504      * @see #isEmpty
 505      */
 506     public int size() {
 507         // ouch, this is now expensive to compute
 508         int n = 0;
 509         for (Object o : table.values()) {
 510             if (o instanceof Entry[])
 511                 n += ((Entry[]) o).length;
 512             else
 513                 n++;
 514         }
 515         return n;
 516     }
 517 
 518     /**
 519      * Iterate over the contents of the table.
 520      * @param group if <code>true</code>, entries for the same relative
 521      * URL are grouped together, and if more than one, returned in an
 522      * array; if <code>false</code>, the iterator always returns
 523      * separate entries.
 524      * @see Entry
 525      * @return an iterator for the table: the entries are either
 526      * single instances of @link(Entry) or a mixture of @link(Entry)
 527      * and @link(Entry)[], depending on the <code>group</code>
 528      * parameter.
 529      */
 530     public Iterator<Object> getIterator(boolean group) {
 531         if (group)
 532             return table.values().iterator();
 533         else {
 534             // flatten the enumeration into a vector, then
 535             // enumerate that
 536             Vector<Object> v = new Vector<>(table.size());
 537             for (Object o : table.values()) {
 538                 if (o instanceof Entry)
 539                     v.addElement(o);
 540                 else {
 541                     for (Entry entry : (Entry[]) o) v.addElement(entry);
 542                 }
 543             }
 544             return v.iterator();
 545         }
 546 
 547     }
 548 
 549     /**
 550      * Get the title for this exclude list.
 551      * @return the title for this exclude list
 552      * @see #setTitle
 553      */
 554     public String getTitle() {
 555         return title;
 556     }
 557 
 558     /**
 559      * Set the title for this exclude list.
 560      * @param title the title for this exclude list
 561      * @see #getTitle
 562      */
 563     public void setTitle(String title) {
 564         this.title = title;
 565     }
 566 
 567     /**
 568      * Write the table out to a file.
 569      * @param f The file to which the table should be written.
 570      * @throws IOException is thrown if any problems occur while the
 571      * file is being written.
 572      */
 573     public void write(File f) throws IOException {
 574         // sort the entries for convenience, and measure col widths
 575         int maxURLWidth = 0;
 576         int maxBugIdWidth = 0;
 577         int maxPlatformWidth = 0;
 578         SortedSet<Entry> entries = new TreeSet<>();
 579         for (Iterator iter = getIterator(false); iter.hasNext(); ) {
 580             Entry entry = (Entry) (iter.next());
 581             entries.add(entry);
 582             if (entry.testCase == null)
 583                 maxURLWidth = Math.max(entry.relativeURL.length(), maxURLWidth);
 584             else
 585                 maxURLWidth = Math.max(entry.relativeURL.length() + entry.testCase.length() + 2, maxURLWidth);
 586             maxBugIdWidth = Math.max(bugIdsToString(entry).length(), maxBugIdWidth);
 587             maxPlatformWidth = Math.max(platformsToString(entry).length(), maxPlatformWidth);
 588         }
 589 
 590         BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8));
 591         out.write("# Exclude List");
 592         out.newLine();
 593         out.write("# SCCS %" + 'W' + "% %" + 'E' + "%"); // TAKE CARE WITH SCCS HEADERS
 594         out.newLine();
 595         if (title != null) {
 596             out.write("### title " + title);
 597             out.newLine();
 598         }
 599         for (Entry e : entries) {
 600             if (e.testCase == null)
 601                 write(out, e.relativeURL, maxURLWidth + 2);
 602             else
 603                 write(out, e.relativeURL + "[" + e.testCase + "]", maxURLWidth + 2);
 604             write(out, bugIdsToString(e), maxBugIdWidth + 2);
 605             write(out, platformsToString(e), maxPlatformWidth + 2);
 606             out.write(e.synopsis);
 607             out.newLine();
 608         }
 609         out.close();
 610     }
 611 
 612     private void write(Writer out, String s, int width) throws IOException {
 613         out.write(s);
 614         for (int i = s.length(); i < width; i++)
 615             out.write(' ');
 616     }
 617 
 618     private String bugIdsToString(Entry e) {
 619         StringBuffer sb = new StringBuffer(e.bugIdStrings.length*10);
 620         sb.append(e.bugIdStrings[0]);
 621         for (int i = 1; i < e.bugIdStrings.length; i++) {
 622             sb.append(',');
 623             sb.append(e.bugIdStrings[i]);
 624         }
 625         return sb.toString();
 626     }
 627 
 628     private String platformsToString(Entry e) {
 629         StringBuffer sb = new StringBuffer(e.platforms.length*10);
 630         sb.append(e.platforms[0]);
 631         for (int i = 1; i < e.platforms.length; i++) {
 632             sb.append(',');
 633             sb.append(e.platforms[i]);
 634         }
 635         return sb.toString();
 636     }
 637 
 638     private static boolean equals(String s1, String s2) {
 639         return (s1 == null && s2 == null
 640                 || s1 != null && s2 != null && s1.equals(s2));
 641     }
 642 
 643     /**
 644      * Is val in the comma separated list.
 645      *
 646      * @param list Comma separated list or a single value.
 647      * @return Null if either parameter is null.
 648      */
 649     private static boolean isInList(String list, String val) {
 650         // check for invalid args
 651         if (list == null || val == null)
 652             return false;
 653 
 654         // loop through possible matches
 655         for (int pos = list.indexOf(val); pos != -1; pos = list.indexOf(val, pos + 1) ) {
 656             // check beginning of string
 657             if (!(pos == 0 || list.charAt(pos -1) == ','))
 658                 continue;
 659 
 660             // check end of string
 661             if (!(pos + val.length() == list.length()  || list.charAt(pos + val.length()) == ','))
 662                 continue;
 663 
 664             // beginning and end are OK; got a match
 665             return true;
 666         }
 667 
 668         // no good matches found
 669         return false;
 670     }
 671 
 672     /**
 673      * @param obj - object to compare
 674      * @return returns true if two entry tables are equal
 675      */
 676     @Override
 677     public boolean equals(Object obj) {
 678         if (obj == null) {
 679             return false;
 680         }
 681         if (getClass() != obj.getClass()) {
 682             return false;
 683         }
 684         final ExcludeList other = (ExcludeList) obj;
 685         if (this.table != other.table && (this.table == null ||
 686                 !this.table.equals(other.table))) {
 687             return false;
 688         }
 689         return true;
 690     }
 691 
 692     @Override
 693     public int hashCode() {
 694         int hash = 3;
 695         hash = 71 * hash + (this.table != null ? this.table.hashCode() : 0);
 696         return hash;
 697     }
 698 
 699     private Map<Key, Object> table = new HashMap<>();
 700     private String title;
 701     private boolean strict;
 702 
 703     private static final class Parser {
 704         Parser(Reader in) throws IOException {
 705             this.in = in;
 706             ch = in.read();
 707         }
 708 
 709         String getTitle() {
 710             return title;
 711         }
 712 
 713         Entry readEntry() throws IOException, Fault {
 714             String url = readURL(); // includes optional test case
 715             if (url == null)
 716                 return null;
 717             String testCase = null; // for now
 718 
 719             if (url.endsWith("]")) {
 720                 int i = url.lastIndexOf("[");
 721                 if (i != -1) {
 722                     testCase = url.substring(i+1, url.length()-1);
 723                     url = url.substring(0, i);
 724                 }
 725             }
 726             String[] bugIdStrings = readBugIds();
 727             String[] platforms = readPlatforms();
 728             String synopsis = readRest();
 729             return new Entry(url, testCase, bugIdStrings, platforms, synopsis);
 730         }
 731 
 732         private boolean isEndOfLine(int ch) {
 733             return (ch == -1 || ch == '\n' || ch == '\r');
 734         }
 735 
 736         private boolean isWhitespace(int ch) {
 737             return (ch == ' ' || ch == '\t');
 738         }
 739 
 740         private String readURL() throws IOException, Fault {
 741             // skip white space, comments and blank lines until a word is found
 742             for (;;) {
 743                 skipWhite();
 744                 switch (ch) {
 745                 case -1:
 746                     // end of file
 747                     return null;
 748                 case '#':
 749                     // comment
 750                     skipComment();
 751                     break;
 752                 case '\r':
 753                 case '\n':
 754                     // blank line (or end of comment)
 755                     ch = in.read();
 756                     break;
 757                 default:
 758                     return readWord();
 759                 }
 760             }
 761         }
 762 
 763         private String[] readBugIds() throws IOException {
 764             // skip white space, then read and sort a list of comma-separated
 765             // numbers with no embedded white-space
 766             skipWhite();
 767             TreeSet<String> s = new TreeSet<>();
 768             StringBuffer sb = new StringBuffer();
 769             for ( ; !isEndOfLine(ch) && !isWhitespace(ch); ch = in.read()) {
 770                 if (ch == ',') {
 771                     if (sb.length() > 0) {
 772                         s.add(sb.toString());
 773                         sb.setLength(0);
 774                     }
 775                 }
 776                 else
 777                     sb.append((char) ch);
 778             }
 779 
 780             if (sb.length() > 0)
 781                 s.add(sb.toString());
 782 
 783             if (s.size() == 0)
 784                 s.add("0");  // backwards compatibility
 785 
 786             return s.toArray(new String[s.size()]);
 787         }
 788 
 789         private String[] readPlatforms() throws IOException {
 790             // skip white space, then read and sort a list of comma-separated
 791             // platform names with no embedded white space. Since the
 792             // set of platforms (and their combinations) is small,
 793             // share the result amongst all equivalent entries.
 794             skipWhite();
 795             String s = readWord();
 796             String[] platforms = platformCache.get(s);
 797             if (platforms == null) {
 798                 // split string into sorted comma separated pieces
 799                 int n = 0;
 800                 for (int i = 0; i < s.length(); i++) {
 801                     if (s.charAt(i) == ',')
 802                         n++;
 803                 }
 804                 Set<String> ts = new TreeSet<>();
 805                 int start = 0;
 806                 int end = s.indexOf(',');
 807                 while (end != -1) {
 808                     ts.add(s.substring(start, end));
 809                     start = end + 1;
 810                     end = s.indexOf(',', start);
 811                 }
 812                 ts.add(s.substring(start));
 813                 platforms = ts.toArray(new String[ts.size()]);
 814                 platformCache.put(s, platforms);
 815             }
 816             return platforms;
 817         }
 818 
 819         private String readRest() throws IOException {
 820             // skip white space, then read up to the end of the line
 821             skipWhite();
 822             StringBuffer word = new StringBuffer(80);
 823             for ( ; !isEndOfLine(ch); ch = in.read())
 824                 word.append((char)ch);
 825             // skip over terminating character
 826             ch = in.read();
 827             return word.toString();
 828         }
 829 
 830         private String readWord() throws IOException {
 831             // read characters up to the next white space
 832             StringBuffer word = new StringBuffer(32);
 833             for ( ; !isEndOfLine(ch) && !isWhitespace(ch); ch = in.read())
 834                 word.append((char)ch);
 835             return word.toString();
 836         }
 837 
 838         private void skipComment() throws IOException, Fault {
 839             ch = in.read();
 840             // first # has already been read
 841             if (ch == '#') {
 842                 ch = in.read();
 843                 if (ch == '#') {
 844                     ch = in.read();
 845                     skipWhite();
 846                     String s = readWord();
 847                     if (s.equals("title")) {
 848                         skipWhite();
 849                         title = readRest();
 850                         return;
 851                     }
 852                 }
 853             }
 854             while (!isEndOfLine(ch))
 855                 ch = in.read();
 856         }
 857 
 858         private void skipWhite() throws IOException {
 859             // skip horizontal white space
 860             // input is line-oriented, so do not skip over end of line
 861             while (ch != -1 && isWhitespace(ch))
 862                 ch = in.read();
 863         }
 864 
 865         private Reader in;      // source stream being read
 866         private int ch;         // current character
 867         private Map<String, String[]> platformCache = new HashMap<>();
 868                                 // cache of results for readPlatforms
 869         private String title;
 870     };
 871 
 872     private static class Key {
 873         Key(String url) {
 874             relativeURL = url;
 875         }
 876 
 877         @Override
 878         public int hashCode() {
 879             // the hashCode for a key is the hashcode of the normalized URL.
 880             // The normalized URL is url.replace(File.separatorChar, '/').toLowerCase();
 881             int h = hash;
 882             if (h == 0) {
 883                 int len = relativeURL.length();
 884 
 885                 for (int i = 0; i < len; i++) {
 886                     char c = relativeURL.charAt(i);
 887                     if (!caseSensitive)
 888                         c = Character.toLowerCase(c);
 889 
 890                     if (c == sep)
 891                         c = '/';
 892                     h = 31*h + c;
 893                 }
 894                 hash = h;
 895             }
 896             return h;
 897         }
 898 
 899         @Override
 900         public boolean equals(Object o) {
 901             // Two keys are equal if their normalized URLs are equal.
 902             // The normalized URL is url.replace(File.separatorChar, '/').toLowerCase();
 903             if (o == null || !(o instanceof Key))
 904                 return false;
 905             String u1 = relativeURL;
 906             String u2 = ((Key) o).relativeURL;
 907             int len = u1.length();
 908             if (len != u2.length())
 909                 return false;
 910             for (int i = 0; i < len; i++) {
 911                 char c1 = u1.charAt(i);
 912 
 913                 if (c1 == sep)
 914                     c1 = '/';
 915                 else if(!caseSensitive)
 916                     c1 = Character.toLowerCase(c1);
 917 
 918                 char c2 = u2.charAt(i);
 919 
 920                 if (c2 == sep)
 921                     c2 = '/';
 922                 else if(!caseSensitive)
 923                     c2 = Character.toLowerCase(c2);
 924 
 925                 if (c1 != c2)
 926                     return false;
 927             }
 928             return true;
 929         }
 930 
 931         private static final char sep = File.separatorChar;
 932         private String relativeURL;
 933         private int hash;
 934     }
 935 
 936     /**
 937      * An entry in the exclude list.
 938      */
 939     public static final class Entry implements Comparable {
 940         /**
 941          * Create an ExcludeList entry.
 942          * @param u The URL for the test, specified relative to the test suite root.
 943          * @param tc One or more test cases within the test to be excluded.
 944          * @param b An array of bug identifiers, justifying why the test is excluded.
 945          * @param p An array of platform identifiers, on which the faults are
 946          *              known to occur
 947          * @param s A short synopsis of the reasons why the test is excluded.
 948          */
 949         public Entry(String u, String tc, String[] b, String[] p, String s) {
 950             if (b == null || p == null)
 951                 throw new NullPointerException();
 952 
 953             // The file format cannot support platforms but no bugids,
 954             // so fault that; other combinations (bugs, no platforms;
 955             // no bugs, no platforms etc) are acceptable.
 956             if (b.length == 0 &&  p.length > 0)
 957                 throw new IllegalArgumentException();
 958 
 959             relativeURL = u;
 960             testCase = tc;
 961             bugIdStrings = b;
 962             platforms = p;
 963             synopsis = s;
 964         }
 965         /**
 966          * Create an ExcludeList entry.
 967          * @param u The URL for the test, specified relative to the test suite root.
 968          * @param tc One or more test cases within the test to be excluded.
 969          * @param b An array of bug numbers, justifying why the test is excluded.
 970          * @param p An array of platform identifiers, on which the faults are
 971          *              known to occur
 972          * @param s A short synopsis of the reasons why the test is excluded.
 973      * @deprecated use constructor with String[] bugIDs instead
 974          */
 975         public Entry(String u, String tc, int[] b, String[] p, String s) {
 976             if (b == null || p == null)
 977                 throw new NullPointerException();
 978 
 979             // The file format cannot support platforms but no bugids,
 980             // so fault that; other combinations (bugs, no platforms;
 981             // no bugs, no platforms etc) are acceptable.
 982             if (b.length == 0 &&  p.length > 0)
 983                 throw new IllegalArgumentException();
 984 
 985             relativeURL = u;
 986             testCase = tc;
 987 
 988             bugIdStrings = new String[b.length];
 989             for (int i = 0; i < b.length; i++)
 990                 bugIdStrings[i] = String.valueOf(b[i]);
 991             bugIds = b;
 992 
 993             platforms = p;
 994             synopsis = s;
 995         }
 996 
 997         public int compareTo(Object o) {
 998             Entry e = (Entry) o;
 999             int n = relativeURL.compareTo(e.relativeURL);
1000             if (n == 0) {
1001                 if (testCase == null && e.testCase == null)
1002                     return 0;
1003                 else if (testCase == null)
1004                     return -1;
1005                 else if (e.testCase == null)
1006                     return +1;
1007                 else
1008                     return testCase.compareTo(e.testCase);
1009             }
1010             else
1011                 return n;
1012         }
1013 
1014         /**
1015          * Get the relative URL identifying the test referenced by this entry.
1016          * @return the relative URL identifying the test referenced by this entry
1017          */
1018         public String getRelativeURL() {
1019             return relativeURL;
1020         }
1021 
1022         /**
1023          * Get the (possibly empty) list of test cases for this entry.
1024          * An entry can have zero, one, or a comma separated list of TCs.
1025          *
1026          * @return List, or null if there are no test cases.
1027          */
1028         public String getTestCases() {
1029             return testCase;
1030         }
1031 
1032         /**
1033          * Get the same data as getTestCases(), but split into many Strings
1034          * This method is costly, so use with care.
1035          *
1036          * @return The parsed comma list, or null if there are no test cases.
1037          */
1038         public String[] getTestCaseList() {
1039             // code borrowed from StringArray
1040             // it is a little wasteful to recalc everytime but saves space
1041             if (testCase == null)
1042                 return null;
1043 
1044             Vector<String> v = new Vector<String>();
1045             int start = -1;
1046             for (int i = 0; i < testCase.length(); i++) {
1047                 if (testCase.charAt(i) == ',') {
1048                     if (start != -1)
1049                         v.addElement(testCase.substring(start, i));
1050                     start = -1;
1051                 } else
1052                     if (start == -1)
1053                         start = i;
1054             }
1055             if (start != -1)
1056                 v.addElement(testCase.substring(start));
1057 
1058             if (v.size() == 0)
1059                 return null;
1060 
1061             String[] a = new String[v.size()];
1062             v.copyInto(a);
1063             return a;
1064         }
1065 
1066         /**
1067          * Get the set of bug IDs referenced by this entry.
1068          * @return the bugs referenced by the entry
1069      * @deprecated use getBugIdStrings() instead
1070          */
1071         public int[] getBugIds() {
1072             if (bugIds == null) {
1073                 bugIds = new int[bugIdStrings.length];
1074                 for (int i = 0; i < bugIds.length; i++) {
1075                     try {
1076 
1077                         StringBuilder sb = new StringBuilder();
1078                         String str = bugIdStrings[i];
1079                         for (int j = 0; j < str.length(); j++) {
1080                             if (Character.isDigit(str.charAt(j))) {
1081                                 sb.append(str.charAt(j));
1082                             }
1083                         }
1084 
1085                         bugIds[i] = Integer.parseInt(sb.toString());
1086                     } catch (NumberFormatException e) {
1087                         bugIds[i] = -1;
1088                     }
1089                 }
1090             }
1091 
1092             return bugIds;
1093         }
1094 
1095         /**
1096          * Get the set of bug IDs referenced by this entry.
1097          * @return the bugs referenced by the entry
1098          */
1099         public String[] getBugIdStrings() {
1100             return bugIdStrings;
1101         }
1102 
1103         /**
1104          * Get the set of platforms or keywords associated with this entry.
1105          * These should normally give details about why the test has been
1106          * excluded.
1107          * @return the set of platforms or keywords associated with this entry
1108          */
1109         public String[] getPlatforms() {
1110             return platforms;
1111         }
1112 
1113         /**
1114          * Get a short description associated with this entry.
1115          * This should normally give details about why the test has been
1116          * excluded.
1117          * @return a short description associated with this entry
1118          */
1119         public String getSynopsis() {
1120             return synopsis;
1121         }
1122 
1123         /**
1124          * Create an entry from a string. The string should be formatted
1125          * as though it were a line of text in an exclude file.
1126          * @param text The text to be read
1127          * @return the first entry read from the supplied text
1128          * @throws ExcludeList.Fault if there is a problem reading the entry.
1129          */
1130         public static Entry read(String text) throws Fault {
1131             try {
1132                 return new Parser(new StringReader(text)).readEntry();
1133             }
1134             catch (IOException e) {
1135                 throw new Fault(i18n, "excl.badEntry", e);
1136             }
1137         }
1138 
1139         /**
1140          * Compare this entry against another.
1141          * @param o the object to compare against
1142          * @return true is the objects are bothe ExcludeList.Entries containing
1143          *    the same details
1144          */
1145         public boolean equals(Object o) {
1146             if (o instanceof Entry) {
1147                 Entry e = (Entry)o;
1148                 return equals(relativeURL, e.relativeURL)
1149                     && equals(testCase, e.testCase)
1150                     && equals(bugIdStrings, e.bugIdStrings)
1151                     && equals(platforms, e.platforms)
1152                     && equals(synopsis, e.synopsis);
1153             }
1154             else
1155                 return false;
1156         }
1157 
1158         @Override
1159         public int hashCode() {
1160             return relativeURL.hashCode();
1161         }
1162 
1163         @Override
1164         public String toString() {
1165             StringBuffer sb = new StringBuffer(64);
1166             sb.append(relativeURL);
1167             if (testCase != null) {
1168                 sb.append('[');
1169                 sb.append(testCase);
1170                 sb.append(']');
1171             }
1172             if (bugIdStrings != null) {
1173                 for (int i = 0; i<bugIdStrings.length; i++) {
1174                     sb.append(i == 0 ? ' ' : ',');
1175                     sb.append(bugIdStrings[i]);
1176                 }
1177             }
1178             if (platforms != null) {
1179                 for (int i = 0; i<platforms.length; i++) {
1180                     sb.append(i == 0 ? ' ' : ',');
1181                     sb.append(platforms[i]);
1182                 }
1183             }
1184             if (synopsis != null) {
1185                 sb.append(' ');
1186                 sb.append(synopsis);
1187             }
1188             return new String(sb);
1189         }
1190 
1191         private static boolean equals(int[] i1, int[] i2) {
1192             if (i1 == null || i2 == null)
1193                 return (i1 == null && i2 == null);
1194 
1195             if (i1.length != i2.length)
1196                 return false;
1197 
1198             for (int x = 0; x < i1.length; x++)
1199                 if (i1[x] != i2[x])
1200                     return false;
1201 
1202             return true;
1203         }
1204 
1205         private static boolean equals(String[] s1, String[] s2) {
1206             if (s1 == null || s2 == null)
1207                 return (s1 == null && s2 == null);
1208 
1209             if (s1.length != s2.length)
1210                 return false;
1211 
1212             for (int x = 0; x < s1.length; x++) {
1213                 if (!equals(s1[x], s2[x]))
1214                     return false;
1215             }
1216 
1217             return true;
1218         }
1219 
1220         private static boolean equals(String s1, String s2) {
1221             return (s1 == null && s2 == null
1222                     || s1 != null && s2 != null && s1.equals(s2));
1223         }
1224 
1225 
1226         private String relativeURL;
1227         private String testCase;
1228         private String[] bugIdStrings;
1229         private int[] bugIds; // null, unless required
1230         private String[] platforms;
1231         private String synopsis;
1232     }
1233 
1234     private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(ExcludeList.class);
1235     /**
1236      * The standard extension for exclude-list files. (".jtx")
1237      */
1238     public static final String EXCLUDEFILE_EXTN = ".jtx";
1239     private static boolean caseSensitive = Boolean.getBoolean("javatest.caseSensitiveJtx");
1240 }