/*
* Copyright 2014-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.facebook.buck.jvm.java.runner;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/**
* A standalone class that's designed to read its classpath from a file given using the standard `@`
* syntax used by javac. <b>WARNING: </b> this class modifies the system classloader. Don't use this
* in-process. This also modifies the System property {@code java.class.path}.
*
* <p>We rely on the fact that you can pass total garbage into the classpath, and it'll be set just
* fine. Because of this, usage is like so:
*
* <pre>java -classpath @path/to/classpath-file com.example.MainClass arg1 arg2 arg3</pre>
*
* <p>The format of the file used for adding new entries to the classpath is simply one entry per
* line. Each entry is checked to see if it resolves to a valid path on the local file system. If it
* does then a URL is constructed from that entry and added to the system classloader.
*
* <p>It is possible to declare more than one @classpathfile, and ordering and duplicates will be
* honoured.
*
* <p>Note: this class only depends on classes present in the JRE, since we don't want to have to
* push more things on to the classpath when using it.
*/
public class FileClassPathRunner {
private FileClassPathRunner() {
// Do not instantiate.
}
public static void main(String[] args) throws IOException, ReflectiveOperationException {
// We must have the name of the class to delegate to added.
if (args.length < 1) {
System.exit(-1);
}
ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
if (!(sysLoader instanceof URLClassLoader)) {
System.exit(-2);
}
URLClassLoader urlClassLoader = (URLClassLoader) sysLoader;
modifyClassLoader(urlClassLoader, true);
// Now read the main class from the args and invoke it
Method main = getMainClass(args);
String[] mainArgs = constructArgs(args);
main.invoke(null, new Object[] {mainArgs});
}
static void modifyClassLoader(
URLClassLoader urlClassLoader, boolean modifySystemClasspathProperty)
throws IOException, ReflectiveOperationException {
List<Path> paths = getClasspathFiles(urlClassLoader.getURLs());
List<URL> readUrls = readUrls(paths, modifySystemClasspathProperty);
addUrlsToClassLoader(urlClassLoader, readUrls);
}
// @VisibileForTesting
@SuppressWarnings("PMD.EmptyCatchBlock")
static List<Path> getClasspathFiles(URL[] urls) {
List<Path> paths = new LinkedList<>();
for (URL url : urls) {
if (!"file".equals(url.getProtocol())) {
continue;
}
// Segment the path, looking for a section that starts with "@". If one is found, reconstruct
// the rest of the path.
// WARNING: While the classfile's path can contain directories that include "@", the path
// cannot contain a directory that starts with "@".
String path = url.getPath();
int found, splitIndex = -1;
if (path == null || path.length() == 0) {
continue;
} else if (path.charAt(0) == '@') {
// path starts with '@'
splitIndex = 1;
} else if ((found = path.indexOf("/@")) >= 0) {
// found first section that begins with '@'
splitIndex = found + 2;
}
if (splitIndex > 0 && splitIndex < path.length()) {
try {
paths.add(Paths.get(path.substring(splitIndex)));
} catch (InvalidPathException e) {
// Carry on regardless
}
}
}
return paths;
}
// @VisibleForTesting
@SuppressWarnings("PMD.EmptyCatchBlock")
static List<URL> readUrls(List<Path> paths, boolean modifySystemClassPathProperty)
throws IOException {
List<URL> readUrls = new LinkedList<>();
List<String> classPathEntries = new LinkedList<>();
// Check to see if each of the provided paths is a file that's readable. If it's readable,
// pull it into memory, and scan it for entries to add to the classpath, using the standard
// format of "file/" to indicate a directory and assuming all other files are jars.
for (Path path : paths) {
if (!Files.exists(path)) {
continue;
}
List<String> lines = Files.readAllLines(path, UTF_8);
for (String line : lines) {
if (line.isEmpty()) {
continue;
}
try {
Path entry = Paths.get(line);
readUrls.add(entry.toUri().toURL());
classPathEntries.add(entry.toString());
} catch (InvalidPathException e) {
// Carry on regardless --- java allows us to put absolute garbage into the classpath.
}
}
}
String classpathProperty = System.getProperty("java.class.path");
if (classpathProperty == null) {
throw new NullPointerException("java.class.path system property must not be null");
}
StringBuilder newClassPath = new StringBuilder();
constructNewClassPath(newClassPath, classpathProperty, classPathEntries);
if (modifySystemClassPathProperty) {
System.setProperty("java.class.path", newClassPath.toString());
}
return readUrls;
}
// @VisibleForTesting
static void constructNewClassPath(
StringBuilder newClassPath,
/* @Nullable */ String existingClassPath,
List<String> classPathEntries) {
if (existingClassPath != null) {
newClassPath.append(existingClassPath);
}
Iterator<String> iterator = classPathEntries.iterator();
if (iterator.hasNext()) {
newClassPath.append(existingClassPath == null ? "" : File.pathSeparatorChar);
newClassPath.append(iterator.next());
}
while (iterator.hasNext()) {
newClassPath.append(File.pathSeparatorChar).append(iterator.next());
}
}
private static void addUrlsToClassLoader(ClassLoader sysLoader, List<URL> readUrls)
throws ReflectiveOperationException {
// Add all the URLs to the classloader
Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
addURL.setAccessible(true);
for (URL readUrl : readUrls) {
addURL.invoke(sysLoader, readUrl);
}
}
private static String[] constructArgs(String[] args) {
String[] mainArgs;
if (args.length == 1) {
mainArgs = new String[0];
} else {
mainArgs = new String[args.length - 1];
System.arraycopy(args, 1, mainArgs, 0, mainArgs.length);
}
return mainArgs;
}
private static Method getMainClass(String[] args) throws ReflectiveOperationException {
Class<?> mainClazz = Class.forName(args[0]);
Method main = mainClazz.getMethod("main", args.getClass());
int modifiers = main.getModifiers();
if (!Modifier.isStatic(modifiers)) {
System.exit(-4);
}
return main;
}
}