1 /*
   2  * Copyright 2003-2008 Sun Microsystems, Inc.  All Rights Reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Sun designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Sun in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
  22  * CA 95054 USA or visit www.sun.com if you need additional information or
  23  * have any questions.
  24  */
  25 
  26 package com.sun.tools.javac.file;
  27 
  28 import java.io.File;
  29 import java.io.IOException;
  30 import java.net.MalformedURLException;
  31 import java.net.URL;
  32 import java.util.HashMap;
  33 import java.util.HashSet;
  34 import java.util.Map;
  35 import java.util.Set;
  36 import java.util.Collection;
  37 import java.util.Collections;
  38 import java.util.LinkedHashSet;
  39 import java.util.StringTokenizer;
  40 import java.util.zip.ZipFile;
  41 import javax.tools.JavaFileManager.Location;
  42 
  43 import com.sun.tools.javac.code.Lint;
  44 import com.sun.tools.javac.util.Context;
  45 import com.sun.tools.javac.util.ListBuffer;
  46 import com.sun.tools.javac.util.Log;
  47 import com.sun.tools.javac.util.Options;
  48 
  49 import static javax.tools.StandardLocation.*;
  50 import static com.sun.tools.javac.main.OptionName.*;
  51 
  52 /** This class converts command line arguments, environment variables
  53  *  and system properties (in File.pathSeparator-separated String form)
  54  *  into a boot class path, user class path, and source path (in
  55  *  Collection<String> form).
  56  *
  57  *  <p><b>This is NOT part of any API supported by Sun Microsystems.  If
  58  *  you write code that depends on this, you do so at your own risk.
  59  *  This code and its internal interfaces are subject to change or
  60  *  deletion without notice.</b>
  61  */
  62 public class Paths {
  63 
  64     /** The context key for the todo list */
  65     protected static final Context.Key<Paths> pathsKey =
  66         new Context.Key<Paths>();
  67 
  68     /** Get the Paths instance for this context.
  69      *  @param context the context
  70      *  @return the Paths instance for this context
  71      */
  72     public static Paths instance(Context context) {
  73         Paths instance = context.get(pathsKey);
  74         if (instance == null)
  75             instance = new Paths(context);
  76         return instance;
  77     }
  78 
  79     /** The log to use for warning output */
  80     private Log log;
  81 
  82     /** Collection of command-line options */
  83     private Options options;
  84 
  85     /** Handler for -Xlint options */
  86     private Lint lint;
  87 
  88     /** Access to (possibly cached) file info */
  89     private FSInfo fsInfo;
  90 
  91     protected Paths(Context context) {
  92         context.put(pathsKey, this);
  93         pathsForLocation = new HashMap<Location,Path>(16);
  94         setContext(context);
  95     }
  96 
  97     void setContext(Context context) {
  98         log = Log.instance(context);
  99         options = Options.instance(context);
 100         lint = Lint.instance(context);
 101         fsInfo = FSInfo.instance(context);
 102     }
 103 
 104     /** Whether to warn about non-existent path elements */
 105     private boolean warn;
 106 
 107     private Map<Location, Path> pathsForLocation;
 108 
 109     private boolean inited = false; // TODO? caching bad?
 110 
 111     /**
 112      * rt.jar as found on the default bootclass path.  If the user specified a
 113      * bootclasspath, null is used.
 114      */
 115     private File bootClassPathRtJar = null;
 116 
 117     Path getPathForLocation(Location location) {
 118         Path path = pathsForLocation.get(location);
 119         if (path == null)
 120             setPathForLocation(location, null);
 121         return pathsForLocation.get(location);
 122     }
 123 
 124     void setPathForLocation(Location location, Iterable<? extends File> path) {
 125         // TODO? if (inited) throw new IllegalStateException
 126         // TODO: otherwise reset sourceSearchPath, classSearchPath as needed
 127         Path p;
 128         if (path == null) {
 129             if (location == CLASS_PATH)
 130                 p = computeUserClassPath();
 131             else if (location == PLATFORM_CLASS_PATH)
 132                 p = computeBootClassPath();
 133             else if (location == ANNOTATION_PROCESSOR_PATH)
 134                 p = computeAnnotationProcessorPath();
 135             else if (location == SOURCE_PATH)
 136                 p = computeSourcePath();
 137             else
 138                 // no defaults for other paths
 139                 p = null;
 140         } else {
 141             p = new Path();
 142             for (File f: path)
 143                 p.addFile(f, warn); // TODO: is use of warn appropriate?
 144         }
 145         pathsForLocation.put(location, p);
 146     }
 147 
 148     protected void lazy() {
 149         if (!inited) {
 150             warn = lint.isEnabled(Lint.LintCategory.PATH);
 151 
 152             pathsForLocation.put(PLATFORM_CLASS_PATH, computeBootClassPath());
 153             pathsForLocation.put(CLASS_PATH, computeUserClassPath());
 154             pathsForLocation.put(SOURCE_PATH, computeSourcePath());
 155 
 156             inited = true;
 157         }
 158     }
 159 
 160     public Collection<File> bootClassPath() {
 161         lazy();
 162         return Collections.unmodifiableCollection(getPathForLocation(PLATFORM_CLASS_PATH));
 163     }
 164     public Collection<File> userClassPath() {
 165         lazy();
 166         return Collections.unmodifiableCollection(getPathForLocation(CLASS_PATH));
 167     }
 168     public Collection<File> sourcePath() {
 169         lazy();
 170         Path p = getPathForLocation(SOURCE_PATH);
 171         return p == null || p.size() == 0
 172             ? null
 173             : Collections.unmodifiableCollection(p);
 174     }
 175 
 176     boolean isBootClassPathRtJar(File file) {
 177         return file.equals(bootClassPathRtJar);
 178     }
 179 
 180     /**
 181      * Split a path into its elements. Empty path elements will be ignored.
 182      * @param path The path to be split
 183      * @return The elements of the path
 184      */
 185     private static Iterable<File> getPathEntries(String path) {
 186         return getPathEntries(path, null);
 187     }
 188 
 189     /**
 190      * Split a path into its elements. If emptyPathDefault is not null, all
 191      * empty elements in the path, including empty elements at either end of
 192      * the path, will be replaced with the value of emptyPathDefault.
 193      * @param path The path to be split
 194      * @param emptyPathDefault The value to substitute for empty path elements,
 195      *  or null, to ignore empty path elements
 196      * @return The elements of the path
 197      */
 198     private static Iterable<File> getPathEntries(String path, File emptyPathDefault) {
 199         ListBuffer<File> entries = new ListBuffer<File>();
 200         int start = 0;
 201         while (start <= path.length()) {
 202             int sep = path.indexOf(File.pathSeparatorChar, start);
 203             if (sep == -1)
 204                 sep = path.length();
 205             if (start < sep)
 206                 entries.add(new File(path.substring(start, sep)));
 207             else if (emptyPathDefault != null)
 208                 entries.add(emptyPathDefault);
 209             start = sep + 1;
 210         }
 211         return entries;
 212     }
 213 
 214     private class Path extends LinkedHashSet<File> {
 215         private static final long serialVersionUID = 0;
 216 
 217         private boolean expandJarClassPaths = false;
 218         private Set<File> canonicalValues = new HashSet<File>();
 219 
 220         public Path expandJarClassPaths(boolean x) {
 221             expandJarClassPaths = x;
 222             return this;
 223         }
 224 
 225         /** What to use when path element is the empty string */
 226         private File emptyPathDefault = null;
 227 
 228         public Path emptyPathDefault(File x) {
 229             emptyPathDefault = x;
 230             return this;
 231         }
 232 
 233         public Path() { super(); }
 234 
 235         public Path addDirectories(String dirs, boolean warn) {
 236             if (dirs != null)
 237                 for (File dir : getPathEntries(dirs))
 238                     addDirectory(dir, warn);
 239             return this;
 240         }
 241 
 242         public Path addDirectories(String dirs) {
 243             return addDirectories(dirs, warn);
 244         }
 245 
 246         private void addDirectory(File dir, boolean warn) {
 247             if (!dir.isDirectory()) {
 248                 if (warn)
 249                     log.warning("dir.path.element.not.found", dir);
 250                 return;
 251             }
 252 
 253             File[] files = dir.listFiles();
 254             if (files == null)
 255                 return;
 256 
 257             for (File direntry : files) {
 258                 if (isArchive(direntry))
 259                     addFile(direntry, warn);
 260             }
 261         }
 262 
 263         public Path addFiles(String files, boolean warn) {
 264             if (files != null)
 265                 for (File file : getPathEntries(files, emptyPathDefault))
 266                     addFile(file, warn);
 267             return this;
 268         }
 269 
 270         public Path addFiles(String files) {
 271             return addFiles(files, warn);
 272         }
 273 
 274         public void addFile(File file, boolean warn) {
 275             File canonFile = fsInfo.getCanonicalFile(file);
 276             if (contains(file) || canonicalValues.contains(canonFile)) {
 277                 /* Discard duplicates and avoid infinite recursion */
 278                 return;
 279             }
 280 
 281             if (! fsInfo.exists(file)) {
 282                 /* No such file or directory exists */
 283                 if (warn)
 284                     log.warning("path.element.not.found", file);
 285             } else if (fsInfo.isFile(file)) {
 286                 /* File is an ordinary file. */
 287                 if (!isArchive(file)) {
 288                     /* Not a recognized extension; open it to see if
 289                      it looks like a valid zip file. */
 290                     try {
 291                         ZipFile z = new ZipFile(file);
 292                         z.close();
 293                         if (warn)
 294                             log.warning("unexpected.archive.file", file);
 295                     } catch (IOException e) {
 296                         // FIXME: include e.getLocalizedMessage in warning
 297                         if (warn)
 298                             log.warning("invalid.archive.file", file);
 299                         return;
 300                     }
 301                 }
 302             }
 303 
 304             /* Now what we have left is either a directory or a file name
 305                confirming to archive naming convention */
 306             super.add(file);
 307             canonicalValues.add(canonFile);
 308 
 309             if (expandJarClassPaths && fsInfo.exists(file) && fsInfo.isFile(file))
 310                 addJarClassPath(file, warn);
 311         }
 312 
 313         // Adds referenced classpath elements from a jar's Class-Path
 314         // Manifest entry.  In some future release, we may want to
 315         // update this code to recognize URLs rather than simple
 316         // filenames, but if we do, we should redo all path-related code.
 317         private void addJarClassPath(File jarFile, boolean warn) {
 318             try {
 319                 for (File f: fsInfo.getJarClassPath(jarFile)) {
 320                     addFile(f, warn);
 321                 }
 322             } catch (IOException e) {
 323                 log.error("error.reading.file", jarFile, e.getLocalizedMessage());
 324             }
 325         }
 326     }
 327 
 328     private Path computeBootClassPath() {
 329         bootClassPathRtJar = null;
 330         String optionValue;
 331         Path path = new Path();
 332 
 333         path.addFiles(options.get(XBOOTCLASSPATH_PREPEND));
 334 
 335         if ((optionValue = options.get(ENDORSEDDIRS)) != null)
 336             path.addDirectories(optionValue);
 337         else
 338             path.addDirectories(System.getProperty("java.endorsed.dirs"), false);
 339 
 340         if ((optionValue = options.get(BOOTCLASSPATH)) != null) {
 341             path.addFiles(optionValue);
 342         } else {
 343             // Standard system classes for this compiler's release.
 344             String files = System.getProperty("sun.boot.class.path");
 345             path.addFiles(files, false);
 346             File rt_jar = new File("rt.jar");
 347             for (File file : getPathEntries(files)) {
 348                 if (new File(file.getName()).equals(rt_jar))
 349                     bootClassPathRtJar = file;
 350             }
 351         }
 352 
 353         path.addFiles(options.get(XBOOTCLASSPATH_APPEND));
 354 
 355         // Strictly speaking, standard extensions are not bootstrap
 356         // classes, but we treat them identically, so we'll pretend
 357         // that they are.
 358         if ((optionValue = options.get(EXTDIRS)) != null)
 359             path.addDirectories(optionValue);
 360         else
 361             path.addDirectories(System.getProperty("java.ext.dirs"), false);
 362 
 363         return path;
 364     }
 365 
 366     private Path computeUserClassPath() {
 367         String cp = options.get(CLASSPATH);
 368 
 369         // CLASSPATH environment variable when run from `javac'.
 370         if (cp == null) cp = System.getProperty("env.class.path");
 371 
 372         // If invoked via a java VM (not the javac launcher), use the
 373         // platform class path
 374         if (cp == null && System.getProperty("application.home") == null)
 375             cp = System.getProperty("java.class.path");
 376 
 377         // Default to current working directory.
 378         if (cp == null) cp = ".";
 379 
 380         return new Path()
 381             .expandJarClassPaths(true)        // Only search user jars for Class-Paths
 382             .emptyPathDefault(new File("."))  // Empty path elt ==> current directory
 383             .addFiles(cp);
 384     }
 385 
 386     private Path computeSourcePath() {
 387         String sourcePathArg = options.get(SOURCEPATH);
 388         if (sourcePathArg == null)
 389             return null;
 390 
 391         return new Path().addFiles(sourcePathArg);
 392     }
 393 
 394     private Path computeAnnotationProcessorPath() {
 395         String processorPathArg = options.get(PROCESSORPATH);
 396         if (processorPathArg == null)
 397             return null;
 398 
 399         return new Path().addFiles(processorPathArg);
 400     }
 401 
 402     /** The actual effective locations searched for sources */
 403     private Path sourceSearchPath;
 404 
 405     public Collection<File> sourceSearchPath() {
 406         if (sourceSearchPath == null) {
 407             lazy();
 408             Path sourcePath = getPathForLocation(SOURCE_PATH);
 409             Path userClassPath = getPathForLocation(CLASS_PATH);
 410             sourceSearchPath = sourcePath != null ? sourcePath : userClassPath;
 411         }
 412         return Collections.unmodifiableCollection(sourceSearchPath);
 413     }
 414 
 415     /** The actual effective locations searched for classes */
 416     private Path classSearchPath;
 417 
 418     public Collection<File> classSearchPath() {
 419         if (classSearchPath == null) {
 420             lazy();
 421             Path bootClassPath = getPathForLocation(PLATFORM_CLASS_PATH);
 422             Path userClassPath = getPathForLocation(CLASS_PATH);
 423             classSearchPath = new Path();
 424             classSearchPath.addAll(bootClassPath);
 425             classSearchPath.addAll(userClassPath);
 426         }
 427         return Collections.unmodifiableCollection(classSearchPath);
 428     }
 429 
 430     /** The actual effective locations for non-source, non-class files */
 431     private Path otherSearchPath;
 432 
 433     Collection<File> otherSearchPath() {
 434         if (otherSearchPath == null) {
 435             lazy();
 436             Path userClassPath = getPathForLocation(CLASS_PATH);
 437             Path sourcePath = getPathForLocation(SOURCE_PATH);
 438             if (sourcePath == null)
 439                 otherSearchPath = userClassPath;
 440             else {
 441                 otherSearchPath = new Path();
 442                 otherSearchPath.addAll(userClassPath);
 443                 otherSearchPath.addAll(sourcePath);
 444             }
 445         }
 446         return Collections.unmodifiableCollection(otherSearchPath);
 447     }
 448 
 449     /** Is this the name of an archive file? */
 450     private boolean isArchive(File file) {
 451         String n = file.getName().toLowerCase();
 452         return fsInfo.isFile(file)
 453             && (n.endsWith(".jar") || n.endsWith(".zip"));
 454     }
 455 
 456     /**
 457      * Utility method for converting a search path string to an array
 458      * of directory and JAR file URLs.
 459      *
 460      * Note that this method is called by apt and the DocletInvoker.
 461      *
 462      * @param path the search path string
 463      * @return the resulting array of directory and JAR file URLs
 464      */
 465     public static URL[] pathToURLs(String path) {
 466         StringTokenizer st = new StringTokenizer(path, File.pathSeparator);
 467         URL[] urls = new URL[st.countTokens()];
 468         int count = 0;
 469         while (st.hasMoreTokens()) {
 470             URL url = fileToURL(new File(st.nextToken()));
 471             if (url != null) {
 472                 urls[count++] = url;
 473             }
 474         }
 475         if (urls.length != count) {
 476             URL[] tmp = new URL[count];
 477             System.arraycopy(urls, 0, tmp, 0, count);
 478             urls = tmp;
 479         }
 480         return urls;
 481     }
 482 
 483     /**
 484      * Returns the directory or JAR file URL corresponding to the specified
 485      * local file name.
 486      *
 487      * @param file the File object
 488      * @return the resulting directory or JAR file URL, or null if unknown
 489      */
 490     private static URL fileToURL(File file) {
 491         String name;
 492         try {
 493             name = file.getCanonicalPath();
 494         } catch (IOException e) {
 495             name = file.getAbsolutePath();
 496         }
 497         name = name.replace(File.separatorChar, '/');
 498         if (!name.startsWith("/")) {
 499             name = "/" + name;
 500         }
 501         // If the file does not exist, then assume that it's a directory
 502         if (!file.isFile()) {
 503             name = name + "/";
 504         }
 505         try {
 506             return new URL("file", "", name);
 507         } catch (MalformedURLException e) {
 508             throw new IllegalArgumentException(file.toString());
 509         }
 510     }
 511 }