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.finder;
  28 
  29 import java.io.*;
  30 import java.nio.charset.StandardCharsets;
  31 import java.util.*;
  32 
  33 import com.sun.javatest.TestDescription;
  34 import com.sun.javatest.TestEnvironment;
  35 import com.sun.javatest.TestFinder;
  36 import com.sun.javatest.util.I18NResourceBundle;
  37 import com.sun.javatest.util.StringArray;
  38 
  39 /**
  40  * A TestFinder that delegates to different test finders in different
  41  * areas of the test suite, as described by a special "map" file.
  42  */
  43 public class ChameleonTestFinder extends TestFinder {
  44     /**
  45      * Create an uninitialized ChameleonTestFinder.
  46      */
  47     public ChameleonTestFinder() {
  48         String ic = System.getProperty("javatest.chameleon.ignoreCase");
  49         if (ic != null)
  50             ignoreCase = ic.equals("true");
  51         else {
  52             String os = System.getProperty("os.name");
  53             ignoreCase = os.startsWith("Windows");
  54         }
  55         exclude(excludeNames);
  56     }
  57 
  58     /**
  59      * Exclude all files with a particular name from being scanned.
  60      * This will typically be for directories like SCCS, Codemgr_wsdata, etc
  61      * @param name The name of files to be excluded
  62      */
  63     public void exclude(String name) {
  64         excludeList.put(name, name);
  65     }
  66 
  67     /**
  68      * Exclude all files with particular names from being scanned.
  69      * This will typically be for directories like SCCS, Codemgr_wsdata, etc
  70      * @param names The names of files to be excluded
  71      */
  72     public void exclude(String[] names) {
  73         for (int i = 0; i < names.length; i++) {
  74             String name = names[i];
  75             excludeList.put(name, name);
  76         }
  77     }
  78 
  79     /**
  80      * Check whether or not to ignore case when matching files against entries.
  81      *
  82      * By default, case will be ignored on Windows platforms.
  83      * (<code><font size=-1>System.getProperty("os.name").startsWith("Windows")</font></code>)
  84      * This can be overridden by setting the following system property:
  85      * <pre>
  86      *    -Djavatest.chameleon.ignoreCase=true|false
  87      * </pre>
  88      *
  89      * This in turn can be overridden by using -ignoreCase or -dontIgnoreCase
  90      * in the args to {@link #init init}.
  91      *
  92      * @return  whether or not to ignore case when matching files against entries.
  93      * @see #setIgnoreCase
  94      */
  95     public boolean getIgnoreCase() {
  96         return ignoreCase;
  97     }
  98 
  99     /**
 100      * Set whether or not to ignore case when matching files against entries.
 101      *
 102      * @param b whether or not to ignore case when matching files against entries.
 103      * @see #getIgnoreCase
 104      */
 105     public void setIgnoreCase(boolean b) {
 106         ignoreCase = b;
 107     }
 108     /**
 109      * Generic initialization routine. You can also initialize the test finder
 110      * directly, with {@link #exclude}, {@link #readEntries}, etc.
 111      * @param args An array of strings giving initialization data.
 112      *  The primary option is "-f <em>file</em>" to specify the name
 113      *  of the file describing which test finder to use in which section
 114      *  of the test suite.
 115      * @param testSuiteRoot     The root file of the test suite.
 116      * @param env               This argument is not required by this test finder.
 117      * @throws TestFinder.Fault if an error is found during initialization.
 118      */
 119     public void init(String[] args, File testSuiteRoot, TestEnvironment env) throws Fault {
 120         super.init(args, testSuiteRoot, env);
 121 
 122         if (entryFile == null)
 123             throw new Fault(i18n, "cham.noConfigFile");
 124 
 125         if (!entryFile.isAbsolute())
 126             entryFile = new File(getRootDir(), entryFile.getPath());
 127 
 128         readEntries(entryFile);
 129     }
 130 
 131     /**
 132      * Read the entries in a file which describe which test finder to use
 133      * in which part of the test suite.
 134      * The file is read line by line. If a line is empty or begins with
 135      * a '#' character, the line is ignored. Otherwise the line is broken
 136      * up as follows:<br>
 137      * <em>directory-or-file</em>  <em>finder-class-name</em>  <em>finder-args</em> <em>...</em><br>
 138      * Then, when a file is to be read by the test finder, the entries above are
 139      * checked in the order they were read for an entry whose first word matches
 140      * the beginning of the name of the file to be read. If and when a match
 141      * is found, the test finder delegates the request to the test finder specified
 142      * in the rest of the entry that provided the match.
 143      * @param file              The file containing the entries to be read.
 144      * @throws TestFinder.Fault if a problem occurs while reading the file.
 145      */
 146     public void readEntries(File file) throws Fault {
 147         //System.err.println("reading " + file);
 148         SortedSet<Entry> s = new TreeSet<>(new Comparator<Entry>() {
 149             public int compare(Entry o1, Entry o2) {
 150                 int n = o1.compareTo(o2);
 151                 // this gives us the reverse of the order we want, so ...
 152                 return -n;
 153             }
 154         });
 155 
 156         try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
 157             String line;
 158             int lineNum = 0;
 159             while ((line = in.readLine()) != null) {
 160                 line = line.trim();
 161                 lineNum++;
 162                 if (line.startsWith("#")  ||  line.length() == 0)
 163                     continue;
 164 
 165                 String[] words = StringArray.split(line);
 166                 if (words.length < 2) {
 167                     throw new Fault(i18n, "cham.missingData",
 168                                     new Object[] {new Integer(lineNum), line});
 169                 }
 170 
 171                 String pattern = words[0];
 172                 String finderClassName = words[1];
 173                 String[] finderArgs = new String[words.length - 2];
 174                 System.arraycopy(words, 2, finderArgs, 0, finderArgs.length);
 175                 Entry e = new Entry(pattern, finderClassName, finderArgs);
 176                 s.add(e);
 177             }
 178         }
 179         catch (FileNotFoundException e) {
 180             throw new Fault(i18n, "cham.cantFindFile", file);
 181         }
 182         catch (IOException e) {
 183             throw new Fault(i18n, "cham.ioError",
 184                             new Object[] {file, e});
 185         }
 186 
 187         entryFile = file;
 188         entries = s.toArray(new Entry[s.size()]);
 189 
 190         //for (int i = 0; i < entries.length; i++)
 191         //    System.err.println(entries[i].prefix + "   " + entries[i].suffix);
 192     }
 193 
 194     protected int decodeArg(String[] args, int i) throws Fault {
 195         if (args[i].equals("-f") && i + 1 < args.length) {
 196             entryFile = new File(args[i + 1]);
 197             return 2;
 198         }
 199         else if (args[i].equalsIgnoreCase("-ignoreCase")) {
 200             ignoreCase = true;
 201             return 1;
 202         }
 203         else if (args[i].equalsIgnoreCase("-dontIgnoreCase")) {
 204             ignoreCase = false;
 205             return 1;
 206         }
 207         else
 208             return super.decodeArg(args, i);
 209     }
 210 
 211 
 212     /**
 213      * Scan a file, looking for test descriptions and other files that might
 214      * need to be scanned.  The implementation depends on the type of test
 215      * finder.
 216      * @param file The file to scan
 217      */
 218     protected void scan(File file) {
 219         //System.err.println("SCAN: " + file);
 220         if (file.isDirectory())
 221             scanDirectory(file);
 222         else
 223             scanFile(file);
 224     }
 225 
 226     public File[] getFiles() {
 227         if (currEntry == null)
 228             return super.getFiles();
 229         else if (currEntry.finder == null)
 230             return new File[0];
 231         else
 232             return currEntry.finder.getFiles();
 233     }
 234 
 235     public TestDescription[] getTests() {
 236         if (currEntry == null)
 237             return super.getTests();
 238         else if (currEntry.finder == null)
 239             return new TestDescription[0];
 240         else
 241             return currEntry.finder.getTests();
 242     }
 243 
 244 
 245     //-----internal routines----------------------------------------------------
 246 
 247     /**
 248      * Scan a directory, looking for more files to scan
 249      * @param dir The directory to scan
 250      */
 251     private void scanDirectory(File dir) {
 252         // scan the contents of the directory, checking for
 253         // subdirectories and other files that should be scanned
 254         currEntry = null;
 255         String[] names = dir.list();
 256         for (int i = 0; i < names.length; i++) {
 257             String name = names[i];
 258             // if the file should be ignored, skip it
 259             // This is typically for directories like SCCS etc
 260             if (excludeList.containsKey(name))
 261                 continue;
 262 
 263             foundFile(new File(dir, name));
 264         }
 265     }
 266 
 267     private void scanFile(File file) {
 268         // see if the file matches a registered test finder, and if so
 269         // delegate to that
 270         for (int i = 0; i < entries.length; i++) {
 271             if (entries[i].matches(file)) {
 272                 currEntry = entries[i];
 273                 currEntry.scanFile(file);
 274                 return;
 275             }
 276         }
 277         // no match found
 278         currEntry = null;
 279     }
 280 
 281     private Object newInstance(Class<?> c) throws Fault {
 282         try {
 283             return c.newInstance();
 284         }
 285         catch (InstantiationException e) {
 286             throw new Fault(i18n, "cham.cantCreateClass",
 287                             new Object[] {c.getName(), e});
 288         }
 289         catch (IllegalAccessException e) {
 290             throw new Fault(i18n, "cham.cantAccessClass",
 291                             new Object[] {c.getName(), e});
 292         }
 293     }
 294 
 295     private Class<?> loadClass(String className) throws Fault {
 296         try {
 297             if (loader == null)
 298                 return Class.forName(className);
 299             else
 300                 return loader.loadClass(className);
 301         }
 302         catch (ClassNotFoundException e) {
 303             throw new Fault(i18n, "cham.cantFindClass",
 304                             new Object[] {className, e});
 305         }
 306         catch (IllegalArgumentException e) {
 307             throw new Fault(i18n, "cham.badClassName", className);
 308         }
 309 
 310     }
 311 
 312     private TestEnvironment getEnv() {
 313         return env;
 314     }
 315 
 316     private File entryFile;
 317     private Entry[] entries;
 318     private boolean ignoreCase;
 319     private Entry currEntry;
 320     private ClassLoader loader;
 321     private Map<String, String> excludeList = new HashMap<>();
 322 
 323     private static final String[] excludeNames = {
 324         "SCCS", "deleted_files"
 325     };
 326 
 327     private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(ChameleonTestFinder.class);
 328 
 329     private class Entry {
 330         Entry(String pattern, String finderClassName, String[] finderArgs) {
 331             int star = pattern.indexOf('*');
 332             if (star == -1) {
 333                 prefix = pattern;
 334                 suffix = null;
 335             }
 336             else {
 337                 prefix = pattern.substring(0, star);
 338                 suffix = pattern.substring(star+1);
 339             }
 340             prefix = new File(getRootDir(), prefix.replace('/', File.separatorChar)).getPath();
 341             if (suffix != null)
 342                 suffix = suffix.replace('/', File.separatorChar);
 343 
 344             this.finderClassName = finderClassName;
 345             this.finderArgs = finderArgs;
 346 
 347             //System.err.println("created entry: prefix: " + prefix);
 348             //System.err.println("created entry: suffix: " + suffix);
 349             //System.err.println("created entry:  class: " + finderClassName);
 350             //System.err.println("created entry:   args: " + StringArray.join(finderArgs));
 351         }
 352 
 353         boolean matches(File file) {
 354             //System.err.println("checking " + file);
 355             //System.err.println(" prefix: " + prefix);
 356             //System.err.println(" suffix: " + suffix);
 357 
 358             String p = file.getPath();
 359             int pLen = p.length();
 360             int preLen = prefix.length();
 361 
 362             // if file does not match the prefix, return false
 363             if (!p.regionMatches(ignoreCase, 0, prefix, 0, preLen))
 364                 return false;
 365 
 366             // if there is a suffix, and the suffix is too short or the
 367             // file does not match the prefix, return false
 368             if (suffix != null) {
 369                 int sufLen = suffix.length();
 370 
 371                 if (sufLen > pLen)
 372                     return false;
 373 
 374                 if (!p.regionMatches(ignoreCase, pLen - sufLen, suffix, 0, sufLen))
 375                     return false;
 376             }
 377 
 378             // if we matched the prefix and possible suffix, we're done
 379             return true;
 380         }
 381 
 382         void scanFile(File file) {
 383             if (!initialized)
 384                 init();
 385 
 386             if (finder != null)
 387                 finder.read(file);
 388         }
 389 
 390         private void init() {
 391             try {
 392                 if (!finderClassName.equals("-")) {
 393                     finder = (TestFinder)(newInstance(loadClass(finderClassName)));
 394                     finder.init(finderArgs, getRoot(), getEnv());
 395                 }
 396             }
 397             catch (Fault e) {
 398                 error(i18n, "cham.cantInitClass", e.getMessage());
 399             }
 400             finally {
 401                 initialized = true;
 402             }
 403         }
 404 
 405         int compareTo(Entry other) {
 406             Entry a = this;
 407             Entry b = other;
 408             int apl = a.prefix.length();
 409             int bpl = b.prefix.length();
 410             if (apl < bpl)
 411                 return -1;
 412             else if (apl == bpl) {
 413                 int pc = a.prefix.compareTo(b.prefix);
 414                 if (pc != 0)
 415                     return pc;
 416                 else {
 417                     // prefixes are the same, check the suffixes
 418                     String as = a.suffix;
 419                     String bs = b.suffix;
 420                     if (as == null && bs == null)
 421                         return 0;
 422 
 423                     if (as == null)
 424                         return -1;
 425 
 426                     if (bs == null)
 427                         return +1;
 428 
 429                     return as.compareTo(bs);
 430                 }
 431             }
 432             else
 433                 return +1;
 434         }
 435 
 436         private String prefix;
 437         private String suffix;
 438         private String finderClassName;
 439         private String[] finderArgs;
 440         private TestFinder finder;
 441         private boolean initialized;
 442     }
 443 }