1 /*
   2  * $Id$
   3  *
   4  * Copyright (c) 1996, 2011, 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.File;
  30 import java.io.IOException;
  31 import java.io.Serializable;
  32 import java.util.Arrays;
  33 import java.util.Iterator;
  34 import java.util.Map;
  35 import java.util.Set;
  36 import java.util.TreeSet;
  37 import java.util.Vector;
  38 import com.sun.javatest.util.PropertyArray;
  39 import com.sun.javatest.util.StringArray;
  40 import java.net.MalformedURLException;
  41 import java.net.URL;
  42 import java.util.ArrayList;
  43 
  44 /**
  45  * TestDescription objects embody the parameters of a test and provide the
  46  * ability to run a test. The parameters are normally found by parsing
  47  * HTML files and looking for distinguished constructions whose parameters
  48  * provide the necessary description of a test.
  49  */
  50 
  51 public class TestDescription implements Serializable
  52 {
  53     /**
  54      * Construct a test description from the parameters of a recognized descriptions.
  55      *
  56      * @param root      The root file of the test suite
  57      * @param file      The file containing the test description
  58      * @param params    The collected parameters of the test description
  59      * @throws IllegalArgumentException if the file argument is an absolute
  60      * filename and does not begin with the root filename.
  61      *
  62      */
  63     public TestDescription(File root, File file, Map<?, ?> params)
  64                 throws IllegalArgumentException {
  65 
  66         synchronized (this.getClass()) {
  67             if (root.equals(cachedRoot))
  68                 rootDir = cachedRootDir;
  69             else {
  70                 if (root.exists() ? root.isFile() : root.getName().endsWith(".html"))
  71                     rootDir = root.getParent();
  72                 else
  73                     rootDir = root.getPath();
  74 
  75                 // cache root->rootDir map to avoid making extra files
  76                 cachedRoot = root;
  77                 cachedRootDir = rootDir;
  78             }
  79         }
  80 
  81         String fp = file.getPath();
  82         String rootRelativeFile;
  83         if (file.isAbsolute()) {
  84             String rp = rootDir;
  85             if (! (fp.startsWith(rp) && fp.charAt(rp.length()) == File.separatorChar))
  86                 throw new IllegalArgumentException("file must be relative to root: " + file);
  87             rootRelativeFile = fp.substring(rp.length() + 1);
  88         }
  89         else
  90             rootRelativeFile = fp;
  91         rootRelativePath = rootRelativeFile.replace(File.separatorChar, '/');
  92 
  93         Vector<String> v = new Vector<>(0, params.size() * 2);
  94         for (Map.Entry<?, ?> entry : params.entrySet()) {
  95             insert(v, (String) entry.getKey(), (String) entry.getValue());
  96         }
  97         fields = new String[v.size()];
  98         v.copyInto(fields);
  99     }
 100 
 101     /**
 102      * Internal constructor used by load()
 103      */
 104     private TestDescription(String root, String file, String[] params) {
 105         rootDir = root;
 106         // skip over the root part of the filename.
 107         char sep = file.charAt(root.length());
 108         rootRelativePath = file.substring(root.length() + 1).replace(sep, '/');
 109 
 110         Vector<String> v = new Vector<>(0, params.length);
 111         for (int i = 0; i < params.length; i += 2) {
 112             String key = params[i];
 113             if (!(key.startsWith("$") || key.equals("testsuite") ||  key.equals("file"))) {
 114                 // don't keep synthetic values from save;
 115                 String value = params[i+1];
 116                 insert(v, key, value);
 117             }
 118         }
 119         fields = new String[v.size()];
 120         v.copyInto(fields);
 121     }
 122 
 123     @Override
 124     public int hashCode() {
 125         int hash = 3;
 126         hash = 43 * hash + Arrays.deepHashCode(this.fields);
 127         return hash;
 128     }
 129 
 130     @Override
 131     public boolean equals(Object td) {
 132         if (!(td instanceof TestDescription))
 133             return false;
 134 
 135         TestDescription otherTd = (TestDescription)td;
 136 
 137         // a quick and simple check
 138         if (otherTd.getParameterCount() != getParameterCount()) {
 139             return false;
 140         }
 141 
 142         // raw compare
 143         int pos = 0;
 144         while (pos < fields.length) {
 145             String otherVal = otherTd.getParameter(fields[pos]);
 146 
 147             if (otherVal == null ||
 148                 !otherVal.equals(fields[pos+1])) {
 149                 return false;
 150             }
 151 
 152             pos += 2;
 153         }
 154 
 155         return true;
 156     }
 157 
 158     /**
 159      * Get the directory for this test description.
 160      * @return the directory containing this test description
 161      */
 162     public File getDir() {
 163         return new File(getFile().getParent());
 164     }
 165 
 166     /**
 167      * Get the file for this test description.
 168      * WARNING: If this description has been read in from a .jtr file, the rootDir
 169      * may be inappropriate for this system.
 170      * @return the file containing this test description
 171      */
 172     public File getFile() {
 173         return new File(rootDir, rootRelativePath.replace('/', File.separatorChar));
 174     }
 175 
 176     /**
 177      * Get the id within the file for this test description.
 178      * @return the id within the file of this test description
 179      */
 180     public String getId() {
 181         return getParameter("id");
 182     }
 183 
 184     /**
 185      * Get the title of this test description. This title is determined from
 186      * the "title" parameter, if present, defaulting to the value returned
 187      * by <code>getName()</code>.
 188      * @return the title of this test description
 189      */
 190     public String getTitle() {
 191         // default title to name
 192         String title = getParameter("title");
 193         if (title == null)
 194             title = getName();
 195 
 196         return title;
 197     }
 198 
 199     /**
 200      * Get the name of this test description; if not given explicitly,
 201      * it defaults to the filename root of the first source file.
 202      * @return the name of this test description
 203      */
 204     public String getName() {
 205 
 206         // the name used to be a parameter you could set explicitly
 207         // now, it is based on the file and id
 208         int lastSep = rootRelativePath.lastIndexOf('/');
 209         String name = (lastSep == -1 ?
 210                        rootRelativePath :
 211                        rootRelativePath.substring(lastSep + 1));
 212 
 213         // strip off extension
 214         int dot = name.indexOf('.');
 215         if (dot != -1)
 216             name = name.substring(0, dot);
 217 
 218         String id = getParameter("id");
 219         if (id != null)
 220             name = name + "_" + id;
 221 
 222         return name;
 223     }
 224 
 225     /**
 226      * Get the set of keywords for this test description,
 227      * as specified by the "keywords" parameter.
 228      * @return the set of keywords
 229      */
 230     public String[] getKeywords() {
 231         return StringArray.split(getParameter("keywords"));
 232     }
 233 
 234     /**
 235      * Get the set of keywords for this test description,
 236      * as specified by the "keywords" parameter.
 237      * They are returned in canonical form (lower-case).
 238      * @return the set of keywords
 239      */
 240     public Set<String> getKeywordTable() {
 241         String[] keys = StringArray.split(getParameter("keywords"));
 242         Set<String> s = new TreeSet<>();
 243         for (int i = 0; i < keys.length; i++) {
 244             String k = keys[i].toLowerCase();
 245             s.add(k);
 246         }
 247         return s;
 248     }
 249 
 250     /**
 251      * Get the set of source files for this test description,
 252      * as specified by the "source" parameter.
 253      * @return The sources as specified in the HTML test description
 254      * @see #getSourceFiles
 255      */
 256     public String[] getSources() {
 257         return StringArray.split(getParameter("source"));
 258     }
 259 
 260     /**
 261      * Get the set of source files for this test description,
 262      * as specified by the "source" parameter. The files in the
 263      * "source" parameter should normally be relative, in which
 264      * case, they will be evaluated relative to the directory
 265      * containing this test description. Then, if any of the
 266      * files are under the user's current directory, they will
 267      * be returned relative to that directory; otherwise, they
 268      * will be returned as absolute filenames.
 269      * @return filenames specified by the source parameter.
 270      * @see #getSources
 271      */
 272     public File[] getSourceFiles() {
 273         String dir  = getFile().getParent();
 274         String[] srcs = getSources();
 275         File[] sourceFiles = new File[srcs.length];
 276         String userCurrDir = System.getProperty("user.dir") + File.separator;
 277         for (int i = 0; i < srcs.length; i++) {
 278             File f = new File(dir, srcs[i].replace('/', File.separatorChar));
 279 
 280             // normalize name
 281             try {
 282                 f = f.getCanonicalFile();
 283             } catch (IOException ex) {
 284                 ex.printStackTrace();
 285             }
 286 
 287             String s = f.getPath();
 288             if (s.startsWith(userCurrDir)) {
 289                 s = s.substring(userCurrDir.length());
 290                 sourceFiles[i] = new File(s);
 291             } else
 292                 sourceFiles[i] = f;
 293         }
 294         return sourceFiles;
 295     }
 296 
 297 
 298     /**
 299      * Get a list of associated files for a specified test description.
 300      * Normally, this will include the file containing the test description,
 301      * and any source files used by the test.  By default, the source files
 302      * are determined from the test description's "source" entry.
 303      * @return a list of associated files for this test description
 304      */
 305     public URL[] getSourceURLs() {
 306         ArrayList<File> res = new ArrayList<File>();
 307 
 308         // always include the file containing the test description
 309         res.add(getFile());
 310         // add in files given in source parameter
 311         res.addAll(Arrays.asList(getSourceFiles()));
 312 
 313         URL[] urls = new URL[res.size()];
 314         for (int i = 0; i < res.size(); i++) {
 315             try {
 316                 urls[i] = res.get(i).toURI().toURL();
 317             } catch (MalformedURLException ex) {
 318                 ex.printStackTrace();
 319                 urls[i] = null;
 320             }
 321         }
 322 
 323         return urls;
 324     }
 325 
 326 
 327     /**
 328      * Get the optional class directory for this test description,
 329      * as specified by the "classDir" parameter.
 330      * @return the class directory, or null if not specified
 331      * @deprecated use <code>getParameter("classDir")</code> instead
 332      */
 333     public String getClassDir() {
 334         return getParameter("classDir");
 335     }
 336 
 337     /**
 338      * Get the execution class for this test description,
 339      * as specified by the "executeClass" parameter.
 340      * @return the execute class name, or null if not specified
 341      * @deprecated use <code>getParameter("executeClass")</code> instead
 342      */
 343     public String getExecuteClass() {
 344         return getParameter("executeClass");
 345     }
 346 
 347     /**
 348      * Get the execution args for this test description,
 349      * as specified by the "executeArgs" parameter.
 350      * @return the execute args, or null if not specified
 351      * @deprecated use <code>getParameter("executeArgs")</code> instead
 352      */
 353     public String getExecuteArgs() {
 354         return getParameter("executeArgs");
 355     }
 356 
 357     /**
 358      * Get the requested timeout value for this test description,
 359      * as specified by the "timeout" parameter.
 360      * @return the timeout value, or 0 if not specified
 361      * @deprecated  use <code>getParameter("timeout")</code> instead
 362      */
 363     public int getTimeout() {
 364         String t = getParameter("timeout");
 365         if (t == null)
 366             return 0;
 367         else
 368             return Integer.parseInt(t);
 369     }
 370 
 371     /**
 372      * Get the root file for this test suite;
 373      * THIS IS PROVIDED FOR BACKWARDS COMPATIBILTY FOR JCK ONLY.
 374      * It returns the name of testsuite.html within the root directory
 375      * of the test suite.
 376      * WARNING: If this description has been read in from a .jtr file, the rootDir
 377      * may be inappropriate for this system.
 378      * @return the root file for this test suite
 379      * @see #getRootDir
 380      * @deprecated No longer relevant for some test suites, so will not be supported
 381      *    in the future.  If needed the value can be determined by asking the test
 382      *    suite's <code>TestFinder</code>.
 383      */
 384     public File getRoot() {
 385         return new File(rootDir, "testsuite.html");
 386     }
 387 
 388     /**
 389      * Get the root directory for this test suite
 390      * WARNING: If this description has been read in from a .jtr file, the rootDir
 391      * may be inappropriate for this system.
 392      * @return the root directory for this test suite
 393      */
 394     public String getRootDir() {
 395         return rootDir;
 396     }
 397 
 398     /**
 399      * Get the path of the test, relative to the root dir for the test suite.
 400      * This is the path to the source file for this description.
 401      * The internal separator is always '/'.
 402      * @return the path for this test description within the test suite
 403      */
 404     public String getRootRelativePath() {
 405         return rootRelativePath;
 406     }
 407 
 408     /**
 409      * Get the file of the test, relative to the root dir for the test suite.
 410      * @return A platform specific path to the source file.
 411      */
 412     public File getRootRelativeFile() {
 413         return new File(rootRelativePath.replace('/', File.separatorChar));
 414     }
 415 
 416     /**
 417      * Get the url of the test, relative to the root dir for the test suite.
 418      * This is the path to the source file for this description, plus the
 419      * test id if necessary.  Again, the path separator is always '/'.
 420      * @return a relative URL for this test within the test suite
 421      */
 422     public String getRootRelativeURL() {
 423         if (rrurl == null) {
 424             String id = getParameter("id");
 425             rrurl = (id == null ?  rootRelativePath : rootRelativePath + "#" + id);
 426             rrurl = rrurl.intern();
 427         }
 428 
 429         return rrurl;
 430     }
 431 
 432     /**
 433      * Get the path of the test directory, relative to the root directory for the test suite.
 434      * @return the a relative path to the directory containing this test description
 435      *
 436      * @deprecated Use getRootRelativeFile().getParent()
 437      */
 438     public File getRootRelativeDir() {
 439         String p = getRootRelativeFile().getParent();
 440         return (p == null ? new File(".") : new File(p));
 441     }
 442 
 443     /**
 444      * Get the number of parameters contained in this test description.
 445      * @return the number of parameters
 446      */
 447     public int getParameterCount() {
 448         return fields.length / 2;
 449     }
 450 
 451     /**
 452      * Get an iterator for the names of the parameters contained in
 453      * this test description.
 454      * @return an iterator for the names of the parameters
 455      */
 456     public Iterator<String> getParameterKeys() {
 457         return new Iterator<String>() {
 458             int pos = 0;
 459 
 460             public boolean hasNext() {
 461                 if(fields == null || fields.length == 0 ||
 462                    pos >= fields.length) {
 463                    return false;
 464                 } else {
 465                    return true;
 466                 }
 467             }
 468 
 469             public String next() {
 470                 if(fields == null || fields.length == 0 ||
 471                    pos == fields.length) {
 472                    return null;
 473                 } else {
 474                    String current = fields[pos];
 475                    pos += 2;
 476                    return current;
 477                 }
 478             }
 479 
 480             public void remove() {
 481                 throw new UnsupportedOperationException();
 482             }
 483         };
 484     }
 485 
 486     /**
 487      * Get a parameter of the test description by name.
 488      * @param key the name of the parameter value to be returned
 489      * @return the value of the specified parameter, or null if not found
 490      */
 491     public String getParameter(String key) {
 492         int lower = 0;
 493         int upper = fields.length - 2;
 494         int mid;
 495 
 496         if (upper < 0)
 497             return null;
 498 
 499         String last = fields[upper];
 500         int cmp = key.compareTo(last);
 501         if (cmp > 0)
 502             return null;
 503 
 504         while (lower <= upper) {
 505             // in next line, take care to ensure that mid is always even
 506             mid = lower + ((upper - lower) / 4) * 2;
 507             String e = fields[mid];
 508             cmp = key.compareTo(e);
 509             if (cmp < 0) {
 510                 upper = mid - 2;
 511             }
 512             else if (cmp > 0) {
 513                 lower = mid + 2;
 514             }
 515             else
 516                 return fields[mid+1];
 517         }
 518 
 519         // did not find an exact match
 520         return null;
 521     }
 522 
 523     /**
 524      * Simple standard debugging output.
 525      */
 526     public String toString() {
 527         return ("TestDescription[" + getTitle() + "]");
 528     }
 529 
 530     /**
 531      * Save TestDescription to a dictionary
 532      * WARNING: If this description has been read in from a .jtr file, the rootDir
 533      * may be inappropriate for this system.
 534      */
 535     void save(Map<String, String> p) {
 536         saveField(p, "$root", rootDir);
 537         saveField(p, "$file", getFile().getPath());
 538         for (int i = 0; i < fields.length; i+=2) {
 539             saveField(p, fields[i], fields[i+1]);
 540         }
 541     }
 542 
 543     private void saveField(Map<String, String> p, String key, String value) {
 544         if (value != null)
 545             p.put(key, value);
 546     }
 547 
 548     /**
 549      * Recover TestDescription from saved dictionary
 550      */
 551     static TestDescription load(String[] params) {
 552         //File r = new File((String)d.get("testsuite"));
 553         //if (!r.isDirectory())
 554         //    r = new File(r.getParent());
 555         //File f = new File((String)d.get("file"));
 556         String r = PropertyArray.get(params, "$root");
 557         if (r == null)
 558             r = PropertyArray.get(params, "testsuite");
 559         String f = PropertyArray.get(params, "$file");
 560         if (f == null)
 561             f = PropertyArray.get(params, "file");
 562         return new TestDescription(r, f, params);
 563     }
 564 
 565     private static void insert(Vector<String> v, String key, String value) {
 566         int lower = 0;
 567         int upper = v.size() - 2;
 568         int mid = 0;
 569 
 570         if (upper < 0) {
 571             v.addElement(key);
 572             v.addElement(value);
 573             return;
 574         }
 575 
 576         String last = v.elementAt(upper);
 577         int cmp = key.compareTo(last);
 578         if (cmp > 0) {
 579             v.addElement(key);
 580             v.addElement(value);
 581             return;
 582         }
 583 
 584         while (lower <= upper) {
 585             // in next line, take care to ensure that mid is always even
 586             mid = lower + ((upper - lower) / 4) * 2;
 587             String e = v.elementAt(mid);
 588             cmp = key.compareTo(e);
 589             if (cmp < 0) {
 590                 upper = mid - 2;
 591             }
 592             else if (cmp > 0) {
 593                 lower = mid + 2;
 594             }
 595             else
 596                 throw new Error("should not happen");
 597         }
 598 
 599         // did not find an exact match (we did not expect to)
 600         // adjust the insert point
 601         if (cmp > 0)
 602             mid += 2;
 603 
 604         v.insertElementAt(key, mid);
 605         v.insertElementAt(value, mid+1);
 606     }
 607 
 608     //-----member variables-------------------------------------------------------
 609 
 610     /**
 611      * Root directory for the test suite
 612      * WARNING: If this description has been read in from a .jtr file, the rootDir
 613      * may be inappropriate for this system.
 614      * @serial
 615      */
 616     private String rootDir;
 617 
 618     /**
 619      * Root relative path for this test description within the test suite
 620      * @serial
 621      */
 622     private String rootRelativePath;
 623 
 624     /**
 625      * The data for this test description, organized as a sequence of
 626      * name-value pairs.
 627      * @serial
 628      */
 629     private String[] fields;
 630 
 631     /**
 632      * Cached version of the root relative path.
 633      * @see #getRootRelativeURL
 634      */
 635     private String rrurl;
 636 
 637     private static File cachedRoot;
 638     private static String cachedRootDir;
 639 }
 640