/*
* Capsule
* Copyright (c) 2014-2016, Parallel Universe Software Co. and Contributors. All rights reserved.
*
* This program and the accompanying materials are licensed under the terms
* of the Eclipse Public License v1.0, available at
* http://www.eclipse.org/legal/epl-v10.html
*/
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.lang.instrument.Instrumentation;
import java.lang.management.ManagementFactory;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.charset.Charset;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.Permission;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.Properties;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.RandomAccess;
import java.util.concurrent.atomic.AtomicReference;
import javax.management.MBeanServer;
import javax.management.MBeanServerConnection;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import static java.util.Collections.*;
import static java.util.Arrays.asList;
/**
* An application capsule.
* <p>
* This API is to be used by caplets (custom capsules) to programmatically (rather than declaratively) configure the capsule and possibly provide custom behavior.
*
* <h2>Writing Caplets</h2>
*
* All non-final protected methods may be overridden by caplets. These methods will usually be called once, but they must be idempotent,
* i.e. if called numerous times they must always return the same value, and produce the same effect as if called once.
* <br>
* Overridden methods need not be thread-safe, and are guaranteed to be called by a single thread at a time.
* <br>
* Overridable (non-final) methods <b>must never</b> be called directly by caplet code, except by their overrides.
* <p>
* Final methods implement various utility or accessors, which may be freely used by caplets.
* <p>
* Caplets might consider overriding some of the following powerful methods:
* {@link #attribute(Map.Entry) attribute}, {@link #getVarValue(String) getVarValue},
* {@link #prelaunch(List, List) prelaunch}.
* <p>
* For command line option handling, see {@link #OPTION(String, String, String, String) OPTION}.<br/>
* Attributes should be registered with {@link #ATTRIBUTE(String, Object, Object, boolean, String) ATTRIBUTE}.
*
* <h3>Handling Files</h3>
*
* Capsule uses a two-staged <i>file resolution mechanism</i>.
*
* When a manifest property contains a reference to anything that might resolve to a file (or multiple files) -- e.g. the name of a Maven artifact --
* the string containing the reference is passed to the {@link #lookup(java.lang.String, java.lang.String, java.util.Map.Entry, java.lang.Object) lookup} method,
* which returns an opaque handle.
*
* When the application launch command line is rendered (or any configuration file that is interpreted by an outside process), the file handles
* are passed to the {@link #resolve(java.lang.Object) resolve} method, which turns the handle into a list of {@code Path}s.
*
* The operation of {@code lookup} and {@code resolve} can be customized by a caplet, but that process is intentionally left undocumented
* until that process is finalized.
*
* @author pron
*/
public class Capsule implements Runnable, InvocationHandler {
public static final String VERSION = "1.0.4";
/*
* This class follows some STRICT RULES:
*
* 1. IT MUST COMPILE TO A SINGLE CLASS FILE (so it must not contain nested or inner classes).
* 2. IT MUST ONLY REFERENCE CLASSES IN THE JDK.
* 3. ALL METHODS MUST BE PURE OR, AT LEAST, IDEMPOTENT (with the exception of the launch method, and the constructor).
*
* Rules #1 and #2 ensure that fat capsules will work with only Capsule.class included in the JAR. Rule #2 helps enforcing rules #1 and #3.
* Rule #3 ensures methods can be called in any order (after construction completes), and makes maintenance and evolution of Capsule simpler.
* This class contains several strange hacks to comply with rule #1.
*
* Also, the code is not meant to be the most efficient, but methods should be as independent and stateless as possible.
* Other than those few methods called in the constructor, all others are can be called in any order, and don't rely on any state.
*
* We do a lot of data transformations that could benefit from Java 8's lambdas+streams, but we want Capsule to support Java 7.
*
* The JavaDoc could really benefit from https://bugs.openjdk.java.net/browse/JDK-4085608 to categorize methods into
* Caplet overrides properties, and utility categories.
*
*
* Caplet Hierarchy (or chain)
* ---------------------------
*
* Capsule subclasses, i.e. caplets, may be arranged in a dynamic "inheritance" hierarchy, where each caplet modifies, or "subclasses"
* the previous ones in the chain.
* The first caplet in the chain (the highest in the hierarchy) is referenced by the 'oc' field, the last is referenced by 'cc', and
* the previous caplet, the "superclass" is referenced by 'sup':
*
* ____ ____ ____ ____
* | | sup | | sup | | sup | |
* | OC | <----- | | <----- | | <----- | CC |
* |____| |____| |____| |____|
*
* A wrapping capsule is inserted into the chain following the wrapped capsule.
*/
//<editor-fold defaultstate="collapsed" desc="Constants">
/////////// Constants ///////////////////////////////////
private static final long START = System.nanoTime();
private static final Map<String, Object[]> OPTIONS = new LinkedHashMap<>(20);
private static final Map<String, Object[]> ATTRIBS = new LinkedHashMap<>(60);
private static Properties PROPERTIES = new Properties(System.getProperties());
// standard values
private static final String PROP_JAVA_VERSION = "java.version";
private static final String PROP_JAVA_HOME = "java.home";
private static final String PROP_OS_NAME = "os.name";
private static final String PROP_USER_HOME = "user.home";
private static final String PROP_JAVA_LIBRARY_PATH = "java.library.path";
private static final String PROP_FILE_SEPARATOR = "file.separator";
private static final String PROP_PATH_SEPARATOR = "path.separator";
private static final String PROP_JAVA_SECURITY_POLICY = "java.security.policy";
private static final String PROP_JAVA_SECURITY_MANAGER = "java.security.manager";
private static final String PROP_TMP_DIR = "java.io.tmpdir";
private static final String ATTR_MANIFEST_VERSION = "Manifest-Version";
private static final String ATTR_PREMAIN_CLASS = "Premain-Class";
private static final String ATTR_MAIN_CLASS = "Main-Class";
private static final String ATTR_CLASS_PATH = "Class-Path";
private static final String ATTR_IMPLEMENTATION_VERSION = "Implementation-Version";
private static final String ATTR_IMPLEMENTATION_TITLE = "Implementation-Title";
private static final String ATTR_IMPLEMENTATION_VENDOR = "Implementation-Vendor";
private static final String ATTR_IMPLEMENTATION_URL = "Implementation-URL";
private static final String FILE_SEPARATOR = System.getProperty(PROP_FILE_SEPARATOR);
private static final char FILE_SEPARATOR_CHAR = FILE_SEPARATOR.charAt(0);
private static final String PATH_SEPARATOR = System.getProperty(PROP_PATH_SEPARATOR);
private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
// Operating systems
private static final String OS_WINDOWS = "windows";
private static final String OS_MACOS = "macos";
private static final String OS_LINUX = "linux";
private static final String OS_SOLARIS = "solaris";
private static final String OS_BSD = "bsd";
private static final String OS_AIX = "aix";
private static final String OS_HP_UX = "hp-ux";
private static final String OS_UNIX = "unix";
private static final String OS_POSIX = "posix";
private static final String OS_VMS = "vms";
private static final String OS = getProperty(PROP_OS_NAME).toLowerCase();
private static final Set<String> PLATFORMS = immutableSet(OS_WINDOWS, OS_MACOS, OS_LINUX, OS_SOLARIS, OS_BSD, OS_AIX, OS_POSIX, OS_UNIX, OS_POSIX, OS_VMS);
private static final String PLATFORM = getOS();
private static final String ENV_CACHE_DIR = "CAPSULE_CACHE_DIR";
private static final String ENV_CACHE_NAME = "CAPSULE_CACHE_NAME";
// misc
private static final String CAPSULE_PROP_PREFIX = "capsule.";
private static final String CACHE_DEFAULT_NAME = "capsule";
private static final String APP_CACHE_NAME = "apps";
private static final String LOCK_FILE_NAME = ".lock";
private static final String TIMESTAMP_FILE_NAME = ".extracted";
private static final String CACHE_NONE = "NONE";
private static final String SEPARATOR_DOT = "\\.";
private static final Path WINDOWS_PROGRAM_FILES_1 = Paths.get("C:", "Program Files");
private static final Path WINDOWS_PROGRAM_FILES_2 = Paths.get("C:", "Program Files (x86)");
private static final int WINDOWS_MAX_CMD = 32500; // actually 32768 - http://blogs.msdn.com/b/oldnewthing/archive/2003/12/10/56028.aspx
private static final ClassLoader MY_CLASSLOADER = Capsule.class.getClassLoader();
private static final Permission PERM_UNSAFE_OVERRIDE = new RuntimePermission("unsafeOverride");
private static final int SOCKET_TIMEOUT = 30000; // Keep high enough for container-based capsules
// Lifecycle
private static final int STAGE_NONE = 0;
private static final int STAGE_LAUNCH = 1;
private static final int STAGE_LIFTOFF = 2;
// options
private static final int OPTION_DEFAULT = 0;
private static final int OPTION_METHOD = 1;
private static final int OPTION_WRAPPER_ONLY = 2;
private static final int OPTION_DESC = 3;
// attributes
private static final int ATTRIB_TYPE = 0;
private static final int ATTRIB_DEFAULT = 1;
private static final int ATTRIB_MODAL = 2;
private static final int ATTRIB_DESC = 3;
// messages
private static final int MESSAGE_EXIT = 1;
private static final int MESSAGE_START_JMX = 2;
private static final int MESSAGE_JMX_URL = 3;
// properties
private static final String PROP_VERSION = OPTION("capsule.version", "false", "printVersion", "Prints the capsule and application versions.");
private static final String PROP_MODES = OPTION("capsule.modes", "false", "printModes", "Prints all available capsule modes.");
private static final String PROP_PRINT_JRES = OPTION("capsule.jvms", "false", "printJVMs", "Prints a list of all JVM installations found.");
private static final String PROP_MERGE = OPTION("capsule.merge", null, "mergeCapsules", true, "Merges a wrapper capsule with a wrapped capsule.");
private static final String PROP_HELP = OPTION("capsule.help", "false", "printHelp", "Prints this help message.");
private static final String PROP_INTROSPECT = OPTION("capsule.introspect", "false", "introspect", "Prints the values of all attributes.");
private static final String PROP_MODE = OPTION("capsule.mode", null, null, "Picks the capsule mode to run.");
private static final String PROP_RESET = OPTION("capsule.reset", "false", null, "Resets the capsule cache before launching. The capsule to be re-extracted (if applicable), and other possibly cached files will be recreated.");
private static final String PROP_LOG_LEVEL = OPTION("capsule.log", "quiet", null, "Picks a log level. Must be one of none, quiet, verbose, or debug.");
private static final String PROP_CAPSULE_JAVA_HOME = OPTION("capsule.java.home", null, null, "Sets the location of the Java home (JVM installation directory) to use; If \'current\' forces the use of the JVM that launched the capsule.");
private static final String PROP_CAPSULE_JAVA_CMD = OPTION("capsule.java.cmd", null, null, "Sets the path to the Java executable to use.");
private static final String PROP_JVM_ARGS = OPTION("capsule.jvm.args", null, null, "Sets additional JVM arguments to use when running the application.");
private static final String PROP_PORT = "capsule.port";
private static final String PROP_ADDRESS = "capsule.address";
private static final String PROP_TRAMPOLINE = "capsule.trampoline";
private static final String PROP_PROFILE = "capsule.profile";
/*
* Map.Entry<String, T> was chosen to represent an attribute because of rules 1 and 2.
*/
/**
* The application's name. E.g. {@code "The Best Word Processor"}
*/
protected static final Entry<String, String> ATTR_APP_NAME = ATTRIBUTE("Application-Name", T_STRING(), null, false, "The application's name");
/**
* The application's unique ID. E.g. {@code "com.acme.bestwordprocessor"}
*/
protected static final Entry<String, String> ATTR_APP_ID = ATTRIBUTE("Application-Id", T_STRING(), null, false, "The application's name");
protected static final Entry<String, String> ATTR_APP_VERSION = ATTRIBUTE("Application-Version", T_STRING(), null, false, "The application's version string");
protected static final Entry<String, List<String>> ATTR_CAPLETS = ATTRIBUTE("Caplets", T_LIST(T_STRING()), null, false, "A list of names of caplet class names or Maven coordinates of caplet artifacts that will be applied to the capsule in the order they are listed");
private static final Entry<String, String> ATTR_LOG_LEVEL = ATTRIBUTE("Capsule-Log-Level", T_STRING(), null, false, "The capsule's default log level");
private static final Entry<String, String> ATTR_MODE_DESC = ATTRIBUTE("Description", T_STRING(), null, true, "Contains the description of its respective mode");
protected static final Entry<String, String> ATTR_APP_CLASS = ATTRIBUTE("Application-Class", T_STRING(), null, true, "The main application class");
protected static final Entry<String, String> ATTR_APP_ARTIFACT = ATTRIBUTE("Application", T_STRING(), null, true, "The Maven coordinates of the application's main JAR or the path of the main JAR within the capsule");
protected static final Entry<String, Object> ATTR_SCRIPT = ATTRIBUTE("Application-Script", T_FILE(), null, true, "A startup script to be run *instead* of `Application-Class`, given as a path relative to the capsule's root");
protected static final Entry<String, String> ATTR_MIN_JAVA_VERSION = ATTRIBUTE("Min-Java-Version", T_STRING(), null, true, "The lowest Java version required to run the application");
protected static final Entry<String, String> ATTR_JAVA_VERSION = ATTRIBUTE("Java-Version", T_STRING(), null, true, "The highest version of the Java installation required to run the application");
protected static final Entry<String, Map<String, String>> ATTR_MIN_UPDATE_VERSION = ATTRIBUTE("Min-Update-Version", T_MAP(T_STRING(), T_STRING(), null), null, true, "A space-separated key-value ('=' separated) list mapping Java versions to the minimum update version required");
protected static final Entry<String, Boolean> ATTR_JDK_REQUIRED = ATTRIBUTE("JDK-Required", T_BOOL(), false, true, "Whether or not a JDK is required to launch the application");
protected static final Entry<String, Boolean> ATTR_AGENT = ATTRIBUTE("Capsule-Agent", T_BOOL(), false, true, "Whether this capsule should inject itself as an agent into the application.");
private static final Entry<String, List<String>> ATTR_ARGS = ATTRIBUTE("Args", T_LIST(T_STRING()), null, true,
"A list of command line arguments to be passed to the application; the UNIX shell-style special variables (`$*`, `$1`, `$2`, ...) can refer to the actual arguments passed on the capsule's command line; if no special var is used, the listed values will be prepended to the supplied arguments (i.e., as if `$*` had been listed last).");
private static final Entry<String, Map<String, String>> ATTR_ENV = ATTRIBUTE("Environment-Variables", T_MAP(T_STRING(), T_STRING(), null), null, true, "A list of environment variables that will be put in the applications environment; formatted \"var=value\" or \"var\"");
protected static final Entry<String, List<String>> ATTR_JVM_ARGS = ATTRIBUTE("JVM-Args", T_LIST(T_STRING()), null, true, "A list of JVM arguments that will be used to launch the application's Java process");
protected static final Entry<String, Map<String, String>> ATTR_SYSTEM_PROPERTIES = ATTRIBUTE("System-Properties", T_MAP(T_STRING(), T_STRING(), ""), null, true, "A list of system properties that will be defined in the applications JVM; formatted \"prop=value\" or \"prop\"");
protected static final Entry<String, List<Object>> ATTR_APP_CLASS_PATH = ATTRIBUTE("App-Class-Path", T_LIST(T_FILE()), null, true, "A list of JARs, relative to the capsule root, that will be put on the application's classpath, in the order they are listed");
protected static final Entry<String, Boolean> ATTR_CAPSULE_IN_CLASS_PATH = ATTRIBUTE("Capsule-In-Class-Path", T_BOOL(), true, true, "Whether or not the capsule JAR itself is on the application's classpath");
protected static final Entry<String, List<Object>> ATTR_BOOT_CLASS_PATH = ATTRIBUTE("Boot-Class-Path", T_LIST(T_FILE()), null, true, "A list of JARs, dependencies, and/or directories, relative to the capsule root, that will be used as the application's boot classpath");
protected static final Entry<String, List<Object>> ATTR_BOOT_CLASS_PATH_A = ATTRIBUTE("Boot-Class-Path-A", T_LIST(T_FILE()), null, true, "A list of JARs dependencies, and/or directories, relative to the capsule root, that will be appended to the applications default boot classpath");
protected static final Entry<String, List<Object>> ATTR_BOOT_CLASS_PATH_P = ATTRIBUTE("Boot-Class-Path-P", T_LIST(T_FILE()), null, true, "A list of JARs dependencies, and/or directories, relative to the capsule root, that will be prepended to the applications default boot classpath");
protected static final Entry<String, List<Object>> ATTR_LIBRARY_PATH_A = ATTRIBUTE("Library-Path-A", T_LIST(T_FILE()), null, true, "A list of JARs and/or directories, relative to the capsule root, to be appended to the default native library path");
protected static final Entry<String, List<Object>> ATTR_LIBRARY_PATH_P = ATTRIBUTE("Library-Path-P", T_LIST(T_FILE()), null, true, "a list of JARs and/or directories, relative to the capsule root, to be prepended to the default native library path");
protected static final Entry<String, String> ATTR_SECURITY_MANAGER = ATTRIBUTE("Security-Manager", T_STRING(), null, true, "The name of a class that will serve as the application's security-manager");
protected static final Entry<String, String> ATTR_SECURITY_POLICY = ATTRIBUTE("Security-Policy", T_STRING(), null, true, "A security policy file, relative to the capsule root, that will be used as the security policy");
protected static final Entry<String, String> ATTR_SECURITY_POLICY_A = ATTRIBUTE("Security-Policy-A", T_STRING(), null, true, "A security policy file, relative to the capsule root, that will be appended to the default security policy");
protected static final Entry<String, Map<Object, String>> ATTR_JAVA_AGENTS = ATTRIBUTE("Java-Agents", T_MAP(T_FILE(), T_STRING(), ""), null, true, "A list of Java agents used by the application; formatted \"agent\" or \"agent=arg1,arg2...\", where agent is either the path to a JAR relative to the capsule root, or a Maven coordinate of a dependency");
protected static final Entry<String, Map<Object, String>> ATTR_NATIVE_AGENTS = ATTRIBUTE("Native-Agents", T_MAP(T_FILE(getNativeLibExtension()), T_STRING(), ""), null, true,
"A list of native JVMTI agents used by the application; formatted \"agent\" or \"agent=arg1,arg2...\", where agent is either the path to a native library, without the platform-specific suffix, relative to the capsule root. The native library file(s) can be embedded in the capsule or listed as Maven native dependencies using the Native-Dependencies-... attributes.");
protected static final Entry<String, List<Object>> ATTR_DEPENDENCIES = ATTRIBUTE("Dependencies", T_LIST(T_FILE()), null, true, "A list of Maven dependencies given as groupId:artifactId:version[(excludeGroupId:excludeArtifactId,...)]");
protected static final Entry<String, Map<Object, String>> ATTR_NATIVE_DEPENDENCIES = ATTRIBUTE("Native-Dependencies", T_MAP(T_FILE(getNativeLibExtension()), T_STRING(), ""), null, true, "A list of Maven dependencies consisting of native library artifacts; each item can be a comma separated pair, with the second component being a new name to give the download artifact");
// outgoing
private static final String VAR_CAPSULE_APP = "CAPSULE_APP";
private static final String VAR_CAPSULE_DIR = "CAPSULE_DIR";
private static final String VAR_CAPSULE_JAR = "CAPSULE_JAR";
private static final String VAR_CLASSPATH = "CLASSPATH";
private static final String VAR_JAVA_HOME = "JAVA_HOME";
private static final String PROP_CAPSULE_JAR = "capsule.jar";
private static final String PROP_CAPSULE_DIR = "capsule.dir";
private static final String PROP_CAPSULE_APP = "capsule.app";
private static final String PROP_CAPSULE_APP_PID = "capsule.app.pid";
// logging
private static final String LOG_PREFIX = "CAPSULE: ";
private static final String LOG_AGENT_PREFIX = "CAPSULE AGENT: ";
protected static final int LOG_NONE = 0;
protected static final int LOG_QUIET = 1;
protected static final int LOG_VERBOSE = 2;
protected static final int LOG_DEBUG = 3;
private static final int PROFILE = emptyOrTrue(System.getProperty(PROP_PROFILE)) ? LOG_QUIET : LOG_DEBUG;
//</editor-fold>
//<editor-fold desc="Main">
/////////// Main ///////////////////////////////////
protected static final PrintStream STDOUT = System.out;
protected static final PrintStream STDERR = System.err;
private static volatile Integer LOG_LEVEL;
private static Path CACHE_DIR;
private static Capsule CAPSULE;
private static boolean AGENT;
final static Capsule myCapsule(List<String> args) {
if (CAPSULE == null) {
final ClassLoader ccl = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(MY_CLASSLOADER);
Capsule capsule = newCapsule(MY_CLASSLOADER, findOwnJarFile());
clearContext();
if ((AGENT || capsule.isEmptyCapsule()) && args != null && !args.isEmpty()) {
processCmdLineOptions(args, ManagementFactory.getRuntimeMXBean().getInputArguments());
if (capsule.isEmptyCapsule())
capsule = capsule.setTarget(args.remove(0));
} else {
processOptions();
}
CAPSULE = capsule.oc; // TODO: capsule or oc ???
} finally {
Thread.currentThread().setContextClassLoader(ccl);
}
}
return CAPSULE;
}
public static final void main(String[] args) {
System.exit(main0(args));
}
/**
* Alternative main entry point that is not using System.exit(), thus avoiding callers to be terminated
* in an unexpected way.
*
* @param args0 the command line arguments
* @return the exit code
*/
@SuppressWarnings({"BroadCatchBlock", "UnusedAssignment"})
public static int main0(String[] args0) {
List<String> args = new ArrayList<>(asList(args0)); // list must be mutable b/c myCapsule() might mutate it
Capsule capsule = null;
try {
capsule = myCapsule(args);
args = unmodifiableList(args);
if (isWrapperFactoryCapsule(capsule)) {
capsule = null; // help gc
return runOtherCapsule(args);
}
if (runActions(capsule, args))
return 0;
return capsule.launch(args);
} catch (Throwable t) {
if (capsule != null) {
capsule.cleanup1();
capsule.onError(t);
} else
printError(t, capsule);
return 1;
}
}
public static void premain(String agentArgs, Instrumentation inst) {
AGENT = true;
PROPERTIES = new Properties(System.getProperties());
Capsule capsule = null;
try {
processOptions();
capsule = myCapsule(agentArgs != null ? new ArrayList<>(split(agentArgs, "\\s+")) : null);
for (Capsule c = capsule.cc; c != null; c = c.sup)
c.agent(inst);
} catch (Throwable t) {
if (capsule != null) {
capsule.cleanup1();
capsule.onError(t);
} else
printError(LOG_QUIET, t);
}
}
/**
* @deprecated marked deprecated to exclude from javadoc
*/
public static Object getCapsule(String capletClassName) {
return CAPSULE.cc.sup(capletClassName);
}
//<editor-fold defaultstate="collapsed" desc="Error Reporting">
/////////// Error Reporting ///////////////////////////////////
private static void printError(int level, Throwable t) {
if (!isLogging(level))
return;
STDERR.print((AGENT ? "CAPSULE AGENT" : "CAPSULE") + " EXCEPTION: " + t.getMessage());
if (hasContext() && (t.getMessage() == null || t.getMessage().length() < 50))
STDERR.print(" while processing " + getContext());
if (getLogLevel(getProperty0(PROP_LOG_LEVEL)) >= LOG_VERBOSE) {
STDERR.println();
deshadow(t).printStackTrace(STDERR);
} else
STDERR.println(" (for stack trace, run with -D" + PROP_LOG_LEVEL + "=verbose)");
}
private static void printError(Throwable t, Capsule capsule) {
printError(LOG_QUIET, t);
if (!AGENT && t instanceof IllegalArgumentException)
printUsage(capsule);
}
private static void printUsage(Capsule capsule) {
printHelp(capsule != null ? capsule.isWrapperCapsule() : true);
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Run Other Capsule">
/////////// Run Other Capsule ///////////////////////////////////
private static boolean isWrapperFactoryCapsule(Capsule capsule) {
return capsule.isFactoryCapsule() && capsule.isWrapperCapsule() && capsule.getJarFile() != null;
}
private static int runOtherCapsule(List<String> args) {
final Path jar = CAPSULE.getJarFile();
CAPSULE = null; // help gc
return runMain(jar, args);
}
private static int runMain(Path jar, List<String> args) {
final String mainClass;
try {
mainClass = getMainClass(jar);
if (mainClass == null)
throw new IllegalArgumentException("JAR file " + jar + " is not an executable (does not have a main class)");
} catch (RuntimeException e) {
throw new IllegalArgumentException(jar + " does not exist or does appear to be a valid JAR", e);
}
try {
final Method main = newClassLoader0(null, jar).loadClass(mainClass).getMethod("main", String[].class);
try {
main.invoke(null, (Object) args.toArray(new String[0]));
return 0;
} catch (Exception e) {
deshadow(e).printStackTrace(STDERR);
return 1;
}
} catch (ReflectiveOperationException e) {
throw rethrow(e);
}
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Command Line">
/////////// Command Line ///////////////////////////////////
/**
* Registers a capsule command-line option. Must be called during the caplet's static initialization.
* <p>
* Capsule options are system properties beginning with the prefix "capsule.", normally passed to the capsule as -D flags on the command line.
* <p>
* Options can be top-level *actions* (like print dependency tree or list JVMs), in which case the {@code methodName} argument must
* be the name of a method used to launch the action instead of launching the capsule.
* <p>
* Options can have a default value, which will be automatically assigned to the system property if undefined. The default values
* {@code "true"} and {@code "false"} are treated specially. If one of them is the assigned default value, and the system property
* is defined with with a value of the empty string, then it will be re-assigned the value {@code "true"}.
* <p>
* <b>Simple Command Line Options for Wrapper Capsules</b><br>
* When the capsule serves as a wrapper (i.e. it's an empty capsule used to launch an executable artifact or another capsule)
* then the options can also be passed to the capsule as simple command line options (arguments starting with a hyphen),
* with the "capsule." prefix removed, and every '.' character replaced with a '-'.
* <p>
* These command line arguments will automatically be converted to system properties, which will take their value from the argument
* following the option (i.e. {@code -option value}), <i>unless</i> the option is given one of the special default values
* {@code "true"} or {@code "false"}, in which case it is treated as a flag with no arguments (note that an option with the default
* value {@code "true"} will therefore not be able to be turned off if simple options are used).
*
* @param defaultValue the option's default value ({@code "true"} and {@code "false"} are specially treated; see above).
* @param optionName the name of the system property for the option; must begin with {@code "capsule."}.
* @param methodName if non-null, then the option is a top-level action (like print dependency tree or list JVMs),
* and this is the method which will run the action.
* The method must accept a single {@code args} parameter of type {@code List<String>}.
* @param wrapperOnly whether or not the option is available in wrapper capsules only
* @param description a description of the option.
* @return the option's name
*/
protected static final String OPTION(String optionName, String defaultValue, String methodName, boolean wrapperOnly, String description) {
if (!optionName.startsWith(CAPSULE_PROP_PREFIX))
throw new IllegalArgumentException("Option name must start with " + CAPSULE_PROP_PREFIX + " but was " + optionName);
final Object[] conf = new Object[]{defaultValue, methodName, wrapperOnly, description};
final Object[] old = OPTIONS.get(optionName);
if (old != null) {
if (asList(conf).subList(0, conf.length - 1).equals(asList(old).subList(0, conf.length - 1))) // don't compare description
throw new IllegalStateException("Option " + optionName + " has a conflicting registration: " + Arrays.toString(old));
}
OPTIONS.put(optionName, conf);
return optionName;
}
/**
* Same as {@link #OPTION(String, String, String, boolean, String) OPTION(optionName, defaultValue, methodName, wrapperOnly, description)}.
*/
protected static final String OPTION(String optionName, String defaultValue, String methodName, String description) {
return OPTION(optionName, defaultValue, methodName, false, description);
}
private static boolean optionTakesArguments(String propertyName) {
final String defaultValue = (String) OPTIONS.get(propertyName)[OPTION_DEFAULT];
return !("false".equals(defaultValue) || "true".equals(defaultValue));
}
private static void processOptions() {
for (Map.Entry<String, Object[]> entry : OPTIONS.entrySet()) {
final String option = entry.getKey();
final String defval = (String) entry.getValue()[OPTION_DEFAULT];
if (getProperty0(option) == null && defval != null && !defval.equals("false")) // the last condition is for backwards compatibility
setProperty(option, defval);
else if (!optionTakesArguments(option) && "".equals(getProperty0(option)))
setProperty(option, "true");
}
}
private static void processCmdLineOptions(List<String> args, List<String> jvmArgs) {
while (!args.isEmpty()) {
if (!first(args).startsWith("-"))
break;
final String arg = args.remove(0);
String optarg = null;
if (arg.contains("="))
optarg = getAfter(arg, '=');
final String option = simpleToOption(getBefore(arg, '='));
if (option == null)
throw new IllegalArgumentException("Unrecognized option: " + arg);
// -D wins over simple flags
boolean overridden = false;
for (String x : jvmArgs) {
if (x.equals("-D" + option) || x.startsWith("-D" + option + "=")) {
overridden = true;
break;
}
}
if (optarg == null)
optarg = optionTakesArguments(option) ? args.remove(0) : "";
if (!overridden)
setProperty(option, optarg);
}
processOptions();
}
// visible for testing
@SuppressWarnings("unchecked")
static final boolean runActions(Capsule capsule, List<String> args) {
verifyAgent(false);
try {
boolean found = false;
for (Map.Entry<String, Object[]> entry : OPTIONS.entrySet()) {
if (entry.getValue()[OPTION_METHOD] != null && systemPropertyEmptyOrNotFalse(entry.getKey())) {
if (!capsule.isWrapperCapsule() && (Boolean) entry.getValue()[OPTION_WRAPPER_ONLY])
throw new IllegalStateException("Action " + entry.getKey() + " is availbale for wrapper capsules only.");
final Method m = getMethod(capsule, (String) entry.getValue()[OPTION_METHOD], List.class);
m.invoke(capsule.cc.sup((Class<? extends Capsule>) m.getDeclaringClass()), args);
found = true;
}
}
if (found)
capsule.cleanup1();
return found;
} catch (InvocationTargetException e) {
throw rethrow(e);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
}
private static String optionToSimple(String option) {
if (PROP_HELP.equals(option))
return "-h";
if (PROP_INTROSPECT.equals(option))
return "-i";
if (PROP_VERSION.equals(option))
return "-v";
return "-" + camelCaseToDashed(option.substring(CAPSULE_PROP_PREFIX.length())).replace('.', '-');
}
private static String simpleToOption(String simple) {
switch (simple) {
case "-h": return PROP_HELP;
case "-i": return PROP_INTROSPECT;
case "-v": return PROP_VERSION;
}
for (String option : OPTIONS.keySet()) {
if (simple.equals(optionToSimple(option)))
return option;
}
return null;
}
private static String camelCaseToDashed(String camel) {
return camel.replaceAll("([A-Z][a-z]+)", "-$1").toLowerCase();
}
private static boolean isCapsuleOption(String propertyName) {
return propertyName.startsWith(CAPSULE_PROP_PREFIX); // OPTIONS.containsKey(propertyName);
}
//</editor-fold>
//</editor-fold>
private static Map<String, List<Path>> JAVA_HOMES; // an optimization trick (can be injected by CapsuleLauncher)
private final Map<String, String> threads = new HashMap<>();
// fields marked /*final*/ are effectively final after finalizeCapsule
private /*final*/ Capsule oc; // first in chain
private /*final*/ Capsule cc; // last in chain
private /*final*/ Capsule sup; // previous in chain
private /*final*/ Capsule _ct; // a temp var
private final boolean wrapper;
private final Manifest manifest; // never null
private /*final*/ Path jarFile; // never null
private /*final*/ String appId; // null iff wrapper capsule wrapping a non-capsule JAR
private /*final*/ String mode;
private /*final*/ String nonCapsuleTarget;
private Path javaHome;
private String javaVersion;
private Path cacheDir;
private Path appDir;
private Path writableAppCache;
private boolean plainCache;
private boolean cacheUpToDate;
private FileLock appCacheLock;
private int lifecycleStage;
private String appArtifactMainClass;
private List<String> jvmArgs_;
private List<String> args_;
private List<Path> tmpFiles = new ArrayList<>();
private Process child;
private boolean agentCalled;
private MBeanServer origMBeanServer;
private MBeanServerConnection jmxConnection;
// Error reporting
private static final ThreadLocal<String> contextType_ = new ThreadLocal<>();
private static final ThreadLocal<String> contextKey_ = new ThreadLocal<>();
private static final ThreadLocal<String> contextValue_ = new ThreadLocal<>();
//
private Object socket;
private InetAddress address;
private int port;
private ObjectInput socketInput;
private ObjectOutput socketOutput;
//<editor-fold defaultstate="collapsed" desc="Constructors">
/////////// Constructors ///////////////////////////////////
/*
* The constructors and methods in this section may be reflectively called by CapsuleLauncher
*/
/**
* Constructs a capsule.
* <p>
* This constructor is used by a caplet that will be listed in the manifest's {@code Main-Class} attribute.
* <b>Caplets are encouraged to "override" the {@link #Capsule(Capsule) other constructor} so that they may be listed
* in the {@code Caplets} attribute.</b>
* <p>
* This constructor or that of a subclass must not make use of any registered capsule options,
* as they may not have been properly pre-processed yet.
*
* @param jarFile the path to the JAR file
*/
@SuppressWarnings({"OverridableMethodCallInConstructor", "LeakingThisInConstructor"})
protected Capsule(Path jarFile) {
clearContext();
Objects.requireNonNull(jarFile, "jarFile can't be null");
this.oc = this;
this.cc = this;
this.sup = null;
this.jarFile = toAbsolutePath(jarFile);
final long start = System.nanoTime(); // can't use clock before log level is set
try (JarInputStream jis = openJarInputStream(jarFile)) {
this.manifest = jis.getManifest();
if (manifest == null)
throw new RuntimeException("Capsule " + jarFile + " does not have a manifest");
} catch (IOException e) {
throw new RuntimeException("Could not read JAR file " + jarFile, e);
}
setLogLevel(chooseLogLevel()); // temporary
log(LOG_VERBOSE, "Jar: " + jarFile);
log(LOG_VERBOSE, "Platform: " + PLATFORM);
this.wrapper = isEmptyCapsule(); // must be done before loadCaplets, to init their wrapper field, but this implies the application must be specified in the manifest
loadCaplets();
setLogLevel(chooseLogLevel()); // temporary
time("Load class " + this.getClass().getName(), START, start);
time("Read JAR in constructor", start);
if (!wrapper)
finalizeCapsule();
else if (isFactoryCapsule())
this.jarFile = null; // an empty factory capsule is marked this way.
clearContext();
}
/**
* Caplets that will be listed on the manifest's {@code Caplets} attribute must use this constructor.
* Caplets are required to have a constructor with the same signature as this constructor, and pass their arguments to up to this constructor.
*
* @param pred The capsule preceding this one in the chain (caplets must not access the passed capsule in their constructor).
*/
@SuppressWarnings("LeakingThisInConstructor")
protected Capsule(Capsule pred) {
this.oc = pred.oc;
this.cc = this;
time("Load class " + this.getClass().getName(), START);
clearContext();
// insertAfter(pred);
// copy final dields
this.wrapper = pred.wrapper;
this.manifest = pred.manifest;
this.jarFile = pred.jarFile;
}
final Capsule setTarget(String target) {
verifyCanCallSetTarget();
final Path jar = isDependency(target) ? firstOrNull(resolve(lookup(target, ATTR_APP_ARTIFACT))) : toAbsolutePath(path(target));
if (jar == null)
throw new RuntimeException(target + " not found.");
if (jar.equals(getJarFile())) // catch simple loops
throw new RuntimeException("Capsule wrapping loop detected with capsule " + getJarFile());
if (isFactoryCapsule()) {
this.jarFile = jar;
return this;
}
final Manifest man;
boolean isCapsule = false;
final long start = clock();
try (JarInputStream jis = openJarInputStream(jar)) {
man = jis.getManifest();
if (man == null || man.getMainAttributes().getValue(ATTR_MAIN_CLASS) == null)
throw new IllegalArgumentException(jar + " is not a capsule or an executable JAR");
for (JarEntry entry; (entry = jis.getNextJarEntry()) != null;) {
if (entry.getName().equals(Capsule.class.getName() + ".class")) {
isCapsule = true;
break;
}
}
} catch (IOException e) {
throw new RuntimeException("Could not read JAR file " + jar, e);
}
time("Read JAR in setTarget", start);
if (!isCapsule)
oc.nonCapsuleTarget = target;
else {
log(LOG_VERBOSE, "Wrapping capsule " + jar);
insertAfter(loadTargetCapsule(cc.getClass().getClassLoader(), jar).cc);
}
finalizeCapsule();
return this;
}
/**
* Called once the capsule construction has been completed (after loading of wrapped capsule, if applicable).
*/
protected void finalizeCapsule() {
if ((_ct = getCallTarget(Capsule.class)) != null)
_ct.finalizeCapsule();
else
finalizeCapsule0();
clearContext();
}
private void finalizeCapsule0() {
validateManifest(oc.manifest);
setLogLevel(chooseLogLevel());
oc.mode = chooseMode1();
initAppId();
if (getAppId() == null && !(hasAttribute(ATTR_APP_ARTIFACT) && !isDependency(getAttribute(ATTR_APP_ARTIFACT))))
throw new IllegalArgumentException("Could not determine app ID. Capsule jar " + getJarFile() + " should have the " + ATTR_APP_NAME + " manifest attribute.");
}
private void verifyCanCallSetTarget() {
if (getAppId() != null)
throw new IllegalStateException("Capsule is finalized");
if (!isEmptyCapsule())
throw new IllegalStateException("Capsule " + getJarFile() + " isn't empty");
}
private void loadCaplets() {
for (String caplet : getAttribute(ATTR_CAPLETS))
loadCaplet(caplet, cc).insertAfter(cc);
}
private void initAppId() {
if (oc.appId != null)
return;
log(LOG_VERBOSE, "Initializing app ID");
final String name = getAppIdNoVer();
if (name == null)
return;
final String version = getAttribute(ATTR_APP_VERSION);
oc.appId = name + (version != null ? "_" + version : "");
log(LOG_VERBOSE, "Initialized app ID: " + oc.appId);
}
protected final boolean isEmptyCapsule() {
return !hasAttribute(ATTR_APP_ARTIFACT) && !hasAttribute(ATTR_APP_CLASS) && !hasAttribute(ATTR_SCRIPT);
}
private void setStage(int stage) {
oc.lifecycleStage = stage;
}
private void verifyAtStage(int stage) {
if (oc.lifecycleStage != stage)
throw new IllegalStateException("This operation is not available at this stage in the capsule's lifecycle.");
}
protected final void verifyAfterStage(int stage) {
if (oc.lifecycleStage <= stage)
throw new IllegalStateException("This operation is not available at this stage in the capsule's lifecycle.");
}
// Called by tests
private static void clearCaches() {
jarEntriesCache.clear();
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Caplet Chain">
/////////// Caplet Chain ///////////////////////////////////
private Capsule loadCaplet(String caplet, Capsule pred) {
log(LOG_VERBOSE, "Loading caplet: " + caplet);
if (isDependency(caplet) || caplet.endsWith(".jar")) {
final List<Path> jars = resolve(lookup(caplet, ATTR_CAPLETS));
if (jars.size() != 1)
throw new RuntimeException("The caplet " + caplet + " has transitive dependencies.");
return newCapsule(first(jars), pred);
} else
return newCapsule(caplet, pred);
}
private void insertAfter(Capsule pred) {
// private b/c this might be a security risk (wrapped capsule inserting a caplet after wrapper)
// and also because it might be too powerful and prevent us from adopting a different caplet chain implementation
log(LOG_VERBOSE, "Applying caplet " + this.getClass().getName());
if (sup == pred)
return;
if (pred != null) {
if (sup != null)
throw new IllegalStateException("Caplet " + this + " is already in the chain (after " + sup + ")");
if (!wrapper && pred.hasCaplet(this.getClass().getName())) {
log(LOG_VERBOSE, "Caplet " + this.getClass().getName() + " has already been applied.");
return;
}
this.sup = pred;
this.oc = sup.oc;
for (Capsule c = cc; c != this; c = c.sup)
c.oc = oc;
if (sup.cc == sup) { // I'm last
for (Capsule c = sup; c != null; c = c.sup)
c.cc = cc;
} else { // I'm in the middle
throw new IllegalArgumentException("Caplet cannot be inserted in the middle of the hierarchy");
// for (Capsule c = sup.cc; c != sup; c = c.sup) {
// if (c.sup == sup)
// c.sup = cc;
// }
// for (Capsule c = cc; c != this; c = c.sup)
// c.cc = sup.cc;
// this.cc = sup.cc;
}
}
}
/**
* Checks whether a caplet with the given class name is installed.
*/
protected final boolean hasCaplet(String name) {
for (Capsule c = cc; c != null; c = c.sup) {
for (Class<?> cls = c.getClass(); cls != null; cls = cls.getSuperclass()) {
if (name.equals(cls.getName()))
return true;
}
}
return false;
}
/**
* The first caplet in the caplet chain starting with the current one and going up (back) that is of the requested type.
*/
protected final <T extends Capsule> T sup(Class<T> caplet) {
for (Capsule c = this; c != null; c = c.sup) {
if (caplet.isInstance(c))
return caplet.cast(c);
}
return null;
}
/**
* The first caplet in the caplet chain starting with the current one and going up (back) that is of the requested type.
*/
protected final Capsule sup(String ClassName) {
for (Capsule c = this; c != null; c = c.sup) {
for (Class cls = c.getClass(); cls != null; cls = cls.getSuperclass()) {
if (ClassName.equals(cls.getName()))
return c;
}
}
return null;
}
// called by utils' Capsule
final List<Class<? extends Capsule>> getCaplets() {
final List<Class<? extends Capsule>> caplets = new ArrayList<>();
for (Capsule c = cc; c != null; c = c.sup)
caplets.add(c.getClass());
Collections.reverse(caplets);
return caplets;
}
protected final <T extends Capsule> T getCallTarget(Class<T> clazz) {
/*
* Here we're implementing both the "invokevirtual" and "invokespecial".
* We want to somehow differentiate the case where the function is called directly -- and should, like invokevirtual, target cc, the
* last caplet in the hieracrchy -- from the case where the function is called with super.foo -- and should, like invokevirtual,
* target sup, the previous caplet in the hierarchy.
*/
Capsule target = null;
if ((sup == null || sup.sup(clazz) == null || this.jarFile != ((Capsule) sup.sup(clazz)).jarFile) && cc != this) { // the jarFile condition tests if this is the first caplet in a wrapper capsule
final StackTraceElement[] st = new Throwable().getStackTrace();
if (st == null || st.length < 3)
throw new AssertionError("No debug information in Capsule class");
final int c1 = 1;
if (!st[c1].getClassName().equals(clazz.getName()))
throw new RuntimeException("Illegal access. Method can only be called by the " + clazz.getName() + " class");
int c2 = 2;
while (isStream(st[c2].getClassName()))
c2++;
if (st[c1].getLineNumber() <= 0 || st[c2].getLineNumber() <= 0)
throw new AssertionError("No debug information in Capsule class");
// we return CC if the caller is also Capsule but not the same method (which would mean this is a sup.foo() call)
if (!st[c2].getMethodName().equals(st[c1].getMethodName())
|| (st[c2].getClassName().equals(clazz.getName()) && Math.abs(st[c2].getLineNumber() - st[c1].getLineNumber()) > 3))
target = cc;
}
if (target == null)
target = sup;
return target != null ? target.sup(clazz) : null;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Properties">
/////////// Properties ///////////////////////////////////
private boolean isWrapperOfNonCapsule() {
return nonCapsuleTarget != null;
}
private boolean isFactoryCapsule() {
if (!getClass().equals(Capsule.class) || !wrapper)
return false;
for (Object attr : manifest.getMainAttributes().keySet()) {
if (ATTRIBS.containsKey(attr.toString()))
return false;
}
for (Attributes atts : manifest.getEntries().values()) {
for (Object attr : atts.keySet()) {
if (ATTRIBS.containsKey(attr.toString()))
return false;
}
}
log(LOG_DEBUG, "Factory (unchanged) capsule");
return true;
}
/**
* Whether or not this is an empty capsule
*/
protected final boolean isWrapperCapsule() {
for (Capsule c = cc; c != null; c = c.sup) {
if (c.wrapper)
return true;
}
return false;
}
/**
* This capsule's current mode.
*/
protected final String getMode() {
return oc.mode;
}
/**
* The OS platform.
*/
protected final String getPlatform() {
return PLATFORM;
}
/**
* This capsule's JAR file.
*/
protected final Path getJarFile() {
return oc.jarFile;
}
/**
* Returns the app's ID.
*/
protected final String getAppId() {
return oc.appId;
}
/**
* Is this the Capsule instance running as an agent in the application?
*/
public boolean isAgent() {
return AGENT;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Capsule JAR">
/////////// Capsule JAR ///////////////////////////////////
private static Path findJarFile(Class<? extends Capsule> capsuleClass) {
assert capsuleClass != null;
final URL url = MY_CLASSLOADER.getResource(capsuleClass.getName().replace('.', '/') + ".class");
if (url == null) // Could happen with embedded capsules
throw new IllegalStateException("The " + capsuleClass + " class must be in a JAR file, but was not found");
if (!"jar".equals(url.getProtocol()))
throw new IllegalStateException("The " + capsuleClass + " class must be in a JAR file, but was loaded from: " + url);
final String path = url.getPath();
if (path == null) // || !path.startsWith("file:")
throw new IllegalStateException("The " + capsuleClass + " class must be in a local JAR file, but was loaded from: " + url);
try {
final URI jarUri = new URI(path.substring(0, path.indexOf('!')));
return Paths.get(jarUri);
} catch (URISyntaxException e) {
throw new AssertionError(e);
}
}
private static Path findOwnJarFile() {
return findJarFile(Capsule.class);
}
private String toJarUrl(String relPath) {
return "jar:file:" + getJarFile().toAbsolutePath() + "!/" + relPath;
}
private static boolean isExecutable(Path path) {
if (!Files.isExecutable(path))
return false;
try (Reader reader = new InputStreamReader(Files.newInputStream(path), "UTF-8")) {
int c = reader.read();
if (c < 0 || (char) c != '#')
return false;
c = reader.read();
if (c < 0 || (char) c != '!')
return false;
return true;
} catch (IOException e) {
throw rethrow(e);
}
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Main Operations">
/////////// Main Operations ///////////////////////////////////
void printVersion(List<String> args) {
if (getAppId() != null) {
STDOUT.println(LOG_PREFIX + "Application " + getAppId());
if (hasAttribute(ATTR_APP_NAME))
STDOUT.println(LOG_PREFIX + getAttribute(ATTR_APP_NAME));
if (hasAttribute(ATTR_APP_VERSION))
STDOUT.println(LOG_PREFIX + "Version: " + getAttribute(ATTR_APP_VERSION));
for (String attr : asList(ATTR_IMPLEMENTATION_VENDOR, ATTR_IMPLEMENTATION_URL)) {
if (getManifestAttribute(attr) != null)
STDOUT.println(LOG_PREFIX + getManifestAttribute(attr));
}
}
STDOUT.println(LOG_PREFIX + "Capsule version " + VERSION);
}
void printModes(List<String> args) {
verifyNonEmpty("Cannot print modes of a wrapper capsule.");
STDOUT.println(LOG_PREFIX + "Application " + getAppId());
STDOUT.println("Available modes:");
final Set<String> modes = getModes();
if (modes.isEmpty())
STDOUT.println("Default mode only");
else {
for (String m : modes) {
final String desc = getModeDescription(m);
STDOUT.println("* " + m + (desc != null ? ": " + desc : ""));
}
}
}
void printJVMs(List<String> args) {
final Map<String, List<Path>> jres = getJavaHomes();
if (jres == null)
println("No detected Java installations");
else {
STDOUT.println(LOG_PREFIX + "Detected Java installations:");
for (Map.Entry<String, List<Path>> j : jres.entrySet()) {
for (Path home : j.getValue())
STDOUT.println(j.getKey() + (isJDK(home) ? " (JDK)" : "") + (j.getKey().length() < 8 ? "\t\t" : "\t") + home);
}
}
final Path jhome = getJavaHome();
STDOUT.println(LOG_PREFIX + "selected " + (jhome != null ? jhome : (getProperty(PROP_JAVA_HOME) + " (current)")));
}
void mergeCapsules(List<String> args) {
if (!isWrapperCapsule())
throw new IllegalStateException("This is not a wrapper capsule");
try {
final Path outCapsule = path(getProperty(PROP_MERGE));
final Path wr = cc.jarFile;
final Path wd = oc.jarFile;
log(LOG_QUIET, "Merging " + wr + (!Objects.deepEquals(wr, wd) ? " + " + wd : "") + " -> " + outCapsule);
mergeCapsule(wr, wd, outCapsule);
} catch (Exception e) {
throw new RuntimeException("Capsule merge failed.", e);
}
}
void printHelp(List<String> args) {
printHelp(wrapper);
}
private static void printHelp(boolean simple) {
// USAGE:
final Path myJar = toFriendlyPath(findOwnJarFile());
final boolean executable = isExecutable(myJar);
final StringBuilder usage = new StringBuilder();
if (!executable)
usage.append("java ");
if (simple) {
if (!executable)
usage.append("-jar ");
usage.append(myJar).append(' ');
}
usage.append("<options> ");
if (!simple && !executable)
usage.append("-jar ");
if (simple)
usage.append("<path or Maven coords of application JAR/capsule>");
else
usage.append(myJar);
STDERR.println("USAGE: " + usage);
// ACTIONS AND OPTIONS:
for (boolean actions : new boolean[]{true, false}) {
STDERR.println("\n" + (actions ? "Actions:" : "Options:"));
for (Map.Entry<String, Object[]> entry : OPTIONS.entrySet()) {
if (entry.getValue()[OPTION_DESC] != null && (entry.getValue()[OPTION_METHOD] != null) == actions) {
if (!simple && (Boolean) entry.getValue()[OPTION_WRAPPER_ONLY])
continue;
final String option = entry.getKey();
final String defaultValue = (String) entry.getValue()[OPTION_DEFAULT];
if (simple && !optionTakesArguments(option) && defaultValue.equals("true"))
continue;
StringBuilder sb = new StringBuilder();
sb.append(simple ? optionToSimple(option) : option);
if (optionTakesArguments(option) || defaultValue.equals("true")) {
sb.append(simple ? ' ' : '=').append("<value>");
if (defaultValue != null)
sb.append(" (default: ").append(defaultValue).append(")");
}
sb.append(" - ").append(entry.getValue()[OPTION_DESC]);
STDERR.println(" " + sb);
}
}
}
// ATTRIBUTES:
if (1 == 2) {
STDERR.println("\nManifest Attributes:");
for (Map.Entry<String, Object[]> entry : ATTRIBS.entrySet()) {
if (entry.getValue()[ATTRIB_DESC] != null) {
final String attrib = entry.getKey();
final String defaultValue = toString(entry.getValue()[ATTRIB_DEFAULT]);
StringBuilder sb = new StringBuilder();
sb.append(attrib);
if (defaultValue != null)
sb.append(" (default: ").append(defaultValue).append(")");
sb.append(" - ").append(entry.getValue()[ATTRIB_DESC]);
STDERR.println(" " + sb);
}
}
}
}
void introspect(List<String> args) {
STDERR.println("Capsule " + VERSION);
final List<Class<? extends Capsule>> caplets = getCaplets();
STDERR.println("Caplets: ");
if (caplets.isEmpty())
STDERR.println(" NONE");
else {
for (Class<?> caplet : caplets) {
StringBuilder sb = new StringBuilder();
sb.append(caplet.getName());
String version;
sb.append((version = getClassVersion(caplet)) != null ? " " + version : "");
STDERR.println(" " + sb);
}
}
STDERR.println();
STDERR.println("Attributes");
for (Map.Entry<String, Object[]> entry : ATTRIBS.entrySet()) {
final String attrib = entry.getKey();
StringBuilder sb = new StringBuilder();
sb.append(attrib);
if (entry.getValue()[ATTRIB_DESC] != null)
sb.append(" (").append(entry.getValue()[ATTRIB_DESC]).append(')');
sb.append(": ");
sb.append("\n\t");
final Entry<String, ?> attr = attr(attrib);
final Object value = getAttributeNoLookup(attr);
sb.append(value);
final Object defaultValue = getAttributeDefaultValue(attr);
if (Objects.equals(value, defaultValue))
sb.append(" (default)");
STDERR.println(" " + sb);
}
}
@SuppressWarnings("empty-statement")
private int launch(List<String> args) throws IOException, InterruptedException {
verifyAgent(false);
setStage(STAGE_LAUNCH);
verifyNonEmpty("Cannot launch a wrapper capsule.");
final ProcessBuilder pb;
final List<String> jvmArgs = ManagementFactory.getRuntimeMXBean().getInputArguments();
pb = prepareForLaunch(jvmArgs, args);
if (pb == null) { // can be null if prelaunch has been overridden by a subclass
log(LOG_VERBOSE, "Nothing to run");
return 0;
}
clearContext();
time("Total", START);
log(LOG_VERBOSE, join(pb.command(), " ") + (pb.directory() != null ? " (Running in " + pb.directory() + ")" : ""));
clearContext();
return launch(pb);
}
private void verifyNonEmpty(String message) {
if (isEmptyCapsule())
throw new IllegalArgumentException(message);
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Threads">
/////////// Threads ///////////////////////////////////
private Thread startThread(String name, String method) {
if (threads.containsKey(name))
throw new IllegalStateException("A thread by the name " + name + " has already been registered.");
threads.put(name, method);
final Thread t = new Thread(this, name);
t.setDaemon(true);
t.start();
return t;
}
/**
* @deprecated marked deprecated to exclude from javadoc
*/
@Override
public final void run() {
final String threadName = Thread.currentThread().getName();
try {
final String methodName = threads.get(threadName);
if (methodName != null) {
try {
getMethod(Capsule.class, methodName).invoke(this);
return;
} catch (ReflectiveOperationException e) {
throw rethrow(e);
}
}
// shutdown hook
cleanup1();
} catch (Throwable e) {
log(LOG_QUIET, "Exception on thread " + threadName + ": " + e.getMessage());
printError(LOG_QUIET, e);
throw rethrow(e);
}
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Launch">
/////////// Launch ///////////////////////////////////
// directly used by CapsuleLauncher
final ProcessBuilder prepareForLaunch(List<String> jvmArgs, List<String> args) {
verifyAgent(false);
oc.jvmArgs_ = nullToEmpty(jvmArgs); // hack
oc.args_ = nullToEmpty(jvmArgs); // hack
log(LOG_VERBOSE, "Launching app " + getAppId() + (getMode() != null ? " in mode " + getMode() : ""));
final long start = clock();
lookupAllDependencies();
final ProcessBuilder pb = prelaunch(nullToEmpty(jvmArgs), nullToEmpty(args));
time("prepareForLaunch", start);
return pb;
}
/**
* Launches the process defined by the given {@link ProcessBuilder}.
*
* @param pb the process builder
* @return the process's exit value
*/
protected int launch(ProcessBuilder pb) throws IOException, InterruptedException {
return (_ct = unsafe(getCallTarget(Capsule.class))) != null ? _ct.launch(pb) : launch0(pb);
}
private int launch0(ProcessBuilder pb) throws IOException, InterruptedException {
if (isTrampoline())
STDOUT.println(trampolineString(pb));
else {
Runtime.getRuntime().addShutdownHook(new Thread(this, "cleanup"));
overridePlatformMBeanServer();
if (!isInheritIoBug())
pb.inheritIO();
oc.child = pb.start();
oc.child = postlaunch(oc.child);
if (oc.child == null)
return 0;
setStage(STAGE_LIFTOFF);
final int pid = getPid(oc.child);
if (pid > 0)
System.setProperty(PROP_CAPSULE_APP_PID, Integer.toString(pid));
if (isInheritIoBug())
pipeIoStreams();
if (oc.socket != null)
startServer();
liftoff();
// receiveLoop();
oc.child.waitFor();
}
return oc.child != null ? oc.child.exitValue() : 0;
}
private String trampolineString(ProcessBuilder pb) {
if (hasAttribute(ATTR_ENV))
throw new RuntimeException("Capsule cannot trampoline because manifest defines the " + ATTR_ENV + " attribute.");
final List<String> cmdline = new ArrayList<>(pb.command());
cmdline.remove("-D" + PROP_TRAMPOLINE);
for (int i = 0; i < cmdline.size(); i++)
cmdline.set(i, "\"" + cmdline.get(i) + "\"");
return join(cmdline, " ");
}
private void cleanup1() {
log(LOG_VERBOSE, "Cleanup");
log(LOG_DEBUG, new Exception("Stack trace"));
cleanup();
}
/**
* Called when the capsule exits after a successful or failed attempt to launch the application.
* If you override this method, you must make sure to call {@code super.cleanup()} even in the event of an abnormal termination
* (i.e. when an exception is thrown). This method must not throw any exceptions. All exceptions origination by {@code cleanup}
* must be wither ignored completely or printed to STDERR.
*/
protected void cleanup() {
if ((_ct = getCallTarget(Capsule.class)) != null)
_ct.cleanup();
else
cleanup0();
}
private void cleanup0() {
try {
if (oc.child != null) {
killChild();
oc.child.waitFor();
}
oc.child = null;
} catch (Exception t) {
deshadow(t).printStackTrace(STDERR);
}
for (Path p : oc.tmpFiles) {
try {
delete(p);
} catch (Exception t) {
log(LOG_VERBOSE, t.getMessage());
}
}
oc.tmpFiles.clear();
}
private void killChild() {
if (isWindows()) {
if (!send(MESSAGE_EXIT, 1))
oc.child.destroy();
} else
oc.child.destroy();
}
private boolean isChildAlive() {
final Process c = oc.child;
return c != null && isAlive(c);
}
protected final Path addTempFile(Path p) {
log(LOG_VERBOSE, "Creating temp file/dir " + p);
oc.tmpFiles.add(p);
return p;
}
private String chooseMode1() {
String m = chooseMode();
if (m != null && !hasMode(m))
throw new IllegalArgumentException("Capsule " + getJarFile() + " does not have mode " + m);
return m;
}
/**
* Chooses this capsule's mode.
* The mode is chosen during the preparations for launch (not at construction time).
*/
protected String chooseMode() {
return (_ct = getCallTarget(Capsule.class)) != null ? _ct.chooseMode() : chooseMode0();
}
private String chooseMode0() {
return emptyToNull(getProperty(PROP_MODE));
}
/**
* Returns a configured {@link ProcessBuilder} that is later used to launch the capsule.
* The ProcessBuilder's IO redirection is left in its default settings.
* Caplets may override this method to display a message prior to launch, or to configure the process's IO streams.
* For more elaborate manipulation of the Capsule's launched process, consider overriding {@link #buildProcess() buildProcess}.
*
* @param jvmArgs the JVM arguments listed on the command line
* @param args the application command-line arguments
* @return a configured {@code ProcessBuilder} (if {@code null}, the launch will be aborted).
*/
protected ProcessBuilder prelaunch(List<String> jvmArgs, List<String> args) {
return (_ct = unsafe(getCallTarget(Capsule.class))) != null ? _ct.prelaunch(jvmArgs, args) : prelaunch0(jvmArgs, args);
}
private ProcessBuilder prelaunch0(List<String> jvmArgs, List<String> args) {
try {
if (!isTrampoline() && getAttribute(ATTR_AGENT))
prepareServer();
final ProcessBuilder pb = buildProcess();
buildEnvironmentVariables(pb);
pb.command().addAll(buildArgs(args));
cleanupCache(null);
return pb;
} catch (Throwable t) {
cleanupCache(t);
throw t;
}
}
/**
* Constructs a {@link ProcessBuilder} that is later used to launch the capsule.
* The returned process builder should contain the command <i>minus</i> the application arguments (which are later constructed by
* {@link #buildArgs(List) buildArgs} and appended to the command).<br>
* While environment variables may be set at this stage, the environment is later configured by
* {@link #buildEnvironmentVariables(Map) buildEnvironmentVariables}.
* <p>
* This implementation tries to create a process running a startup script, and, if one has not been set, constructs a Java process.
* <p>
* This method should be overridden to add new types of processes the capsule can launch (like, say, Python scripts).
* If all you want is to configure the returned {@link ProcessBuilder}, for example to set IO stream redirection,
* you should override {@link #prelaunch(List, List) prelaunch}.
*
* @return a {@code ProcessBuilder} (must never be {@code null}).
*/
protected ProcessBuilder buildProcess() {
return (_ct = unsafe(getCallTarget(Capsule.class))) != null ? _ct.buildProcess() : buildProcess0();
}
private ProcessBuilder buildProcess0() {
if (oc.jvmArgs_ == null)
throw new IllegalStateException("Capsule has not been prepared for launch!");
final ProcessBuilder pb = new ProcessBuilder();
if (!buildScriptProcess(pb))
buildJavaProcess(pb, oc.jvmArgs_);
return pb;
}
/**
* Returns a list of command line arguments to pass to the application.
*
* @param args The command line arguments passed to the capsule at launch
*/
protected List<String> buildArgs(List<String> args) {
return (_ct = getCallTarget(Capsule.class)) != null ? _ct.buildArgs(args) : buildArgs0(args);
}
private List<String> buildArgs0(List<String> args) {
return expandArgs(getAttribute(ATTR_ARGS), args);
}
// visible for testing
static List<String> expandArgs(List<String> args0, List<String> args) {
final List<String> args1 = new ArrayList<String>();
boolean expanded = false;
for (String a : args0) {
if (a.startsWith("$")) {
if (a.equals("$*")) {
args1.addAll(args);
expanded = true;
continue;
} else {
try {
final int i = Integer.parseInt(a.substring(1));
args1.add(args.get(i - 1));
expanded = true;
continue;
} catch (NumberFormatException e) {
}
}
}
args1.add(a);
}
if (!expanded)
args1.addAll(args);
return args1;
}
private void buildEnvironmentVariables(ProcessBuilder pb) {
Map<String, String> env = new HashMap<>(pb.environment());
env = buildEnvironmentVariables(env);
pb.environment().clear();
pb.environment().putAll(env);
}
/**
* Returns a map of environment variables (property-value pairs).
*
* @param env the current environment
*/
protected Map<String, String> buildEnvironmentVariables(Map<String, String> env) {
return (_ct = getCallTarget(Capsule.class)) != null ? _ct.buildEnvironmentVariables(env) : buildEnvironmentVariables0(env);
}
private Map<String, String> buildEnvironmentVariables0(Map<String, String> env) {
final Map<String, String> jarEnv = getAttribute(ATTR_ENV);
for (Map.Entry<String, String> e : jarEnv.entrySet()) {
boolean overwrite = false;
String var = e.getKey();
if (var.endsWith(":")) {
overwrite = true;
var = var.substring(0, var.length() - 1);
}
if (overwrite || !env.containsKey(var))
env.put(var, e.getValue() != null ? e.getValue() : "");
}
if (getAppId() != null) {
if (getAppDir() != null)
env.put(VAR_CAPSULE_DIR, processOutgoingPath(getAppDir()));
env.put(VAR_CAPSULE_JAR, processOutgoingPath(getJarFile()));
env.put(VAR_CAPSULE_APP, getAppId());
}
return env;
}
private static boolean isTrampoline() {
return systemPropertyEmptyOrTrue(PROP_TRAMPOLINE);
}
/**
* Called after the application has been launched by the capsule.
* If this method returns a process, capsule will publish its pid (by setting a system property that may be queried by jcmd), await
* its termination, and exit, returning its exit value. If this method returns {@code null}, the capsule will exit immediately,
* without waiting for the child process to terminate. This method is also allowed to never return.
*
* @param child the child process running the application
*/
protected Process postlaunch(Process child) {
return ((_ct = getCallTarget(Capsule.class)) != null) ? _ct.postlaunch(child) : postlaunch0(child);
}
private Process postlaunch0(Process child) {
return child;
}
/**
* Called after the application has been launched by the capsule and {@link #postlaunch(Process) postlauch} has returned a non-null process.
*/
protected void liftoff() {
if ((_ct = getCallTarget(Capsule.class)) != null)
_ct.liftoff();
else
liftoff0();
}
private void liftoff0() {
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Agent">
/////////// Agent ///////////////////////////////////
private static void verifyAgent(boolean isAgent) {
if (AGENT != isAgent)
throw new IllegalStateException("This operation is only available when " + (isAgent ? "agent" : "non-agent"));
}
/**
* Called on a capsule agent instance in the application process.
*/
protected void agent(Instrumentation inst) {
if (oc.agentCalled)
return;
oc.agentCalled = true;
if (getProperty(PROP_ADDRESS) != null || getProperty(PROP_PORT) != null)
startClient();
}
@SuppressWarnings("unchecked")
private <T> T agentAttributes(Entry<String, T> attr, T value) {
if (ATTR_AGENT == attr) // prevent infinite recursion
return value;
if (!getAttribute(ATTR_AGENT))
return value;
if (ATTR_JAVA_AGENTS == attr) {
// add the capsule as an agent
final Map<Object, String> agents = new LinkedHashMap<>(cast(ATTR_JAVA_AGENTS, value));
Path ownJar = null;
try {
ownJar = findOwnJarFile();
} catch (final IllegalStateException ignored) {
}
if (ownJar != null)
agents.put(processOutgoingPath(ownJar), isWrapperCapsule() ? processOutgoingPath(getJarFile()) : "");
return (T) agents;
}
if (ATTR_SYSTEM_PROPERTIES == attr) {
if (oc.address != null) {
final Map<String, String> props = new HashMap<>(cast(ATTR_SYSTEM_PROPERTIES, value));
props.put(PROP_ADDRESS, oc.address.getHostAddress());
props.put(PROP_PORT, Integer.toString(oc.port));
props.put(PROP_LOG_LEVEL, Integer.toString(getLogLevel()));
return (T) props;
}
}
return value;
}
private void prepareServer() {
try {
log(LOG_VERBOSE, "Starting capsule server.");
InetSocketAddress sa = getLocalAddress();
final ServerSocket server = new ServerSocket(sa.getPort(), 5, sa.getAddress());
sa = (InetSocketAddress) server.getLocalSocketAddress();
oc.address = sa.getAddress();
oc.port = sa.getPort();
oc.socket = server;
log(LOG_VERBOSE, "Binding capsule server at: " + oc.address.getHostAddress() + ":" + oc.port);
} catch (IOException e) {
throw rethrow(e);
}
}
private void startServer() throws IOException {
try (ServerSocket server = (ServerSocket) oc.socket) {
server.setSoTimeout(SOCKET_TIMEOUT);
log(LOG_VERBOSE, "Waiting for client to connect...");
final Socket s = server.accept();
openSocketStreams(s);
oc.socket = s;
log(LOG_VERBOSE, "Client connected");
} catch (IOException e) {
if (isChildAlive()) {
log(LOG_QUIET, "Client connection failed.");
printError(LOG_QUIET, e);
closeComm();
}
}
}
private void startClient() {
log(LOG_VERBOSE, "Starting capsule client: " + getProperty(PROP_ADDRESS) + ":" + getProperty(PROP_PORT));
if (getProperty(PROP_ADDRESS) == null || getProperty(PROP_PORT) == null)
throw new IllegalStateException("Comm channel not defined");
try {
oc.address = InetAddress.getByName(getProperty(PROP_ADDRESS));
oc.port = Integer.valueOf(getProperty(PROP_PORT));
final Socket s = new Socket();
s.connect(new InetSocketAddress(oc.address, oc.port), SOCKET_TIMEOUT);
openSocketStreams(s);
oc.socket = s;
log(LOG_VERBOSE, "Client connected,");
startThread("capsule-comm", "receiveLoop");
} catch (IOException e) {
log(LOG_VERBOSE, "Client connection failed.");
printError(LOG_VERBOSE, e);
closeComm();
}
}
private void openSocketStreams(Socket s) throws IOException {
synchronized (oc) {
try {
s.setSoTimeout(SOCKET_TIMEOUT);
oc.socketOutput = new ObjectOutputStream(s.getOutputStream());
oc.socketOutput.flush();
oc.socketInput = new ObjectInputStream(s.getInputStream());
s.setSoTimeout(0);
} catch (IOException e) {
if (e instanceof SocketTimeoutException)
log(LOG_VERBOSE, "Socket timed out");
close(s);
throw e;
}
}
}
private void closeComm() {
synchronized (oc) {
log(LOG_VERBOSE, "Closing comm");
if (oc.socket != null)
close(oc.socket);
oc.socket = null;
oc.address = null;
oc.port = 0;
oc.socketOutput = null;
oc.socketInput = null;
}
}
@SuppressWarnings("empty-statement")
private void receiveLoop() {
try {
while (receive())
;
} catch (IOException e) {
try {
final Process p = child;
if (p != null && !waitFor(p, 100))
printError(LOG_QUIET, e);
} catch (InterruptedException ex) {
}
}
}
private boolean send(int message, Object payload) {
if (!AGENT)
verifyAfterStage(STAGE_LAUNCH);
if (oc.socketOutput == null)
return false;
try {
send0(message, payload);
return true;
} catch (IOException e) {
log(LOG_VERBOSE, "Sending of message " + message + ": " + payload + " failed - " + e.getMessage());
log(LOG_DEBUG, e);
return false;
}
}
private void send0(int message, Object payload) throws IOException {
synchronized (oc) {
if (oc.socketOutput == null)
throw new IOException("comm channel not defined");
log(LOG_VERBOSE, "Sending message " + message + " : " + payload);
oc.socketOutput.writeInt(message);
oc.socketOutput.writeObject(payload);
oc.socketOutput.flush();
}
}
private boolean receive() throws IOException {
synchronized (oc) {
if (!AGENT)
verifyAfterStage(STAGE_LAUNCH);
if (oc.socket == null)
return false;
try {
final int message = oc.socketInput.readInt();
final Object payload = oc.socketInput.readObject();
log(LOG_VERBOSE, "Message received " + message + " : " + payload);
receive(message, payload);
return true;
} catch (EOFException e) {
log(LOG_VERBOSE, "Received EOF");
log(LOG_VERBOSE, e);
return false;
} catch (ClassNotFoundException e) {
throw new IOException(e);
}
}
}
private void receive(int message, Object payload) {
switch (message) {
case MESSAGE_EXIT:
System.exit((Integer) payload);
break;
case MESSAGE_START_JMX:
if (AGENT) {
final JMXServiceURL jmxurl = startJMXServer();
if (jmxurl != null)
send(MESSAGE_JMX_URL, jmxurl);
}
break;
case MESSAGE_JMX_URL:
if (!AGENT)
connectToJMX((JMXServiceURL) payload);
break;
}
}
/**
* For internal use; subject to change/removal.
*
* @deprecated exclude from javadocs
*/
protected InetSocketAddress getLocalAddress() {
return (_ct = getCallTarget(Capsule.class)) != null ? _ct.getLocalAddress() : getLocalAddress0();
}
private InetSocketAddress getLocalAddress0() {
return new InetSocketAddress(0);
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="App ID">
/////////// App ID ///////////////////////////////////
private String getAppIdNoVer() {
String id = getAttribute(ATTR_APP_ID);
if (isEmpty(id))
id = getAttribute(ATTR_APP_NAME);
if (id == null) {
id = getAttribute(ATTR_APP_CLASS);
if (id != null && hasModalAttribute(ATTR_APP_CLASS))
throw new IllegalArgumentException("App ID-related attribute " + ATTR_APP_CLASS + " is defined in a modal section of the manifest. "
+ " In this case, you must add the " + ATTR_APP_ID + " attribute to the manifest's main section.");
}
return id;
}
static String getAppArtifactId(String coords) {
if (coords == null)
return null;
final String[] cs = coords.split(":");
return cs[0] + "." + cs[1];
}
static String getAppArtifactVersion(String coords) {
if (coords == null)
return null;
final String[] cs = coords.split(":");
if (cs.length < 3)
return null;
return cs[2];
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Capsule Cache">
/////////// Capsule Cache ///////////////////////////////////
/**
* For internal use; subject to change/removal.
*
* @deprecated exclude from javadocs
*/
protected final Path getCacheDir() {
if (oc.cacheDir == null) {
Path cache = CACHE_DIR;
if (cache != null) {
cache = initCacheDir(cache);
} else {
final String cacheDirEnv = System.getenv(ENV_CACHE_DIR);
if (cacheDirEnv != null) {
if (cacheDirEnv.equalsIgnoreCase(CACHE_NONE))
return null;
cache = initCacheDir(Paths.get(cacheDirEnv));
if (cache == null)
throw new RuntimeException("Could not initialize cache directory " + Paths.get(cacheDirEnv));
} else {
final String name = getCacheName();
cache = initCacheDir(getCacheHome().resolve(name));
if (cache == null) {
try {
cache = addTempFile(Files.createTempDirectory(getTempDir(), "capsule-"));
} catch (IOException e) {
log(LOG_VERBOSE, "Could not create directory: " + cache + " -- " + e.getMessage());
cache = null;
}
}
}
}
log(LOG_VERBOSE, "Cache directory: " + cache);
oc.cacheDir = cache;
}
return oc.cacheDir;
}
private static String getCacheName() {
final String cacheNameEnv = System.getenv(ENV_CACHE_NAME);
final String cacheName = cacheNameEnv != null ? cacheNameEnv : CACHE_DEFAULT_NAME;
return (isWindows() ? "" : ".") + cacheName;
}
private Path initCacheDir(Path cache) {
try {
if (!Files.exists(cache))
Files.createDirectories(cache, getPermissions(getExistingAncestor(cache)));
return cache;
} catch (IOException e) {
log(LOG_VERBOSE, "Could not create directory: " + cache + " -- " + e.getMessage());
return null;
}
}
private static Path getCacheHome() {
final Path cacheHome;
final Path userHome = Paths.get(getProperty(PROP_USER_HOME));
if (!isWindows())
cacheHome = userHome;
else {
Path localData;
final String localAppData = getenv("LOCALAPPDATA");
if (localAppData != null) {
localData = Paths.get(localAppData);
if (!Files.isDirectory(localData))
throw new RuntimeException("%LOCALAPPDATA% set to nonexistent directory " + localData);
} else {
localData = userHome.resolve(Paths.get("AppData", "Local"));
if (!Files.isDirectory(localData))
localData = userHome.resolve(Paths.get("Local Settings", "Application Data"));
if (!Files.isDirectory(localData))
throw new RuntimeException("%LOCALAPPDATA% is undefined, and neither "
+ userHome.resolve(Paths.get("AppData", "Local")) + " nor "
+ userHome.resolve(Paths.get("Local Settings", "Application Data")) + " have been found");
}
cacheHome = localData;
}
return cacheHome;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="App Cache">
/////////// App Cache ///////////////////////////////////
/**
* This capsule's cache directory, or {@code null} if capsule has been configured not to extract, or the app cache dir hasn't been set up yet.
*/
protected final Path getAppDir() {
return oc.appDir;
}
private Path getOrCreateAppDir() {
if (oc.appDir == null)
oc.appDir = buildAppCacheDir();
return oc.appDir;
}
/**
* Returns this capsule's cache directory.
* The difference between this method and {@link #getAppDir()} is that this method throws an exception if the app cache
* cannot be retrieved, while {@link #getAppDir()} returns {@code null}.
*
* @throws IllegalStateException if the app cache hasn't been set up (yet).
*/
protected final Path appDir() {
final Path dir = getOrCreateAppDir();
if (dir == null) {
String message = "Capsule not extracted.";
if (getAppId() == null) {
if (isEmptyCapsule())
message += " This is a wrapper capsule and the wrapped capsule hasn't been set (yet)";
else
message += " App ID has not been determined yet.";
}
throw new IllegalStateException(message);
}
return dir;
}
/**
* Returns a writable directory that can be used to store files related to launching the capsule.
*/
protected final Path getWritableAppCache() {
if (oc.writableAppCache == null) {
Path cache = getOrCreateAppDir();
if (cache == null || !Files.isWritable(cache)) {
try {
cache = addTempFile(Files.createTempDirectory(getTempDir(), "capsule-"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
oc.writableAppCache = cache;
}
return oc.writableAppCache;
}
private boolean hasWritableAppCache() {
return oc.writableAppCache != null && !oc.writableAppCache.equals(getAppDir());
}
/**
* Returns the path of the application cache (this is the directory where the capsule is extracted if necessary).
*/
protected Path buildAppCacheDir() {
return (_ct = unsafe(getCallTarget(Capsule.class))) != null ? _ct.buildAppCacheDir() : buildAppCacheDir0();
}
private Path buildAppCacheDir0() {
initAppId();
if (getAppId() == null)
return null;
oc.plainCache = true;
try {
final long start = clock();
final Path dir = toAbsolutePath(getCacheDir().resolve(APP_CACHE_NAME).resolve(getAppId()));
Files.createDirectories(dir, getPermissions(getExistingAncestor(dir)));
oc.cacheUpToDate = isAppCacheUpToDate1(dir);
if (!oc.cacheUpToDate) {
resetAppCache(dir);
extractCapsule(dir);
} else
log(LOG_VERBOSE, "App cache " + dir + " is up to date.");
time("buildAppCacheDir", start);
return dir;
} catch (IOException e) {
log(LOG_VERBOSE, "IOException while creating app cache: " + e.getMessage());
log(LOG_VERBOSE, e);
return null;
}
}
private void resetAppCache(Path dir) throws IOException {
try {
log(LOG_DEBUG, "(Re)Creating cache for " + getJarFile() + " in " + dir.toAbsolutePath());
final Path lockFile = dir.resolve(LOCK_FILE_NAME);
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) {
for (Path f : ds) {
if (!lockFile.equals(f))
delete(f);
}
}
} catch (IOException e) {
throw new IOException("Exception while clearing app cache directory " + dir.toAbsolutePath(), e);
}
}
private boolean isAppCacheUpToDate1(Path dir) throws IOException {
boolean res = testAppCacheUpToDate(dir);
if (!res) {
lockAppCache(dir);
res = testAppCacheUpToDate(dir);
if (res)
unlockAppCache(dir);
}
return res;
}
private boolean testAppCacheUpToDate(Path dir) throws IOException {
if (systemPropertyEmptyOrTrue(PROP_RESET))
return false;
Path extractedFile = dir.resolve(TIMESTAMP_FILE_NAME);
if (!Files.exists(extractedFile))
return false;
final FileTime jarTime = Files.getLastModifiedTime(getJarFile());
final FileTime extractedTime = Files.getLastModifiedTime(extractedFile);
final boolean fresh = extractedTime.compareTo(jarTime) >= 0;
log(LOG_DEBUG, "JAR timestamp: " + jarTime + " Cache timestamp: " + extractedTime + " (" + (fresh ? "fresh" : "stale") + ")");
return fresh;
}
private void extractCapsule(Path dir) throws IOException {
try {
log(LOG_VERBOSE, "Extracting " + getJarFile() + " to app cache directory " + dir.toAbsolutePath());
log(LOG_DEBUG, new Exception("Stack trace"));
extractJar(openJarInputStream(getJarFile()), dir);
} catch (IOException e) {
throw new IOException("Exception while extracting jar " + getJarFile() + " to app cache directory " + dir.toAbsolutePath(), e);
}
}
private void cleanupCache(Throwable exception) {
try {
try {
if (exception == null)
markCache();
} finally {
unlockAppCache();
}
} catch (IOException e) {
throw rethrow(e);
}
}
private void markCache() throws IOException {
if (!oc.plainCache || oc.appDir == null || oc.cacheUpToDate)
return;
if (Files.isWritable(oc.appDir))
Files.createFile(oc.appDir.resolve(TIMESTAMP_FILE_NAME));
}
private void lockAppCache(Path dir) throws IOException {
final Path lockFile = addTempFile(dir.resolve(LOCK_FILE_NAME));
log(LOG_VERBOSE, "Locking " + lockFile);
final FileChannel c = FileChannel.open(lockFile, new HashSet<>(asList(StandardOpenOption.CREATE, StandardOpenOption.WRITE)), getPermissions(dir));
if (this.appCacheLock != null) {
// This shouldn't happen, but due to some other bug it's possible that the cache was locked and not released.
// In this case, attempting to lock again would throw an OverlappingFileLockException and may obscure the real problem.
log(LOG_QUIET, "Attempting to double lock (ignoring, but this is most likely a bug in CAPSULE)");
c.close();
} else {
this.appCacheLock = c.lock();
}
}
private void unlockAppCache(Path dir) throws IOException {
if (appCacheLock != null) {
log(LOG_VERBOSE, "Unlocking " + dir.resolve(LOCK_FILE_NAME));
appCacheLock.release();
appCacheLock.acquiredBy().close();
appCacheLock = null;
}
}
private void unlockAppCache() throws IOException {
if (!oc.plainCache || oc.appDir == null)
return;
unlockAppCache(oc.appDir);
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Script Process">
/////////// Script Process ///////////////////////////////////
private boolean buildScriptProcess(ProcessBuilder pb) {
final Object script = getAttribute(ATTR_SCRIPT);
if (script == null)
return false;
setJavaHomeEnv(pb, getJavaHome());
final List<Object> classPath = getAttribute(ATTR_APP_CLASS_PATH);
resolveNativeDependencies();
pb.environment().put(VAR_CLASSPATH, compileClassPath(resolve(classPath)));
pb.command().add(processOutgoingPath(script));
return true;
}
private Path setJavaHomeEnv(ProcessBuilder pb, Path javaHome) {
if (javaHome == null)
return null;
pb.environment().put(VAR_JAVA_HOME, processOutgoingPath(javaHome));
return javaHome;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Java Process">
/////////// Java Process ///////////////////////////////////
private boolean buildJavaProcess(ProcessBuilder pb, List<String> cmdLine) {
final List<String> command = pb.command();
command.add(processOutgoingPath(getJavaExecutable()));
command.addAll(buildJVMArgs(cmdLine));
addOption(command, "-Xbootclasspath:", compileClassPath(resolve(buildBootClassPath(cmdLine))));
addOption(command, "-Xbootclasspath/p:", compileClassPath(resolve(getAttribute(ATTR_BOOT_CLASS_PATH_P))));
addOption(command, "-Xbootclasspath/a:", compileClassPath(resolve(getAttribute(ATTR_BOOT_CLASS_PATH_A))));
command.addAll(compileAgents("-javaagent:", buildAgents(true)));
command.addAll(compileAgents("-agentpath:", buildAgents(false)));
final List<Path> classPath = resolve(getAttribute(ATTR_APP_CLASS_PATH));
final String mainClass = getMainClass(classPath);
command.addAll(compileSystemProperties(buildSystemProperties(cmdLine))); // must be called after buildClassPath and all resolutions
final String cpstr;
if ((cpstr = compileClassPath(handleLongClasspath(classPath, mainClass.length(), command, oc.args_))) != null) {
command.add("-classpath");
command.add(cpstr);
}
command.add(mainClass);
return true;
}
private void lookupAllDependencies() {
final long start = clock();
if (hasAttribute(ATTR_APP_ARTIFACT))
lookup(getAttribute(ATTR_APP_ARTIFACT));
for (Map.Entry<String, Object[]> attr : ATTRIBS.entrySet()) {
if (hasFILE_T(attr.getValue()[ATTRIB_TYPE]))
getAttribute(attr(attr.getKey()));
}
time("lookupAllDependencies", start);
}
/**
* Returns the path to the executable that will be used to launch Java.
* The default implementation uses the {@code capsule.java.cmd} property or the {@code JAVACMD} environment variable,
* and if not set, returns the value of {@code getJavaExecutable(getJavaHome())}.
*/
protected Path getJavaExecutable() {
return (_ct = getCallTarget(Capsule.class)) != null ? _ct.getJavaExecutable() : getJavaExecutable0();
}
private Path getJavaExecutable0() {
String javaCmd = emptyToNull(getProperty(PROP_CAPSULE_JAVA_CMD));
if (javaCmd != null)
return path(javaCmd);
return getJavaExecutable(getJavaHome());
}
/**
* Finds the path to the executable that will be used to launch Java within the given {@code javaHome}.
*/
protected static final Path getJavaExecutable(Path javaHome) {
return getJavaExecutable0(javaHome);
}
private static List<String> compileSystemProperties(Map<String, String> ps) {
final List<String> command = new ArrayList<>();
for (Map.Entry<String, String> entry : ps.entrySet())
command.add("-D" + entry.getKey() + (entry.getValue() != null && !entry.getValue().isEmpty() ? "=" + entry.getValue() : ""));
return command;
}
private String compileClassPath(List<Path> cp) {
if (isEmpty(cp))
return null;
return join(cp, PATH_SEPARATOR);
}
private List<String> compileAgents(String clo, Map<Path, String> agents) {
final List<String> command = new ArrayList<>();
for (Map.Entry<Path, String> agent : nullToEmpty(agents).entrySet())
command.add(clo + first(resolve(agent.getKey())) + (agent.getValue().isEmpty() ? "" : ("=" + agent.getValue())));
return command;
}
private static void addOption(List<String> cmdLine, String prefix, String value) {
if (value == null)
return;
cmdLine.add(prefix + value);
}
private List<Object> buildClassPath0(List<Object> classPath0) {
final long start = clock();
final List<Object> classPath = new ArrayList<>();
// The caplets and capsule jars
if (!isWrapperOfNonCapsule() && getAttribute(ATTR_CAPSULE_IN_CLASS_PATH))
addCapsuleJars(classPath);
if (hasAttribute(ATTR_APP_ARTIFACT)) {
final String artifact = getAttribute(ATTR_APP_ARTIFACT);
if (isGlob(artifact))
throw new IllegalArgumentException("Glob pattern not allowed in " + ATTR_APP_ARTIFACT + " attribute.");
final Object app = lookup((isWrapperOfNonCapsule() && !isDependency(artifact)) ? toAbsolutePath(path(artifact)).toString()
: sanitize(artifact), ATTR_APP_ARTIFACT);
classPath.add(app);
}
addAllIfAbsent(classPath, classPath0);
classPath.add(lookup("*.jar", ATTR_APP_CLASS_PATH));
classPath.addAll(nullToEmpty(getAttribute(ATTR_DEPENDENCIES)));
// The caplets and capsule jars
if (!isWrapperOfNonCapsule() && isDeepEmpty(classPath))
addCapsuleJars(classPath);
// Remove duplicate JARs while preserving order
final List<Object> ret = new ArrayList<>(new LinkedHashSet<>(classPath));
time("buildClassPath", start);
return ret;
}
private void addCapsuleJars(List<Object> classPath) {
Capsule c = this.cc;
do {
Path p = null;
try {
p = findJarFile(c.getClass());
} catch (final IllegalStateException ignored) {
} // Ignore non-JARs
if (p != null && !classPath.contains(p))
classPath.add(p);
} while ((c = sup) != null);
classPath.add(getJarFile());
}
/**
* Compiles and returns the application's boot classpath as a list of paths.
*/
private List<?> buildBootClassPath(List<String> cmdLine) {
String option = null;
for (String o : cmdLine) {
if (o.startsWith("-Xbootclasspath:"))
option = o.substring("-Xbootclasspath:".length());
}
return option != null ? toPath(asList(option.split(PATH_SEPARATOR))) : getAttribute(ATTR_BOOT_CLASS_PATH);
}
private Map<String, String> buildSystemProperties(List<String> cmdLine) {
final Map<String, String> systemProperties = buildSystemProperties();
// command line overrides everything
for (String option : cmdLine) {
if (option.startsWith("-D") && (!isCapsuleOption(option.substring(2)) || getAttribute(ATTR_AGENT)))
addSystemProperty(option.substring(2), systemProperties);
}
return systemProperties;
}
private Map<String, String> buildSystemProperties() {
final Map<String, String> systemProperties = new HashMap<>();
// attribute
for (Map.Entry<String, String> pv : getAttribute(ATTR_SYSTEM_PROPERTIES).entrySet())
systemProperties.put(pv.getKey(), pv.getValue());
// library path
final List<Path> libraryPath = buildNativeLibraryPath();
systemProperties.put(PROP_JAVA_LIBRARY_PATH, compileClassPath(libraryPath));
// security manager
if (hasAttribute(ATTR_SECURITY_POLICY) || hasAttribute(ATTR_SECURITY_POLICY_A)) {
systemProperties.put(PROP_JAVA_SECURITY_MANAGER, "");
if (hasAttribute(ATTR_SECURITY_POLICY_A))
systemProperties.put(PROP_JAVA_SECURITY_POLICY, toJarUrl(getAttribute(ATTR_SECURITY_POLICY_A)));
if (hasAttribute(ATTR_SECURITY_POLICY))
systemProperties.put(PROP_JAVA_SECURITY_POLICY, "=" + toJarUrl(getAttribute(ATTR_SECURITY_POLICY)));
}
if (hasAttribute(ATTR_SECURITY_MANAGER))
systemProperties.put(PROP_JAVA_SECURITY_MANAGER, getAttribute(ATTR_SECURITY_MANAGER));
// Capsule properties (must come last b/c of getAppDir)
if (getAppId() != null) {
if (getAppDir() != null)
systemProperties.put(PROP_CAPSULE_DIR, processOutgoingPath(getAppDir()));
systemProperties.put(PROP_CAPSULE_JAR, processOutgoingPath(getJarFile()));
systemProperties.put(PROP_CAPSULE_APP, getAppId());
}
return systemProperties;
}
private static void addSystemProperty(String p, Map<String, String> ps) {
try {
String name = getBefore(p, '=');
String value = getAfter(p, '=');
ps.put(name, value);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Illegal system property definition: " + p);
}
}
//<editor-fold desc="Native Dependencies">
/////////// Native Dependencies ///////////////////////////////////
private List<Path> buildNativeLibraryPath() {
final List<Path> libraryPath = new ArrayList<>(getPlatformNativeLibraryPath());
resolveNativeDependencies();
libraryPath.addAll(0, resolve(getAttribute(ATTR_LIBRARY_PATH_P)));
libraryPath.addAll(resolve(getAttribute(ATTR_LIBRARY_PATH_A)));
if (!listJar(getJarFile(), "*." + getNativeLibExtension(), true).isEmpty())
appDir();
if (getAppDir() != null)
libraryPath.add(getAppDir());
if (hasWritableAppCache())
libraryPath.add(getWritableAppCache());
return libraryPath;
}
/**
* Returns the default native library path for the Java platform the application uses.
*/
protected List<Path> getPlatformNativeLibraryPath() {
return (_ct = getCallTarget(Capsule.class)) != null ? _ct.getPlatformNativeLibraryPath() : getPlatformNativeLibraryPath0();
}
private List<Path> getPlatformNativeLibraryPath0() {
// WARNING: this assumes the platform running the app (say a different Java home), has the same java.library.path.
final List<String> ps = split(getProperty(PROP_JAVA_LIBRARY_PATH), PATH_SEPARATOR);
ps.remove(".");
return toPath(ps);
}
private void resolveNativeDependencies() {
resolve(getAttribute(ATTR_NATIVE_DEPENDENCIES).keySet());
}
//</editor-fold>
private List<String> buildJVMArgs(List<String> cmdLine) {
final Map<String, String> jvmArgs = new LinkedHashMap<>();
for (String option : buildJVMArgs())
addJvmArg(option, jvmArgs);
for (String option : nullToEmpty(parseCommandLineArguments(getProperty(PROP_JVM_ARGS))))
addJvmArg(option, jvmArgs);
// command line overrides everything
for (String option : cmdLine) {
if (!option.startsWith("-D") && !option.startsWith("-Xbootclasspath:"))
addJvmArg(option, jvmArgs);
}
return new ArrayList<String>(jvmArgs.values());
}
private List<String> buildJVMArgs() {
final Map<String, String> jvmArgs = new LinkedHashMap<>();
for (String a : getAttribute(ATTR_JVM_ARGS)) {
a = a.trim();
if (!a.isEmpty() && !a.startsWith("-Xbootclasspath:") && !a.startsWith("-javaagent:"))
addJvmArg(toNativePath(expand(a)), jvmArgs);
}
return new ArrayList<String>(jvmArgs.values());
}
private static void addJvmArg(String a, Map<String, String> args) {
args.put(getJvmArgKey(a), a);
}
private static String getJvmArgKey(String a) {
if (a.equals("-client") || a.equals("-server"))
return "compiler";
if (a.equals("-enablesystemassertions") || a.equals("-esa")
|| a.equals("-disablesystemassertions") || a.equals("-dsa"))
return "systemassertions";
if (a.equals("-jre-restrict-search") || a.equals("-no-jre-restrict-search"))
return "-jre-restrict-search";
if (a.startsWith("-Xloggc:"))
return "-Xloggc";
if (a.startsWith("-Xss"))
return "-Xss";
if (a.startsWith("-Xmx"))
return "-Xmx";
if (a.startsWith("-Xms"))
return "-Xms";
if (a.startsWith("-XX:+") || a.startsWith("-XX:-"))
return "-XX:" + a.substring("-XX:+".length());
if (a.contains("="))
return a.substring(0, a.indexOf('='));
return a;
}
private Map<Path, String> buildAgents(boolean java) {
final long start = clock();
final Map<Object, String> agents0 = getAttribute(java ? ATTR_JAVA_AGENTS : ATTR_NATIVE_AGENTS);
final Map<Path, String> agents = new LinkedHashMap<>(agents0.size());
for (Map.Entry<Object, String> agent : agents0.entrySet()) {
final Object agentName = agent.getKey();
final String agentOptions = agent.getValue();
final Path agentPath = first(resolve(agentName));
agents.put(agentPath, ((agentOptions != null && !agentOptions.isEmpty()) ? agentOptions : ""));
}
time("buildAgents (" + (java ? "java" : "native") + ")", start);
return emptyToNull(agents);
}
private String getMainClass(List<Path> classPath) {
log(LOG_DEBUG, "getMainClass: " + classPath);
String mainClass = getAttribute(ATTR_APP_CLASS);
if (mainClass == null && hasAttribute(ATTR_APP_ARTIFACT))
mainClass = oc.appArtifactMainClass;
if (mainClass == null)
throw new RuntimeException("Jar " + first(classPath).toAbsolutePath() + " does not have a main class defined in the manifest.");
return mainClass;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Get Java Home">
/////////// Get Java Home ///////////////////////////////////
/**
* The path to the Java installation this capsule's app will use.
*/
protected final Path getJavaHome() {
if (oc.javaHome == null) {
Entry<String, Path> jhome = chooseJavaHome();
if (jhome == null)
jhome = entry(getProperty(PROP_JAVA_VERSION), Paths.get(getProperty(PROP_JAVA_HOME)));
oc.javaVersion = jhome.getKey();
oc.javaHome = jhome.getValue();
log(LOG_VERBOSE, "Using JVM: " + oc.javaHome);
}
return oc.javaHome;
}
/**
* Chooses which Java installation to use for running the app.
*
* @return the path of the Java installation to use for launching the app and its version, or {@code null} if the current JVM is to be used.
*/
protected Entry<String, Path> chooseJavaHome() {
return (_ct = getCallTarget(Capsule.class)) != null ? _ct.chooseJavaHome() : chooseJavaHome0();
}
private Entry<String, Path> chooseJavaHome0() {
final long start = clock();
final String propJHome = emptyToNull(getProperty(PROP_CAPSULE_JAVA_HOME));
Entry<String, Path> jhome = null;
if (!"current".equals(propJHome)) {
jhome = propJHome != null ? entry(getJavaVersion(Paths.get(propJHome)), Paths.get(propJHome)) : null;
if (jhome == null && !isMatchingJavaVersion(getProperty(PROP_JAVA_VERSION), isJDK(Paths.get(getProperty(PROP_JAVA_HOME))))) {
final boolean jdk = getAttribute(ATTR_JDK_REQUIRED);
jhome = findJavaHome(jdk);
if (isLogging(LOG_VERBOSE))
log(LOG_VERBOSE, "Finding JVM: " + ((System.nanoTime() - start) / 1_000_000) + "ms");
if (jhome == null) {
throw new RuntimeException("Could not find Java installation for requested version "
+ '[' + "Min. Java version: " + getAttribute(ATTR_MIN_JAVA_VERSION)
+ " JavaVersion: " + getAttribute(ATTR_JAVA_VERSION)
+ " Min. update version: " + getAttribute(ATTR_MIN_UPDATE_VERSION) + ']'
+ " (JDK required: " + jdk + ")"
+ ". You can override the used Java version with the -D" + PROP_CAPSULE_JAVA_HOME + " flag.");
}
}
}
time("chooseJavaHome", start);
return jhome;
}
private Entry<String, Path> findJavaHome(boolean jdk) {
Map<String, List<Path>> homes = nullToEmpty(getJavaHomes());
Path bestPath = null;
String bestVersion = null;
for (Map.Entry<String, List<Path>> e : homes.entrySet()) {
for (Path home : e.getValue()) {
final String v = e.getKey();
log(LOG_DEBUG, "Trying JVM: " + e.getValue() + " (version " + v + ")");
if (isMatchingJavaVersion(v, isJDK(home))) {
log(LOG_DEBUG, "JVM " + e.getValue() + " (version " + v + ") matches");
if (bestVersion == null || compareVersions(v, bestVersion) > 0) {
log(LOG_DEBUG, "JVM " + e.getValue() + " (version " + v + ") is best so far");
bestVersion = v;
bestPath = home;
}
}
}
}
return bestVersion != null ? entry(bestVersion, bestPath) : null;
}
private boolean isMatchingJavaVersion(String javaVersion, boolean jdk) {
final boolean jdkRequired = getAttribute(ATTR_JDK_REQUIRED);
if (jdkRequired && !jdk) {
log(LOG_DEBUG, "Java version " + javaVersion + " fails to match because JDK required and this is not a JDK");
return false;
}
if (hasAttribute(ATTR_MIN_JAVA_VERSION) && compareVersions(javaVersion, getAttribute(ATTR_MIN_JAVA_VERSION)) < 0) {
log(LOG_DEBUG, "Java version " + javaVersion + " fails to match due to " + ATTR_MIN_JAVA_VERSION + ": " + getAttribute(ATTR_MIN_JAVA_VERSION));
return false;
}
if (hasAttribute(ATTR_JAVA_VERSION) && compareVersions(javaVersion, shortJavaVersion(getAttribute(ATTR_JAVA_VERSION)), 3) > 0) {
log(LOG_DEBUG, "Java version " + javaVersion + " fails to match due to " + name(ATTR_JAVA_VERSION) + ": " + getAttribute(ATTR_JAVA_VERSION));
return false;
}
if (getMinUpdateFor(javaVersion) > parseJavaVersion(javaVersion)[3]) {
log(LOG_DEBUG, "Java version " + javaVersion + " fails to match due to " + name(ATTR_MIN_UPDATE_VERSION) + ": " + getAttribute(ATTR_MIN_UPDATE_VERSION) + " (" + getMinUpdateFor(javaVersion) + ")");
return false;
}
log(LOG_DEBUG, "Java version " + javaVersion + " matches");
return true;
}
private int getMinUpdateFor(String version) {
final Map<String, String> m = getAttribute(ATTR_MIN_UPDATE_VERSION);
final int[] ver = parseJavaVersion(version);
for (Map.Entry<String, String> entry : m.entrySet()) {
if (equals(ver, toInt(shortJavaVersion(entry.getKey()).split(SEPARATOR_DOT)), 3))
return Integer.parseInt(entry.getValue());
}
return 0;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Attributes">
/////////// Attributes ///////////////////////////////////
@SuppressWarnings("unchecked")
private <T> T attribute0(Entry<String, T> attr) {
T value;
if (ATTR_APP_ID == attr) {
String id = attribute00(ATTR_APP_ID);
if (id == null && getManifestAttribute(ATTR_IMPLEMENTATION_TITLE) != null)
id = getManifestAttribute(ATTR_IMPLEMENTATION_TITLE);
if (id == null && hasAttribute(ATTR_APP_ARTIFACT) && isDependency(getAttribute(ATTR_APP_ARTIFACT)))
id = getAppArtifactId(getAttribute(ATTR_APP_ARTIFACT));
value = (T) id;
} else if (ATTR_APP_ARTIFACT == attr) {
String artifact = attribute00(ATTR_APP_ARTIFACT);
if (artifact == null && oc.nonCapsuleTarget != null)
artifact = oc.nonCapsuleTarget;
value = (T) artifact;
} else if (ATTR_APP_NAME == attr) {
String name = attribute00(ATTR_APP_NAME);
if (name == null)
name = getManifestAttribute(ATTR_IMPLEMENTATION_TITLE);
value = (T) name;
} else if (ATTR_APP_VERSION == attr) {
String ver = attribute00(ATTR_APP_VERSION);
if (ver == null && getManifestAttribute(ATTR_IMPLEMENTATION_VERSION) != null)
ver = getManifestAttribute(ATTR_IMPLEMENTATION_VERSION);
if (ver == null && hasAttribute(ATTR_APP_ARTIFACT) && isDependency(getAttribute(ATTR_APP_ARTIFACT)))
ver = getAppArtifactVersion(getAttribute(ATTR_APP_ARTIFACT));
value = (T) ver;
} else if (ATTR_APP_CLASS_PATH == attr) {
value = (T) buildClassPath0(attribute00(ATTR_APP_CLASS_PATH));
} else
value = attribute00(attr);
// various modifications:
value = windowsAttributes(attr, value);
value = agentAttributes(attr, value);
return value;
}
/*
* The methods in this section are the only ones accessing the manifest. Therefore other means of
* setting attributes can be added by changing these methods alone.
*/
/**
* Registers a manifest attribute. Must be called during the caplet's static initialization.
*
* @param attrName the attribute's name
* @param type the attribute's type, obtained by calling one (or a combination) of the "type" methods:
* {@link #T_STRING() T_STRING}, {@link #T_BOOL() T_BOOL}, {@link #T_LONG() T_LONG}, {@link #T_DOUBLE() T_DOUBLE},
* {@link #T_LIST(Object) T_LIST}, {@link #T_MAP(Object, Object, Object) T_MAP}, {@link #T_SET(Object) T_SET}
* @param defaultValue the attribute's default value, or {@code null} for none; a {@code null} value for collection or map types will be transformed into the type's empty value (i.e. empty list, empty map, etc.)
* @param allowModal whether the attribute is modal (i.e. can be specified per mode); if {@code false}, then the attribute is only allowed in the manifest's main section.
* @param description a description of the attribute
* @return the attribute's name
*/
protected static final <T> Entry<String, T> ATTRIBUTE(String attrName, T type, T defaultValue, boolean allowModal, String description) {
if (!isValidType(type))
throw new IllegalArgumentException("Type " + type + " is not supported for attributes");
final Object[] conf = new Object[]{type, defaultValue, allowModal, description};
final Object[] old = ATTRIBS.get(attrName);
if (old != null) {
if (asList(conf).subList(0, conf.length - 1).equals(asList(old).subList(0, conf.length - 1))) // don't compare description
throw new IllegalStateException("Attribute " + attrName + " has a conflicting registration: " + Arrays.toString(old));
}
ATTRIBS.put(attrName, conf);
return attr(attrName);
}
private static <T> Entry<String, T> attr(String name) {
return entry(name, null);
}
/**
* Returns the value of the given manifest attribute with consideration to the capsule's mode.
* If the attribute is not defined, its default value will be returned
* (if set with {@link #ATTRIBUTE(String, Object, Object, boolean, String) ATTRIBUTE()}).
* <p>
* Note that caplets may manipulate the value this method returns by overriding {@link #attribute(Map.Entry) }.
*
* @param attr the attribute
* @return the value of the attribute.
*/
protected final <T> T getAttribute(Entry<String, T> attr) {
try {
T value = getAttributeNoLookup(attr);
if (hasFILE_T(type(attr))) {
value = lookupInAttribute(value, attr);
setContext("attribute", name(attr), value);
}
return value;
} catch (Exception e) {
throw new RuntimeException("Exception while getting attribute " + name(attr), e);
}
}
/**
* Returns the default value of the given manifest attribute.
*
* @param attr the attribute
* @return the default value of the attribute.
*/
protected final <T> T getAttributeNoLookup(Entry<String, T> attr) {
setContext("attribute", name(attr), null);
if (name(ATTR_CAPLETS).equals(name(attr)))
return attribute0(attr);
try {
T value = cc.attribute(attr);
setContext("attribute", name(attr), value);
return value;
} catch (Exception e) {
throw new RuntimeException("Exception while getting attribute " + name(attr), e);
}
}
protected final <T> T getAttributeDefaultValue(Entry<String, T> attr) {
final Object[] conf = ATTRIBS.get(name(attr));
final T type = type(attr);
return defaultValue(type, (T) (conf != null ? conf[ATTRIB_DEFAULT] : null));
}
@SuppressWarnings("unchecked")
private static <T> T cast(Entry<String, T> attr, Object value) {
return (T) value;
}
/**
* Returns an attribute's name.
*/
protected final String name(Entry<String, ?> attribute) {
if (attribute == null)
return null;
return attribute.getKey();
}
private <T> T type(Entry<String, T> attribute) {
final Object[] conf = ATTRIBS.get(name(attribute));
return (T) (conf != null ? conf[ATTRIB_TYPE] : T_STRING());
}
private static boolean isLegalModeName(String name) {
return !name.contains("/") && !name.endsWith(".class") && !name.endsWith(".jar")
&& !isJavaVersionSpecific(name) && !isOsSpecific(name);
}
private void validateManifest(Manifest manifest) {
final String mainClass = manifest.getMainAttributes().getValue(ATTR_MAIN_CLASS);
// if (mainClass != null && !mainClass.equals(manifest.getMainAttributes().getValue(ATTR_PREMAIN_CLASS)))
// throw new IllegalStateException("Capsule manifest must specify " + mainClass
// + " in the " + ATTR_PREMAIN_CLASS + " attribute.");
if (manifest.getMainAttributes().getValue(ATTR_CLASS_PATH) != null)
throw new IllegalStateException("Capsule manifest contains a " + ATTR_CLASS_PATH + " attribute."
+ " Use " + ATTR_APP_CLASS_PATH + " and/or " + ATTR_DEPENDENCIES + " instead.");
validateNonModalAttributes(manifest);
if (!hasAttribute(ATTR_APP_NAME) && hasModalAttribute(ATTR_APP_ARTIFACT))
throw new IllegalArgumentException("App ID-related attribute " + ATTR_APP_ARTIFACT + " is defined in a modal section of the manifest. "
+ " In this case, you must add the " + ATTR_APP_NAME + " attribute to the manifest's main section.");
// validate section case-insensitivity
final Set<String> sectionsLowercase = new HashSet<>();
for (String section : manifest.getEntries().keySet()) {
if (!sectionsLowercase.add(section.toLowerCase()))
throw new IllegalArgumentException("Manifest in JAR " + jarFile + " contains a case-insensitive duplicate of section " + section);
}
}
private void validateNonModalAttributes(Manifest manifest) {
for (Map.Entry<String, Attributes> entry : manifest.getEntries().entrySet()) {
for (Object attr : entry.getValue().keySet()) {
if (!allowsModal(attr.toString()))
throw new IllegalStateException("Manifest section " + entry.getKey() + " contains non-modal attribute " + attr);
}
}
}
private boolean hasModalAttribute(Entry<String, ?> attr) {
final Attributes.Name key = new Attributes.Name(name(attr));
for (Map.Entry<String, Attributes> entry : oc.manifest.getEntries().entrySet()) {
if (entry.getValue().containsKey(key))
return true;
}
return false;
}
private boolean hasMode(String mode) {
if (!isLegalModeName(mode))
throw new IllegalArgumentException(mode + " is an illegal mode name");
if (oc.manifest.getAttributes(mode) != null)
return true;
return false;
}
/**
* Returns the names of all modes defined in this capsule's manifest.
*/
protected final Set<String> getModes() {
final Set<String> modes = new HashSet<>();
for (Map.Entry<String, Attributes> entry : oc.manifest.getEntries().entrySet()) {
if (isLegalModeName(entry.getKey()) && !isDigest(entry.getValue()))
modes.add(entry.getKey());
}
return unmodifiableSet(modes);
}
@SuppressWarnings("unchecked")
private String getManifestAttribute(String attr) {
return oc.manifest.getMainAttributes().getValue(attr);
}
/**
* Returns the description of the given mode.
*/
protected final String getModeDescription(String mode) {
if (!isLegalModeName(mode))
throw new IllegalArgumentException(mode + " is an illegal mode name");
if (oc.manifest != null && oc.manifest.getAttributes(mode) != null)
return oc.manifest.getAttributes(mode).getValue(name(ATTR_MODE_DESC));
return null;
}
private static boolean isDigest(Attributes attrs) {
for (Object name : attrs.keySet()) {
if (!name.toString().toLowerCase().endsWith("-digest") && !name.toString().equalsIgnoreCase("Magic"))
return false;
}
return true;
}
private static boolean isOsSpecific(String section) {
section = section.toLowerCase();
if (PLATFORMS.contains(section))
return true;
for (String os : PLATFORMS) {
if (section.endsWith("-" + os))
return true;
}
return false;
}
private static final Pattern PAT_JAVA_SPECIFIC_SECTION = Pattern.compile("\\A(.+-|)java-[0-9]+\\z");
private static boolean isJavaVersionSpecific(String section) {
return PAT_JAVA_SPECIFIC_SECTION.matcher(section.toLowerCase()).find();
}
/**
* CAPLET OVERRIDE ONLY: Returns the value of the given capsule attribute with consideration to the capsule's mode.
* Caplets may override this method to manipulate capsule attributes. This method must not be called directly except
* as {@code super.attribute(attr)} calls in the caplet's implementation of this method.
* <p>
* The default implementation parses and returns the relevant manifest attribute or its default value if undefined.
*
* @param attr the attribute
* @return the value of the attribute.
* @see #getAttribute(Map.Entry)
*/
protected <T> T attribute(Entry<String, T> attr) {
return sup != null ? sup.attribute(attr) : attribute0(attr);
}
@SuppressWarnings("unchecked")
private <T> T attribute00(Entry<String, T> attr) {
final Object[] conf = ATTRIBS.get(name(attr));
// if (conf == null)
// throw new IllegalArgumentException("Attribute " + attr.getKey() + " has not been registered with ATTRIBUTE");
final T type = type(attr);
T value = oc.getAttribute0(name(attr), type);
if (isEmpty(value))
value = defaultValue(type, (T) (conf != null ? conf[ATTRIB_DEFAULT] : null));
setContext("attribute", attr.getKey(), value);
return value;
}
private <T> T parseAttribute(String attr, T type, String s) {
try {
return parse(expand(s), type, this);
} catch (RuntimeException e) {
throw new IllegalArgumentException("Error parsing attribute " + attr + ". Expected " + typeString(type) + " but was: " + s, e);
}
}
private <T> T getAttribute0(String attr, T type) {
T value = null;
final String majorJavaVersion = majorJavaVersion(oc.javaVersion);
if (manifest != null) {
value = merge(value, parseAttribute(attr, type, getAttributes(manifest, null, null).getValue(attr)));
if (majorJavaVersion != null)
value = merge(value, parseAttribute(attr, type, getAttributes(manifest, null, "java-" + majorJavaVersion).getValue(attr)));
value = merge(value, parseAttribute(attr, type, getPlatformAttribute(null, attr)));
if (getMode() != null && allowsModal(attr)) {
value = merge(value, parseAttribute(attr, type, getAttributes(manifest, mode, null).getValue(attr)));
if (majorJavaVersion != null)
value = merge(value, parseAttribute(attr, type, getAttributes(manifest, mode, "java-" + majorJavaVersion).getValue(attr)));
value = merge(value, parseAttribute(attr, type, getPlatformAttribute(getMode(), attr)));
}
setContext("attribute of " + jarFile, attr, value);
}
return value;
}
private String getPlatformAttribute(String mode, String attr) {
if (PLATFORM == null)
return null;
String value = null;
if (value == null)
value = getAttributes(manifest, mode, PLATFORM).getValue(attr);
if (value == null && isUnix())
value = getAttributes(manifest, mode, OS_UNIX).getValue(attr);
if (value == null && (isUnix() || isMac()))
value = getAttributes(manifest, mode, OS_POSIX).getValue(attr);
return value;
}
private static Attributes getAttributes(Manifest manifest, String mode, String platform) {
if (emptyToNull(mode) == null && emptyToNull(platform) == null)
return manifest.getMainAttributes();
if (emptyToNull(mode) == null)
return getAttributes(manifest, platform);
if (emptyToNull(platform) == null)
return getAttributes(manifest, mode);
return getAttributes(manifest, mode + "-" + platform);
}
/**
* Tests whether the given attribute is found in the manifest.
*
* @param attr the attribute
*/
protected final boolean hasAttribute(Entry<String, ?> attr) {
return !isEmpty(getAttribute(attr));
}
private boolean allowsModal(String attr) {
final Object[] vals = ATTRIBS.get(attr);
return vals != null ? (Boolean) vals[ATTRIB_MODAL] : true;
}
//<editor-fold defaultstate="collapsed" desc="Attribute Types and Parsing">
/////////// Attribute Types and Parsing ///////////////////////////////////
/**
* Represents the attribute type {@code String}
*/
protected static final String T_STRING() {
return "";
}
/**
* Represents an attribute type that contains a file path, dependency or class name
*/
protected static final Object T_FILE() {
return T_FILE("");
}
/**
* Represents an attribute type that contains a file path, dependency or class name
*/
protected static final Object T_FILE(String type) {
return new AtomicReference<Object>(type);
}
/**
* Represents the attribute type {@code Boolean}
*/
protected static final Boolean T_BOOL() {
return false;
}
/**
* Represents the attribute type {@code Long}
*/
protected static final Long T_LONG() {
return 0L;
}
/**
* Represents the attribute type {@code Double}
*/
protected static final Double T_DOUBLE() {
return 0.0;
}
/**
* A {@code List} of type {@code type}
*
* @param type One of {@link #T_STRING() T_STRING}, {@link #T_FILE() T_FILE}, {@link #T_BOOL() T_BOOL}, {@link #T_LONG() T_LONG}, {@link #T_DOUBLE() T_DOUBLE}
*/
protected static final <E> List<E> T_LIST(E type) {
return singletonList(type);
}
/**
* A {@code Set} of type {@code type}
*
* @param type One of {@link #T_STRING() T_STRING}, {@link #T_FILE() T_FILE}, {@link #T_BOOL() T_BOOL}, {@link #T_LONG() T_LONG}, {@link #T_DOUBLE() T_DOUBLE}
*/
protected static final <E> Set<E> T_SET(E type) {
return singleton(type);
}
/**
* A {@code Map} from type {@code keyType} to type {@code valType}
*
* @param keyType One of {@link #T_STRING() T_STRING}, {@link #T_FILE() T_FILE}, {@link #T_BOOL() T_BOOL}, {@link #T_LONG() T_LONG}, {@link #T_DOUBLE() T_DOUBLE}
* @param valType One of {@link #T_STRING() T_STRING}, {@link #T_FILE() T_FILE}, {@link #T_BOOL() T_BOOL}, {@link #T_LONG() T_LONG}, {@link #T_DOUBLE() T_DOUBLE}
* @param defaultValue The default value for a key without a value in the attribute string, or {@code null} if all keys must explicitly specify a value.
*/
@SuppressWarnings("unchecked")
protected static final <K, V> Map<K, V> T_MAP(K keyType, V valType, V defaultValue) {
if (defaultValue != null) {
final Map<K, V> map = new LinkedHashMap<>();
map.put(keyType, valType);
map.put(null, promote(defaultValue, valType));
return map;
} else
return singletonMap(keyType, valType);
}
@SuppressWarnings("unchecked")
private static boolean isValidType(Object type) {
if (type == null)
return false;
Object etype = null;
if (type instanceof Collection) {
if (!(type instanceof List || type instanceof Set))
return false;
etype = ((Collection<?>) type).iterator().next();
} else if (type instanceof Map) {
final Map.Entry<String, ?> desc = ((Map<String, ?>) type).entrySet().iterator().next();
etype = desc.getValue();
} else if (type instanceof AtomicReference) {
return ((AtomicReference<?>) type).get() instanceof String;
}
if (etype != null) {
if (etype instanceof Collection || etype instanceof Map)
return false;
return isValidType(etype);
} else
return ((Collection<Class>) (Object) asList(String.class, Boolean.class, Long.class, Double.class)).contains(type.getClass());
}
private static String typeString(Object type) {
if (type instanceof Collection) {
final Object etype = ((Collection<?>) type).iterator().next();
final String collType = type instanceof Set ? "Set" : "List";
return collType + " of " + typeString(etype) + " in the form \"v1 v2 ...\"";
} else if (type instanceof Map) {
final Map.Entry<String, ?> desc = ((Map<String, ?>) type).entrySet().iterator().next();
final Object ktype = desc.getKey();
final Object vtype = desc.getValue();
return "map of " + typeString(ktype) + " to " + typeString(vtype) + " in the form \"k1=v1 k2=v2 ...\"";
} else
return type instanceof AtomicReference ? "path or dependency" : type.getClass().getSimpleName();
}
@SuppressWarnings("unchecked")
private <T> T defaultValue(T type, T d) {
if (d == null) {
if (type instanceof List)
return (T) emptyList();
if (type instanceof Set)
return (T) emptySet();
if (type instanceof Map)
return (T) emptyMap();
}
return d;
}
// visible for testing
@SuppressWarnings("unchecked")
static <T> T parse(String s, T type, Capsule capsule) {
if (s == null)
return null;
if (type instanceof Collection) {
final Object etype = ((Collection<?>) type).iterator().next();
final List<String> slist = parse(s);
// if (type instanceof List && etype instanceof String)
// return (T) slist;
final Collection<Object> coll = type instanceof Set ? new LinkedHashSet<>() : new ArrayList<>();
for (String se : slist)
coll.add(parse(se, etype, capsule));
return (T) coll;
} else if (type instanceof Map) {
final Iterator<Map.Entry<?, ?>> it = ((Map) type).entrySet().iterator();
final Map.Entry<?, ?> desc = it.next();
final Object ktype = desc.getKey();
final Object vtype = desc.getValue();
final Object defaultValue = it.hasNext() ? it.next().getValue() : null;
final String sdefaultValue = defaultValue != null ? defaultValue.toString() : null;
final Map<String, String> smap = parse(s, sdefaultValue);
// if (ktype instanceof String && vtype instanceof String)
// return (T) smap;
final Map<Object, Object> map = new LinkedHashMap<>();
for (Map.Entry<String, String> se : smap.entrySet())
map.put(parsePrimitive(se.getKey(), ktype, capsule), parsePrimitive(se.getValue(), vtype, capsule));
return (T) map;
} else
return parsePrimitive(s, type, capsule);
}
@SuppressWarnings("unchecked")
private static <T> T parsePrimitive(String s, T type, Capsule capsule) {
if (s == null)
return null;
if (type instanceof AtomicReference)
return (T) capsule.sanitize(s);
if (type instanceof String)
return (T) s;
if (type instanceof Boolean)
return (T) (Boolean) Boolean.parseBoolean(s);
if (type instanceof Long)
return (T) (Long) Long.parseLong(s);
if (type instanceof Double)
return (T) (Double) Double.parseDouble(s);
throw new IllegalArgumentException("Unsupported primitive attribute type: " + type.getClass().getName());
}
@SuppressWarnings("unchecked")
private static <T> T promote(Object x, T type) {
if (!(x instanceof Number && type instanceof Number))
return (T) x;
if (x instanceof Integer) {
if (type instanceof Long)
x = Long.valueOf((Integer) x);
else if (type instanceof Double)
x = Double.valueOf((Integer) x);
}
return (T) x;
}
private static List<String> parse(String value) {
return split(value, "\\s+");
}
private static Map<String, String> parse(String value, String defaultValue) {
return split(value, '=', "\\s+", defaultValue);
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="T_FILE handling">
/////////// T_FILE handling ///////////////////////////////////
private static boolean hasFILE_T(Object type) {
if (type instanceof Collection) {
final Object etype = ((Collection<?>) type).iterator().next();
return hasFILE_T(etype);
} else if (type instanceof Map) {
final Map.Entry<String, ?> desc = ((Map<String, ?>) type).entrySet().iterator().next();
final Object ktype = desc.getKey();
final Object vtype = desc.getValue();
return hasFILE_T(ktype) || hasFILE_T(vtype);
} else
return type instanceof AtomicReference;
}
private <T> T lookupInAttribute(T value, Entry<String, T> attrib) {
return lookupInAttribute(value, type(attrib), attrib, value);
}
@SuppressWarnings("unchecked")
private <T, T1> T lookupInAttribute(Object o, T type, Entry<String, T1> attrib, T1 value) {
if (o == null)
return null;
if (type instanceof Collection) {
final Object etype = ((Collection<?>) type).iterator().next();
final Collection<?> coll0 = (Collection<?>) o;
final Collection<Object> coll = type instanceof Set ? new LinkedHashSet<>() : new ArrayList<>();
for (Object e : coll0)
coll.add(e instanceof String ? lookupInAttribute(e, etype, attrib, value) : e);
return (T) coll;
} else if (type instanceof Map) {
final Iterator<Map.Entry<?, ?>> it = ((Map) type).entrySet().iterator();
final Map.Entry<?, ?> desc = it.next();
final Object ktype = desc.getKey();
final Object vtype = desc.getValue();
final Map<?, ?> map0 = (Map<?, ?>) o;
final Map<Object, Object> map = new HashMap<>();
for (Map.Entry<?, ?> e : map0.entrySet())
map.put(lookupInAttribute(e.getKey(), ktype, attrib, value), lookupInAttribute(e.getValue(), vtype, attrib, value));
return (T) map;
} else if (type instanceof AtomicReference) {
return (T) (o instanceof String ? lookup((String) o, ((AtomicReference<String>) type).get(), attrib, (value instanceof Map
? ((Map<Object, Object>) value).get(o) : null)) : o);
} else
return (T) o;
}
//</editor-fold>
private static final Attributes EMPTY_ATTRIBUTES = new Attributes();
private static Attributes getAttributes(Manifest manifest, String name) {
// Attributes as = = manifest.getAttributes(name);
// return as != null ? as : EMPTY_ATTRIBUTES;
for (Map.Entry<String, Attributes> entry : manifest.getEntries().entrySet()) {
if (entry.getKey().equalsIgnoreCase(name))
return entry.getValue();
}
return EMPTY_ATTRIBUTES;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Dependency Utils">
/////////// Dependency Utils ///////////////////////////////////
private static boolean isDependency(String lib) {
return lib.contains(":") && !lib.contains(":\\");
}
private static final Pattern PAT_DEPENDENCY = Pattern.compile("(?<groupId>[^:\\(]+):(?<artifactId>[^:\\(]+)(:(?<version>\\(?[^:\\(]*))?(:(?<classifier>[^:\\(]+))?(\\((?<exclusions>[^\\(\\)]*)\\))?");
private Path dependencyToLocalJar(Path jar, String dep, String type, Entry<String, ?> attrContext) {
String res = dependencyToLocalJar0(jar, dep, type, attrContext);
return res != null ? path(toNativePath(res)) : null;
}
private static String dependencyToLocalJar0(Path jar, String dep, String type, Entry<String, ?> attrContext) {
final Matcher m = PAT_DEPENDENCY.matcher(dep);
if (!m.matches())
throw new IllegalArgumentException("Could not parse dependency: " + dep);
final String group = emptyToNull(m.group("groupId"));
final String artifact = emptyToNull(m.group("artifactId"));
final String version = emptyToNull(m.group("version"));
final String classifier = emptyToNull(m.group("classifier"));
final boolean caplet = ATTR_CAPLETS.equals(attrContext);
final String libdir = (caplet ? "capsule" : "lib");
final String filename = artifact
+ (version != null ? '-' + version : "")
+ (classifier != null ? '-' + classifier : "")
+ "." + type;
final List<String> names = new ArrayList<>();
if (group != null) {
names.add(libdir + '/' + group + '/' + filename);
names.add(libdir + '/' + group + '-' + filename);
}
names.add(libdir + '/' + filename);
if (group != null) {
names.add(group + "/" + filename);
names.add(group + '-' + filename);
}
names.add(filename);
Collections.reverse(names);
int index = -1;
try {
for (String entry : cachedEntries(jar))
index = Math.max(index, names.indexOf(entry));
} catch (IOException e) {
throw rethrow(e);
}
return index >= 0 ? names.get(index) : null;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Paths">
/////////// Paths ///////////////////////////////////
/**
* Converts a string file/dependency descriptor listed in the manifest to an opaque file descriptor used in attributes of type {@link #T_FILE() T_FILE}).
*
* @param x the file/dependency descriptor
* @param type the file type (extension), needed only for artifact coordinates; if {@code null}, the default ({@code jar}) is used.
* @param attrContext the attribute containing the file reference to look up; may be {@code null}
* @param context if the attribute {@code attrContext} is a map, the value associated with {@code x}'s respective key; otherwise, {@code null}.
* @return an opaque file descriptor that will later be resolved
*/
protected final Object lookup(String x, String type, Entry<String, ?> attrContext, Object context) {
Object res = cc.lookup0(x, nullToEmpty(type), attrContext, context);
log(LOG_DEBUG, "lookup " + x + "(" + type + ", " + name(attrContext) + ") -> " + res);
if (res == null)
throw new RuntimeException("Lookup for " + x + " has failed.");
return res;
}
/**
* Same as {@link #lookup(String, String, Entry, Object) lookup(x, null, null, null)}
*/
protected final Object lookup(String x) {
return lookup(x, null, null, null);
}
/**
* Same as {@link #lookup(String, String, Entry, Object) lookup(x, null, attrContext, null)}
*/
protected final Object lookup(String x, Entry<String, ?> attrContext) {
return lookup(x, null, attrContext, null);
}
/**
* For internal use; subject to change/removal.
*
* @deprecated exclude from javadocs
*/
protected Object lookup0(Object x, String type, Entry<String, ?> attrContext, Object context) {
_ct = unsafe(getCallTarget(Capsule.class));
final String target = (_ct != null ? _ct.getClass().getName() : Capsule.class.getName()) + '@' + Integer.toHexString(System.identityHashCode(_ct));
log(LOG_DEBUG, "lookup0 " + target + " " + x);
final Object res = _ct != null ? _ct.lookup0(x, type, attrContext, context) : lookup00(x, type, attrContext, context);
log(LOG_DEBUG, "lookup0 " + target + " " + x + " -> " + res);
return res;
}
private Object lookup00(Object x, String type, Entry<String, ?> attrContext, Object context) {
if (x instanceof String) {
String desc = (String) x;
final boolean isDependency = isDependency(desc);
if (!isDependency) {
if (!desc.contains(".") && !type.isEmpty())
desc += "." + type;
desc = toNativePath(desc);
}
if (isDependency)
x = dependencyToLocalJar(getJarFile(), desc, type.isEmpty() ? "jar" : type, attrContext);
else if (isGlob(desc))
x = listJar(getJarFile(), desc, false);
else
x = path(desc);
}
if (x == null)
return null;
if (ATTR_NATIVE_DEPENDENCIES.equals(attrContext)) {
if (type != null && !"jar".equals(type)) {
if (x instanceof Entry)
x = ((Entry<?, ?>) x).getValue();
x = new Object[]{x, context};
}
}
if (asList(ATTR_APP_ARTIFACT, ATTR_SCRIPT, ATTR_NATIVE_DEPENDENCIES).contains(attrContext)) {
if (!(x instanceof Entry))
x = mutableEntry(attrContext, x);
}
return x;
}
/**
* Resolves a file descriptor returned from {@code lookup}.
* This method may have side effects, such as writing to the file system (but they should be idempotent).
* It is not allowed to access the paths returned from this method on the file system.
*
* @param x the value returned from {@code lookup}
* @return the resolved paths
*/
protected final List<Path> resolve(Object x) {
final List<Path> res = cc.resolve0(x);
log(LOG_DEBUG, "resolve " + x + " -> " + res);
if (res == null)
throw new RuntimeException("Could not resolve " + x);
if (res.isEmpty())
log(LOG_VERBOSE, "WARNING resolve " + x + " was empty");
return res;
}
private List<Path> resolve(List<Object> ps) {
if (ps == null)
return null;
final List<Path> res = new ArrayList<>(ps.size());
for (Object p : ps)
addAllIfAbsent(res, resolve(p));
return res;
}
/**
* For internal use; subject to change/removal.
*
* @deprecated exclude from javadocs
*/
protected List<Path> resolve0(Object x) {
_ct = unsafe(getCallTarget(Capsule.class));
final String target = (_ct != null ? _ct.getClass().getName() : Capsule.class.getName()) + '@' + Integer.toHexString(System.identityHashCode(_ct));
log(LOG_DEBUG, "resolve0 " + target + " " + x);
final List<Path> res = _ct != null ? _ct.resolve0(x) : resolve00(x);
log(LOG_DEBUG, "resolve0 " + target + " " + x + " -> " + res);
return res;
}
/**
* Returns the path or paths to the given file descriptor.
* The given descriptor can be a dependency, a file name (relative to the app cache)
* or a glob pattern (again, relative to the app cache). The returned list can contain more than one element
* if a dependency is given and it resolves to more than a single artifact, or if a glob pattern is given,
* which matches more than one file.
*/
private List<Path> resolve00(Object x) {
if (x == null)
return null;
if (x instanceof Collection) {
final List<Path> res = new ArrayList<>();
for (Object xe : ((Collection<?>) x))
addAllIfAbsent(res, resolve(xe));
return res;
}
if (x instanceof Entry) {
final Entry<Entry<String, ?>, ?> context = (Entry<Entry<String, ?>, ?>) x;
final Entry<String, ?> attr = context.getKey();
x = context.getValue();
if (x instanceof Collection) {
final List<Path> res = new ArrayList<>();
for (Object xe : ((Collection<?>) x))
addAllIfAbsent(res, resolve(mutableEntry(context, xe)));
return res;
}
if (ATTR_APP_ARTIFACT.equals(attr)) {
final List<Path> jars;
if (x instanceof Path)
jars = Arrays.asList((Path) x);
else if (x instanceof List && firstOrNull((List<?>) x) instanceof Path)
jars = (List<Path>) x;
else
jars = resolve(x);
final Path jar = simpleResolve(firstOrNull(jars));
final Manifest man = getManifest(jar);
oc.appArtifactMainClass = getMainClass(man);
final List<Path> res = jars;
// final List<Path> res = new ArrayList<>();
// addAllIfAbsent(res, resolveJar(jar, man));
// addAllIfAbsent(res, jars.subList(1, jars.size()));
x = res;
} else if (ATTR_SCRIPT.equals(attr)) {
ensureExecutable(simpleResolve(x));
} else if (ATTR_NATIVE_DEPENDENCIES.equals(attr)) {
final Object[] fileAndRename = (Object[]) x;
final Path lib = only(resolve(fileAndRename[0]));
final String rename = (String) fileAndRename[1];
Path res = lib;
if (rename != null && !rename.isEmpty() && !lib.startsWith(getWritableAppCache())) {
try {
res = getWritableAppCache().resolve(rename);
log(LOG_DEBUG, "Copying native lib " + lib + " to " + res);
Files.copy(lib, res, StandardCopyOption.REPLACE_EXISTING);
fileAndRename[0] = res;
} catch (IOException e) {
throw new RuntimeException("Exception while copying native libs", e);
}
}
x = res;
}
return resolve(x);
}
if (x instanceof Path && ((Path) x).isAbsolute()) {
Path p = ((Path) x).normalize();
//
final Path currentJavaHome = Paths.get(System.getProperty(PROP_JAVA_HOME));
if (p.startsWith(Paths.get(System.getProperty(PROP_JAVA_HOME))))
p = move(p, currentJavaHome, getJavaHome());
return singletonList(p);
} else {
final Path p = simpleResolve(x);
if (p != null)
return resolve(p);
}
throw new RuntimeException("Could not resolve item " + x);
}
private Path simpleResolve(Object x) {
if (x instanceof Path) {
Path p = (Path) x;
p = p.isAbsolute() ? p : appDir().resolve(p);
p = p.toAbsolutePath().normalize();
return p;
}
if (x instanceof URL) {
final URL url = (URL) x;
if ("jar".equals(url.getProtocol())) {
if (!path(getBefore(url.getPath(), '!')).toAbsolutePath().equals(getJarFile()))
throw new AssertionError("URL " + url + " not pointing at capsule JAR " + getJarFile());
return simpleResolve(path(getAfter(url.getPath(), '!').substring(1)));
}
}
return null;
}
private List<Path> resolveJar(Path jar, Manifest man) {
if (man == null)
man = getManifest(jar);
final List<Path> res = new ArrayList<>();
res.add(jar);
jar = simpleResolve(jar);
// add JAR's manifest's Class-Path
for (String e : nullToEmpty(parse(man.getMainAttributes().getValue(ATTR_CLASS_PATH)))) {
Path p;
try {
p = path(new URL(e).toURI());
} catch (MalformedURLException | URISyntaxException ex) {
p = jar.getParent().resolve(path(toNativePath(e)));
}
if (!res.contains(p))
res.add(toAbsolutePath(p));
}
return res;
}
private String processOutgoingPath(Object p) {
return toString(firstOrNull(resolve(p)));
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="JAR Extraction">
/////////// JAR Extraction ///////////////////////////////////
private static void extractJar(JarInputStream jar, Path targetDir) throws IOException {
for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;) {
if (entry.isDirectory() || !shouldExtractFile(entry.getName()))
continue;
if (Files.exists(targetDir.resolve(entry.getName()))) {
log(LOG_QUIET, "Warning: duplicate file " + entry.getName() + " in capsule");
continue;
}
writeFile(targetDir, entry.getName(), jar);
}
}
private static boolean shouldExtractFile(String fileName) {
if (fileName.equals(Capsule.class.getName().replace('.', '/') + ".class")
|| (fileName.startsWith(Capsule.class.getName().replace('.', '/') + "$") && fileName.endsWith(".class")))
return false;
if (fileName.endsWith(".class"))
return false;
if (fileName.startsWith("capsule/") && !fileName.endsWith(".jar"))
return false;
if (fileName.startsWith("META-INF/"))
return false;
return true;
}
private List<Path> listJar(Path jar, String glob, boolean regular) {
final long start = clock();
final List<Path> res = new ArrayList<>();
final Pattern p = Pattern.compile(globToRegex(glob));
try (ZipInputStream zis = openJarInputStream(jar)) {
for (ZipEntry entry; (entry = zis.getNextEntry()) != null;) {
if ((!regular || !entry.isDirectory()) && p.matcher(entry.getName()).matches())
res.add(path(entry.getName())); // new URL("jar", "", jar + "!/" + entry.getName())
}
} catch (IOException e) {
throw rethrow(e);
}
time("listJar", start);
return res;
}
private Path mergeCapsule(Path wrapperCapsule, Path wrappedCapsule, Path outCapsule) throws IOException {
try {
if (Objects.equals(wrapperCapsule, wrappedCapsule)) {
Files.copy(wrappedCapsule, outCapsule);
return outCapsule;
}
final String wrapperVersion = VERSION;
final String wrappedVersion;
try {
wrappedVersion = getCapsuleVersion(newClassLoader(null, wrapperCapsule).loadClass(Capsule.class.getName()));
} catch (ClassNotFoundException e) {
throw new RuntimeException(wrapperCapsule + " is not a valid capsule");
}
if (wrappedVersion == null)
throw new RuntimeException(wrapperCapsule + " is not a valid capsule");
if (Integer.parseInt(getBefore(wrapperVersion, '.')) != Integer.parseInt(getBefore(wrappedVersion, '.')))
throw new RuntimeException("Incompatible Capsule versions: " + wrapperCapsule + " (" + wrapperVersion + "), " + wrappedCapsule + " (" + wrappedVersion + ")");
final int higherVersion = compareVersions(wrapperVersion, wrappedVersion);
try (final OutputStream os = Files.newOutputStream(outCapsule);
final JarInputStream wr = openJarInputStream(wrapperCapsule);
final JarInputStream wd = copyJarPrefix(Files.newInputStream(wrappedCapsule), os)) {
final JarInputStream first = higherVersion >= 0 ? wr : wd;
final JarInputStream second = higherVersion < 0 ? wr : wd;
final Manifest man = new Manifest(wd.getManifest());
final String wrMainClass = wr.getManifest().getMainAttributes().getValue(ATTR_MAIN_CLASS);
if (!Capsule.class.getName().equals(wrMainClass)) {
if (first != wr)
throw new RuntimeException("Main class of wrapper capsule " + wrapperCapsule + " (" + wrMainClass + ") is not " + Capsule.class.getName()
+ " and is of lower version ( " + wrapperVersion + ") than that of the wrapped capsule " + wrappedCapsule + " (" + wrappedVersion + "). Cannot merge.");
man.getMainAttributes().putValue(ATTR_MAIN_CLASS, wrMainClass);
}
final List<String> wrCaplets = nullToEmpty(parse(wr.getManifest().getMainAttributes().getValue(name(ATTR_CAPLETS))));
final ArrayList<String> caplets = new ArrayList<>(nullToEmpty(parse(man.getMainAttributes().getValue(name(ATTR_CAPLETS)))));
addAllIfAbsent(caplets, wrCaplets);
man.getMainAttributes().putValue(name(ATTR_CAPLETS), join(caplets, " "));
try (final JarOutputStream out = new JarOutputStream(os, man)) {
final Set<String> copied = new HashSet<>();
for (JarEntry entry; (entry = first.getNextJarEntry()) != null;) {
if (!entry.getName().equals(MANIFEST_NAME)) {
out.putNextEntry(new JarEntry(entry));
copy(first, out);
out.closeEntry();
copied.add(entry.getName());
}
}
for (JarEntry entry; (entry = second.getNextJarEntry()) != null;) {
if (!entry.getName().equals(MANIFEST_NAME) && !copied.contains(entry.getName())) {
out.putNextEntry(new JarEntry(entry));
copy(second, out);
out.closeEntry();
}
}
log(LOG_VERBOSE, "Testing capsule " + outCapsule);
newCapsule0(newClassLoader(ClassLoader.getSystemClassLoader(), outCapsule), outCapsule); // test capsule
log(LOG_VERBOSE, "Done testing capsule " + outCapsule);
return outCapsule;
}
}
} catch (Exception e) {
try {
Files.delete(outCapsule);
} catch (IOException ex) {
}
throw e;
}
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Path Utils">
/////////// Path Utils ///////////////////////////////////
private FileSystem getFileSystem() {
return cc.jarFile != null ? cc.jarFile.getFileSystem() : FileSystems.getDefault();
}
private Path path(String p, String... more) {
return getFileSystem().getPath(p, more);
}
private Path path(URI uri) {
return getFileSystem().provider().getPath(uri);
}
private List<Path> toPath(List<String> ps) {
if (ps == null)
return null;
final List<Path> aps = new ArrayList<>(ps.size());
for (String p : ps)
aps.add(path(p));
return aps;
}
private static Path toAbsolutePath(Path p) {
return p != null ? p.toAbsolutePath().normalize() : null;
}
private static List<Path> resolve(Path root, List<String> ps) {
if (ps == null)
return null;
final List<Path> aps = new ArrayList<>(ps.size());
for (String p : ps)
aps.add(root.resolve(p));
return aps;
}
private Path sanitize(Path p) {
if (p == null)
return null;
final Path path = p.normalize();
if (p.isAbsolute()) {
if (getAppDir() != null && path.startsWith(getAppDir()))
return path;
if (path.startsWith(getJavaHome()) || path.startsWith(Paths.get(System.getProperty(PROP_JAVA_HOME))))
return path;
} else if (!path.startsWith(".."))
return path;
throw new IllegalArgumentException("Path " + p + " is not local to app cache " + getAppDir());
}
private String sanitize(String p) {
if (isDependency(p))
return p;
if (p.contains("/") || p.contains(FILE_SEPARATOR)) {
p = toNativePath(p);
sanitize(Paths.get(p));
}
return p;
}
private static Path toFriendlyPath(Path p) {
if (p.isAbsolute()) {
Path rel = p.getFileSystem().getPath("").toAbsolutePath().relativize(p);
if (rel.normalize().equals(rel))
return rel;
}
return p;
}
/**
* Returns a path to a file or directory moved from {@code fromDir} to {@code toDir}.
* This method does not actually moves any files in the filesystem.
*
* @param what the path to move; must start with {@code fromDir}
* @param fromDir the directory containing {@code what}
* @param toDir the directory {@code what} is moved to
* @return the moved path, which will start with {@code toDir}.
*/
protected static Path move(Path what, Path fromDir, Path toDir) {
if (!what.startsWith(fromDir))
throw new IllegalArgumentException(what + " is not under " + fromDir);
return toDir.resolve(fromDir.relativize(what));
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="OS">
/////////// OS ///////////////////////////////////
/**
* Tests whether the current OS is Windows.
*/
@SuppressWarnings("StringEquality")
protected static final boolean isWindows() {
return PLATFORM == OS_WINDOWS;
}
/**
* Tests whether the current OS is MacOS.
*/
@SuppressWarnings("StringEquality")
protected static final boolean isMac() {
return PLATFORM == OS_MACOS;
}
/**
* Tests whether the current OS is UNIX/Linux.
*/
@SuppressWarnings("StringEquality")
protected static final boolean isUnix() {
return PLATFORM == OS_LINUX || PLATFORM == OS_SOLARIS || PLATFORM == OS_BSD
|| PLATFORM == OS_AIX || PLATFORM == OS_HP_UX;
}
private static String getOS() {
if (OS.startsWith("windows"))
return OS_WINDOWS;
if (OS.startsWith("mac"))
return OS_MACOS;
if (OS.contains("linux"))
return OS_LINUX;
if (OS.contains("solaris") || OS.contains("sunos") || OS.contains("illumos"))
return OS_SOLARIS;
if (OS.contains("bsd"))
return OS_BSD;
if (OS.contains("aix"))
return OS_AIX;
if (OS.contains("hp-ux"))
return OS_HP_UX;
if (OS.contains("vms"))
return OS_VMS;
log(LOG_QUIET, "WARNING Unrecognized OS: " + System.getProperty(PROP_OS_NAME));
return null;
}
/**
* The suffix of a native library on this OS.
*/
protected static final String getNativeLibExtension() {
if (isWindows())
return "dll";
if (isMac())
return "dylib";
if (isUnix())
return "so";
throw new RuntimeException("Unsupported operating system: " + System.getProperty(PROP_OS_NAME));
}
private static long getMaxCommandLineLength() {
if (isWindows())
return WINDOWS_MAX_CMD;
return Long.MAX_VALUE;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="JAR Utils">
/////////// JAR Utils ///////////////////////////////////
private static JarInputStream openJarInputStream(Path jar) throws IOException {
return new JarInputStream(skipToZipStart(Files.newInputStream(jar), null));
}
private static JarInputStream copyJarPrefix(InputStream is, OutputStream os) throws IOException {
return new JarInputStream(skipToZipStart(is, null));
}
protected static InputStream getEntryInputStream(Path jar, String name) throws IOException {
return getEntry(openJarInputStream(jar), name);
}
private static InputStream getEntry(ZipInputStream zis, String name) throws IOException {
for (ZipEntry entry; (entry = zis.getNextEntry()) != null;) {
if (entry.getName().equals(name))
return zis;
}
return null;
}
private static String getMainClass(Path jar) {
return getMainClass(getManifest(jar));
}
private static String getMainClass(Manifest manifest) {
if (manifest == null)
return null;
return manifest.getMainAttributes().getValue(ATTR_MAIN_CLASS);
}
private static Manifest getManifest(Path jar) {
try (JarInputStream jis = openJarInputStream(jar)) {
return jis.getManifest();
} catch (IOException e) {
throw new RuntimeException("Error reading manifest from " + jar, e);
}
}
private static Map<Path, List<String>> jarEntriesCache = new HashMap<>();
private static Iterable<String> cachedEntries(Path jar) throws IOException {
final Map<Path, List<String>> cache = jarEntriesCache;
if (cache != null && cache.containsKey(jar))
return cache.get(jar);
final List<String> entries = new ArrayList<>();
try (ZipInputStream zis = openJarInputStream(jar)) {
for (ZipEntry entry; (entry = zis.getNextEntry()) != null;)
entries.add(entry.getName());
}
if (cache != null)
cache.put(jar, entries);
return entries;
}
private static final int[] ZIP_HEADER = new int[]{'P', 'K', 0x03, 0x04};
private static InputStream skipToZipStart(InputStream is, OutputStream os) throws IOException {
if (!is.markSupported())
is = new BufferedInputStream(is);
int state = 0;
for (;;) {
if (state == 0)
is.mark(ZIP_HEADER.length);
final int b = is.read();
if (b < 0)
throw new IllegalArgumentException("Not a JAR/ZIP file");
if (state >= 0 && b == ZIP_HEADER[state]) {
state++;
if (state == ZIP_HEADER.length)
break;
} else {
state = -1;
if (b == '\n' || b == 0) // start matching on \n and \0
state = 0;
}
if (os != null)
os.write(b);
}
is.reset();
return is;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="File Utils">
/////////// File Utils ///////////////////////////////////
private static void writeFile(Path targetDir, String fileName, InputStream is) throws IOException {
fileName = toNativePath(fileName);
final String dir = getDirectory(fileName);
if (dir != null)
Files.createDirectories(targetDir.resolve(dir));
final Path targetFile = targetDir.resolve(fileName);
Files.copy(is, targetFile);
}
private static String toNativePath(String filename) {
if (filename.contains("://")) // detect URL
return filename;
final char ps = (!filename.contains("/") && filename.contains("\\")) ? '\\' : '/';
return ps != FILE_SEPARATOR_CHAR ? filename.replace(ps, FILE_SEPARATOR_CHAR) : filename;
}
private static String getDirectory(String filename) {
final int index = filename.lastIndexOf(FILE_SEPARATOR_CHAR);
if (index < 0)
return null;
return filename.substring(0, index);
}
/**
* Deletes the given file or directory (even if nonempty).
*/
static void delete(Path path) throws IOException {
log(LOG_DEBUG, "Deleting " + path);
if (!Files.exists(path))
return;
if (Files.isDirectory(path)) {
try (DirectoryStream<Path> ds = Files.newDirectoryStream(path)) {
for (Path f : ds)
delete(f);
}
}
Files.delete(path);
}
/**
* Copies the source file or directory (recursively) to the target location.
*/
static void copy(Path source, Path target) throws IOException {
Files.copy(source, target, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
if (Files.isDirectory(source)) {
try (DirectoryStream<Path> ds = Files.newDirectoryStream(source)) {
for (Path f : ds)
copy(f, target.resolve(f.getFileName()));
}
}
}
private static Path ensureExecutable(Path file) {
if (!Files.isExecutable(file)) {
try {
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(file);
if (!perms.contains(PosixFilePermission.OWNER_EXECUTE)) {
Set<PosixFilePermission> newPerms = EnumSet.copyOf(perms);
newPerms.add(PosixFilePermission.OWNER_EXECUTE);
Files.setPosixFilePermissions(file, newPerms);
}
} catch (UnsupportedOperationException e) {
} catch (IOException e) {
throw rethrow(e);
}
}
return file;
}
/**
* Copies the input stream to the output stream.
* Neither stream is closed when the method returns.
*/
static void copy(InputStream is, OutputStream out) throws IOException {
final byte[] buffer = new byte[1024];
for (int bytesRead; (bytesRead = is.read(buffer)) != -1;)
out.write(buffer, 0, bytesRead);
out.flush();
}
private static Path getTempDir() {
try {
return Paths.get(getProperty(PROP_TMP_DIR));
} catch (Exception e) {
return null;
}
}
private static Path getExistingAncestor(Path p) {
p = p.toAbsolutePath().getParent();
while (p != null && !Files.exists(p))
p = p.getParent();
return p;
}
/**
* Returns the permissions of the given file or directory.
*/
protected static FileAttribute<?>[] getPermissions(Path p) throws IOException {
final List<FileAttribute> attrs = new ArrayList<>();
final PosixFileAttributeView posix = Files.getFileAttributeView(p, PosixFileAttributeView.class);
if (posix != null)
attrs.add(PosixFilePermissions.asFileAttribute(posix.readAttributes().permissions()));
return attrs.toArray(new FileAttribute[attrs.size()]);
}
private static boolean isGlob(String s) {
return s.contains("*") || s.contains("?") || s.contains("{") || s.contains("[");
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="JRE Installations">
/////////// JRE Installations ///////////////////////////////////
private static boolean isJDK(Path javaHome) {
final String name = javaHome.toString().toLowerCase();
return !name.contains("jre") && (name.contains("jdk") || Files.exists(javaHome.resolve("include").resolve("jni.h")));
}
/**
* Returns all found Java installations.
*
* @return a map from installations' versions to their respective (possibly multiple) paths
*/
protected static Map<String, List<Path>> getJavaHomes() {
if (JAVA_HOMES == null) {
try {
Path homesDir = null;
for (Path d = Paths.get(getProperty(PROP_JAVA_HOME)); d != null; d = d.getParent()) {
if (d.getFileName() != null && isJavaDir(d.getFileName().toString()) != null) {
homesDir = d.getParent();
break;
}
}
Map<String, List<Path>> homes = getJavaHomes(homesDir);
if (homes != null && isWindows())
homes = windowsJavaHomesHeuristics(homesDir, homes);
JAVA_HOMES = homes;
} catch (IOException e) {
throw rethrow(e);
}
}
return JAVA_HOMES;
}
private static Map<String, List<Path>> windowsJavaHomesHeuristics(Path dir, Map<String, List<Path>> homes) throws IOException {
Path dir2 = null;
if (dir.startsWith(WINDOWS_PROGRAM_FILES_1))
dir2 = WINDOWS_PROGRAM_FILES_2.resolve(WINDOWS_PROGRAM_FILES_1.relativize(dir));
else if (dir.startsWith(WINDOWS_PROGRAM_FILES_2))
dir2 = WINDOWS_PROGRAM_FILES_1.resolve(WINDOWS_PROGRAM_FILES_2.relativize(dir));
if (dir2 != null) {
Map<String, List<Path>> allHomes = new HashMap<>(nullToEmpty(homes));
multiputAll(allHomes, nullToEmpty(getJavaHomes(dir2)));
return allHomes;
} else
return homes;
}
private static Map<String, List<Path>> getJavaHomes(Path dir) throws IOException {
if (dir == null || !Files.isDirectory(dir))
return null;
final Map<String, List<Path>> dirs = new HashMap<>();
try (DirectoryStream<Path> fs = Files.newDirectoryStream(dir)) {
for (Path f : fs) {
String ver;
List<Path> homes;
if (Files.isDirectory(f) && (ver = isJavaDir(f.getFileName().toString())) != null
&& (homes = searchJavaHomeInDir(f)) != null && homes.size() > 0) {
if (parseJavaVersion(ver)[3] == 0)
ver = getActualJavaVersion(first(homes));
multiput(dirs, ver, homes);
}
}
}
return dirs;
}
private static String getJavaVersion(Path home) {
if (home == null)
return null;
String ver;
for (Path f = home; f != null && f.getNameCount() > 0; f = f.getParent()) {
ver = isJavaDir(f.getFileName().toString());
if (ver != null)
return ver;
}
return getActualJavaVersion(home);
}
// visible for testing
static String isJavaDir(String fileName) {
/*
* This method considers some well-known Java home directory naming schemes.
* It will likely require changes to accomodate other schemes used by various package managers.
*/
fileName = fileName.toLowerCase();
if ((fileName.startsWith("java-") || fileName.startsWith("jdk-") || fileName.startsWith("jre-"))
&& (fileName.contains("-openjdk") || fileName.contains("-oracle"))) {
final Matcher m = Pattern.compile("^[^\\-]+-([0-9\\.]+)-").matcher(fileName);
if (!m.find())
throw new RuntimeException("Cannot parse Java directory name: " + fileName);
return shortJavaVersion(m.group(1));
} else if (fileName.startsWith("jdk-") && (fileName.contains("-openjdk") || fileName.contains("-oracle"))) {
final Matcher m = Pattern.compile("java-([0-9]+)-").matcher(fileName);
m.find();
return shortJavaVersion(m.group(1));
} else if (fileName.startsWith("jdk") || fileName.startsWith("jre") || fileName.endsWith(".jdk") || fileName.endsWith(".jre")) {
if (fileName.startsWith("jdk-") || fileName.startsWith("jre-"))
fileName = fileName.substring(4);
else if (fileName.startsWith("jdk") || fileName.startsWith("jre"))
fileName = fileName.substring(3);
if (fileName.endsWith(".jdk") || fileName.endsWith(".jre"))
fileName = fileName.substring(0, fileName.length() - 4);
return shortJavaVersion(fileName);
} else
return null;
}
private static List<Path> searchJavaHomeInDir(Path dir) throws IOException {
final List<Path> homes = new ArrayList<>();
final boolean jdk = isJDK(dir);
if (isJavaHome(dir))
homes.add(dir.toAbsolutePath());
try (DirectoryStream<Path> fs = Files.newDirectoryStream(dir)) {
for (Path f : fs) {
if (Files.isDirectory(f)) {
if (isJavaHome(f))
homes.add(f.toAbsolutePath());
if (homes.size() >= 2 || (homes.size() >= 1 && !(jdk || isJDK(f))))
break;
final List<Path> rec = searchJavaHomeInDir(f);
if (rec != null)
homes.addAll(rec);
}
}
}
return homes;
}
private static boolean isJavaHome(Path dir) {
return Files.isRegularFile(dir.resolve("bin").resolve("java" + (isWindows() ? ".exe" : "")));
}
private static Path getJavaExecutable0(Path javaHome) {
final String exec = (isWindows() && System.console() == null) ? "javaw" : "java";
return javaHome.resolve("bin").resolve(exec + (isWindows() ? ".exe" : ""));
}
private static final Pattern PAT_JAVA_VERSION_LINE = Pattern.compile(".*?\"(.+?)\"");
private static String getActualJavaVersion(Path javaHome) {
try {
final String versionLine = first(exec(1, true, new ProcessBuilder(asList(getJavaExecutable0(javaHome).toString(), "-version"))));
final Matcher m = PAT_JAVA_VERSION_LINE.matcher(versionLine);
if (!m.matches())
throw new IllegalArgumentException("Could not parse version line: " + versionLine);
final String version = m.group(1);
return version;
} catch (Exception e) {
throw rethrow(e);
}
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Version Strings">
/////////// Version Strings ///////////////////////////////////
// visible for testing
static String shortJavaVersion(String v) {
try {
final String[] vs = v.split(SEPARATOR_DOT);
if (vs.length == 1) {
if (Integer.parseInt(vs[0]) < 5)
throw new RuntimeException("Unrecognized major Java version: " + v);
v = "1." + v + ".0";
}
if (vs.length == 2)
v += ".0";
return v;
} catch (NumberFormatException e) {
return null;
}
}
private static String majorJavaVersion(String v) {
if (v == null)
return null;
final String[] vs = v.split(SEPARATOR_DOT);
if (vs.length == 1)
return vs[0];
if (vs.length >= 2)
return vs[1];
throw new AssertionError("unreachable");
}
/**
* Compares two dotted software versions, regarding only the first several version components.
*
* @param a first version
* @param b second version
* @param n the number of (most significant) components to consider
* @return {@code 0} if {@code a == b}; {@code > 0} if {@code a > b}; {@code < 0} if {@code a < b};
*/
protected static final int compareVersions(String a, String b, int n) {
return compareVersions(parseJavaVersion(a), parseJavaVersion(b), n);
}
/**
* Compares two dotted software versions.
*
* @param a first version
* @param b second version
* @return {@code 0} if {@code a == b}; {@code > 0} if {@code a > b}; {@code < 0} if {@code a < b};
*/
protected static final int compareVersions(String a, String b) {
return compareVersions(parseJavaVersion(a), parseJavaVersion(b));
}
private static int compareVersions(int[] a, int[] b) {
return compareVersions(a, b, 5);
}
private static int compareVersions(int[] a, int[] b, int n) {
for (int i = 0; i < n; i++) {
if (a[i] != b[i])
return a[i] - b[i];
}
return 0;
}
private static boolean equals(int[] a, int[] b, int n) {
for (int i = 0; i < n; i++) {
if (a[i] != b[i])
return false;
}
return true;
}
private static final Pattern PAT_JAVA_VERSION = Pattern.compile("(?<major>\\d+)(\\.(?<minor>\\d+))?(?:\\.(?<patch>\\d+))?(_(?<update>\\d+))?(-(?<pre>[^-]+))?(-(?<build>.+))?");
// visible for testing
static int[] parseJavaVersion(String v) {
final Matcher m = PAT_JAVA_VERSION.matcher(v);
if (!m.matches())
throw new IllegalArgumentException("Could not parse version: " + v);
final int[] ver = new int[5];
ver[0] = toInt(m.group("major"));
ver[1] = toInt(m.group("minor"));
ver[2] = toInt(m.group("patch"));
ver[3] = toInt(m.group("update"));
if (ver[0] > 1 && ver[1] == 0) {
ver[1] = ver[0];
ver[0] = 1;
}
final String pre = m.group("pre");
if (pre != null) {
if (pre.startsWith("rc"))
ver[4] = -1;
else if (pre.startsWith("beta"))
ver[4] = -2;
else if (pre.startsWith("ea"))
ver[4] = -3;
}
return ver;
}
// visible for testing
static String toJavaVersionString(int[] version) {
final StringBuilder sb = new StringBuilder();
sb.append(version[0]).append('.');
sb.append(version[1]).append('.');
sb.append(version[2]);
if (version.length > 3 && version[3] > 0)
sb.append('_').append(version[3]);
if (version.length > 4 && version[4] != 0) {
final String pre;
switch (version[4]) {
case -1:
pre = "rc";
break;
case -2:
pre = "beta";
break;
case -3:
pre = "ea";
break;
default:
pre = "?";
}
sb.append('-').append(pre);
}
return sb.toString();
}
private static int toInt(String s) {
return s != null ? Integer.parseInt(s) : 0;
}
private static int[] toInt(String[] ss) {
int[] res = new int[ss.length];
for (int i = 0; i < ss.length; i++)
res[i] = ss[i] != null ? Integer.parseInt(ss[i]) : 0;
return res;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="String Expansion">
/////////// String Expansion ///////////////////////////////////
private static final Pattern PAT_VAR = Pattern.compile("\\$(?:([a-zA-Z0-9_\\-]+)|(?:\\{([^\\}]*)\\}))");
private String expand(String str) {
if (str == null)
return null;
final StringBuffer sb = new StringBuffer();
final Matcher m = PAT_VAR.matcher(str);
while (m.find())
m.appendReplacement(sb, Matcher.quoteReplacement(getVarValue(xor(m.group(1), m.group(2)))));
m.appendTail(sb);
str = sb.toString();
// str = expandCommandLinePath(str);
return str;
}
/**
* Resolves {@code $VARNAME} or {@code ${VARNAME}} in attribute values.
*
* @param var the variable name
* @return the variable's value
*/
protected String getVarValue(String var) {
return (_ct = getCallTarget(Capsule.class)) != null ? _ct.getVarValue(var) : getVarValue0(var);
}
private String getVarValue0(String var) {
String value = null;
switch (var) {
case VAR_CAPSULE_DIR:
try {
value = processOutgoingPath(appDir());
break;
} catch (IllegalStateException e) {
throw new IllegalStateException("Cannot resolve variable $" + var, e);
}
case VAR_CAPSULE_APP:
if (getAppId() == null)
throw new RuntimeException("Cannot resolve variable $" + var + " in an empty capsule.");
value = getAppId();
break;
case VAR_CAPSULE_JAR:
case "0":
value = processOutgoingPath(getJarFile());
break;
case VAR_JAVA_HOME:
final String jhome = processOutgoingPath(getJavaHome());
if (jhome == null)
throw new RuntimeException("Cannot resolve variable $" + var + "; Java home not set.");
value = jhome;
break;
}
if (value == null) {
value = getProperty(var);
if (value != null)
log(LOG_DEBUG, "Resolved variable $" + var + " with a property");
}
if (value == null) {
value = getenv(var);
if (value != null)
log(LOG_DEBUG, "Resolved variable $" + var + " with an environement variable");
}
if (value == null)
throw new RuntimeException("Cannot resolve variable $" + var);
return value;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="String Utils">
/////////// String Utils ///////////////////////////////////
private static String toString(Object o) {
if (o == null)
return null;
if (o instanceof Path)
return toString((Path) o);
return o.toString();
}
private static String toString(Path p) {
String s = p.toString();
if (FILE_SEPARATOR_CHAR != '/' && isUnix())
s = s.replace(FILE_SEPARATOR_CHAR, '/');
return s;
}
private static List<String> split(String str, String separator) {
if (str == null)
return null;
final String[] es = str.split(separator);
final List<String> list = new ArrayList<>(es.length);
for (String e : es) {
e = e.trim();
if (!e.isEmpty())
list.add(e);
}
return list;
}
private static Map<String, String> split(String map, char kvSeparator, String separator, String defaultValue) {
if (map == null)
return null;
Map<String, String> m = new LinkedHashMap<>();
for (String entry : Capsule.split(map, separator)) {
final String key = getBefore(entry, kvSeparator);
String value = getAfter(entry, kvSeparator);
if (value == null) {
if (defaultValue != null)
value = defaultValue;
else
throw new IllegalArgumentException("Element " + entry + " in \"" + map + "\" is not a key-value entry separated with " + kvSeparator + " and no default value provided");
}
m.put(key.trim(), value.trim());
}
return m;
}
private static String join(Collection<?> coll, String separator) {
if (coll == null)
return null;
if (coll.isEmpty())
return "";
StringBuilder sb = new StringBuilder();
for (Object e : coll) {
if (e != null)
sb.append(toString(e)).append(separator);
}
sb.delete(sb.length() - separator.length(), sb.length());
return sb.toString();
}
private static String getBefore(String s, char separator) {
final int i = s.indexOf(separator);
if (i < 0)
return s;
return s.substring(0, i);
}
private static String getAfter(String s, char separator) {
final int i = s.indexOf(separator);
if (i < 0)
return null;
return s.substring(i + 1);
}
private static long getStringsLength(Collection<?> coll) {
if (coll == null)
return 0;
long len = 0;
for (Object o : coll)
len += o.toString().length();
return len;
}
private static String nullToEmpty(String s) {
return s != null ? s : "";
}
private static String emptyToNull(String s) {
if (s == null)
return null;
s = s.trim();
return s.isEmpty() ? null : s;
}
private static <T> T xor(T x, T y) {
assert x == null ^ y == null;
return x != null ? x : y;
}
// visible for testing
static List<String> parseCommandLineArguments(final String str) {
/*
* Modified from Ant's CommandLine parser
* http://grepcode.com/file/repo1.maven.org/maven2/org.apache.ant/ant/1.9.5/org/apache/tools/ant/types/Commandline.java#Commandline.translateCommandline%28java.lang.String%29
*/
if (str == null || str.length() == 0)
return emptyList();
final int NORMAL = 0,
IN_QUOTE = 1,
IN_DOUBLE_QUOTE = 2;
final StringTokenizer tok = new StringTokenizer(str, "\"\'\\ ", true);
final ArrayList<String> result = new ArrayList<>();
final StringBuilder current = new StringBuilder();
int state = NORMAL;
boolean lastTokenHasBeenQuoted = false;
while (tok.hasMoreTokens()) {
final String nextTok = tok.nextToken();
switch (state) {
case IN_QUOTE:
if ("\'".equals(nextTok)) {
lastTokenHasBeenQuoted = true;
state = NORMAL;
} else
current.append(nextTok);
break;
case IN_DOUBLE_QUOTE:
if ("\"".equals(nextTok)) {
lastTokenHasBeenQuoted = true;
state = NORMAL;
} else
current.append(nextTok);
break;
default:
if ("\'".equals(nextTok))
state = IN_QUOTE;
else if ("\"".equals(nextTok))
state = IN_DOUBLE_QUOTE;
else if (" ".equals(nextTok)) {
if (lastTokenHasBeenQuoted || current.length() != 0) {
result.add(current.toString());
current.setLength(0);
}
} else
current.append(nextTok);
lastTokenHasBeenQuoted = false;
break;
}
}
if (lastTokenHasBeenQuoted || current.length() != 0)
result.add(current.toString());
if (state == IN_QUOTE || state == IN_DOUBLE_QUOTE)
throw new IllegalArgumentException("unbalanced quotes in " + str);
return result;
}
@SuppressWarnings("fallthrough")
static String globToRegex(String pattern) {
// Based on Neil Traft: http://stackoverflow.com/a/17369948/750563
final String DOT = "[^/]"; // "." -- exclude slashes
final StringBuilder sb = new StringBuilder(pattern.length());
int inGroup = 0;
int inClass = 0;
int firstIndexInClass = -1;
char[] arr = pattern.toCharArray();
for (int i = 0; i < arr.length; i++) {
char ch = arr[i];
switch (ch) {
case '\\':
if (++i >= arr.length) {
sb.append('\\');
} else {
char next = arr[i];
switch (next) {
case ',':
// escape not needed
break;
case 'Q':
case 'E':
// extra escape needed
sb.append('\\');
default:
sb.append('\\');
}
sb.append(next);
}
break;
case '*':
if (inClass == 0)
sb.append(DOT + "*");
else
sb.append('*');
break;
case '?':
if (inClass == 0)
sb.append(DOT);
else
sb.append('?');
break;
case '[':
inClass++;
firstIndexInClass = i + 1;
sb.append('[');
break;
case ']':
inClass--;
sb.append(']');
break;
case '.':
case '(':
case ')':
case '+':
case '|':
case '^':
case '$':
case '@':
case '%':
if (inClass == 0 || (firstIndexInClass == i && ch == '^'))
sb.append('\\');
sb.append(ch);
break;
case '!':
if (firstIndexInClass == i)
sb.append('^');
else
sb.append('!');
break;
case '{':
inGroup++;
sb.append('(');
break;
case '}':
inGroup--;
sb.append(')');
break;
case ',':
if (inGroup > 0)
sb.append('|');
else
sb.append(',');
break;
default:
sb.append(ch);
}
}
return sb.toString();
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Collection Utils">
/////////// Collection Utils ///////////////////////////////////
@SuppressWarnings("unchecked")
private static <T> List<T> nullToEmpty(List<T> list) {
return list != null ? list : (List<T>) emptyList();
}
@SuppressWarnings("unchecked")
private static <K, V> Map<K, V> nullToEmpty(Map<K, V> map) {
return map != null ? map : (Map<K, V>) emptyMap();
}
private static <T> List<T> emptyToNull(List<T> list) {
return (list != null && !list.isEmpty()) ? list : null;
}
private static <K, V> Map<K, V> emptyToNull(Map<K, V> map) {
return (map != null && !map.isEmpty()) ? map : null;
}
// private static <K, V> Map<K, List<V>> multiput(Map<K, List<V>> map, K key, V value) {
// List<V> list = map.get(key);
// if (list == null) {
// list = new ArrayList<>();
// map.put(key, list);
// }
// list.add(value);
// return map;
// }
//
private static <K, V> Map<K, List<V>> multiput(Map<K, List<V>> map, K key, List<V> values) {
if (values == null)
return map;
List<V> list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
list.addAll(values);
return map;
}
private static <K, V> Map<K, List<V>> multiputAll(Map<K, List<V>> map, Map<K, List<V>> map2) {
for (Map.Entry<K, List<V>> entry : map2.entrySet()) {
List<V> list = map.get(entry.getKey());
if (list == null) {
list = new ArrayList<>();
map.put(entry.getKey(), list);
}
list.addAll(entry.getValue());
}
return map;
}
private static boolean isEmpty(Iterable<?> c) {
if (c instanceof Collection)
return ((Collection<?>) c).isEmpty();
final Iterator<?> it = c.iterator();
return !it.hasNext();
}
private static boolean isDeepEmpty(Object o) {
if (o == null)
return true;
else if ("".equals(o))
return true;
else if (o instanceof Path)
return false; // special case: a path is an iterable that may contain itself
else if (o instanceof Iterable) {
for (Object x : (Iterable<?>) o) {
if (!isDeepEmpty(x))
return false;
}
return true;
} else
return false;
}
private static <T> T first(Iterable<T> c) {
if (c == null || isEmpty(c))
throw new IllegalArgumentException("Not found");
return (c instanceof List && c instanceof RandomAccess) ? ((List<T>) c).get(0) : c.iterator().next();
}
private static <T> T firstOrNull(Iterable<T> c) {
if (c == null || isEmpty(c))
return null;
return (c instanceof List && c instanceof RandomAccess) ? ((List<T>) c).get(0) : c.iterator().next();
}
private static <T> T only(List<T> c) {
if (c == null || c.isEmpty())
throw new IllegalArgumentException("Not found");
if (c.size() > 1)
throw new IllegalArgumentException("Expected a single element but found " + c.size() + ": " + c);
return c.get(0);
}
private static <C extends Collection<T>, T> C addAll(C c, Collection<T> c1) {
if (c1 != null)
c.addAll(c1);
return c;
}
private static <C extends Collection<? super T>, T> C addAllIfAbsent(C c, Collection<T> c1) {
if (c1 != null) {
for (T e : c1) {
if (!c.contains(e))
c.add(e);
}
}
return c;
}
private static <M extends Map<K, V>, K, V> M putAllIfAbsent(M m, Map<K, V> m1) {
for (Map.Entry<K, V> entry : m1.entrySet()) {
if (!m.containsKey(entry.getKey()))
m.put(entry.getKey(), entry.getValue());
}
return m;
}
private static <K, V> Entry<K, V> entry(K k, V v) {
return new AbstractMap.SimpleImmutableEntry<K, V>(k, v);
}
private static <K, V> Entry<K, V> mutableEntry(K k, V v) {
return new AbstractMap.SimpleEntry<K, V>(k, v);
}
@SafeVarargs
private static <T> Set<T> immutableSet(T... elems) {
return unmodifiableSet(new HashSet<T>(asList(elems)));
}
private static boolean isEmpty(Object x) {
if (x == null)
return true;
if (x instanceof String)
return ((String) x).isEmpty();
if (x instanceof Collection)
return ((Collection) x).isEmpty();
if (x instanceof Map)
return ((Map) x).isEmpty();
return false;
}
@SuppressWarnings("unchecked")
private static <T> T merge(T v1, T v2) {
if (v2 == null)
return v1;
if (v1 instanceof Collection) {
final Collection<Object> c1 = (Collection<Object>) v1;
final Collection<Object> c2 = (Collection<Object>) v2;
final Collection<Object> cm;
if (v1 instanceof List)
cm = new ArrayList<>(c1.size() + c2.size());
else if (v1 instanceof Set)
cm = new HashSet<>(c1.size() + c2.size());
else
throw new RuntimeException("Unhandled type: " + v1.getClass().getName());
cm.addAll(c1);
addAllIfAbsent(cm, c2);
return (T) cm;
} else if (v1 instanceof Map) {
final Map<Object, Object> mm = new HashMap<>();
mm.putAll((Map<Object, Object>) v1);
mm.putAll((Map<Object, Object>) v2);
return (T) mm;
} else
return v2;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Reflection Utils">
/////////// Reflection Utils ///////////////////////////////////
private static Method getMethod(Capsule capsule, String name, Class<?>... parameterTypes) throws NoSuchMethodException {
for (Capsule c = capsule.cc; c != null; c = c.sup) {
try {
return getMethod(c.getClass(), name, parameterTypes);
} catch (NoSuchMethodException e) {
}
}
throw new NoSuchMethodException(name + "(" + Arrays.toString(parameterTypes) + ")");
}
private static Method getMethod(Class<?> clazz, String name, Class<?>... parameterTypes) throws NoSuchMethodException {
try {
return accessible(clazz.getDeclaredMethod(name, parameterTypes));
} catch (NoSuchMethodException e) {
if (clazz.getSuperclass() == null)
throw new NoSuchMethodException(name + "(" + Arrays.toString(parameterTypes) + ")");
return getMethod(clazz.getSuperclass(), name, parameterTypes);
}
}
private static Method getMethod(Class<?> clazz, Method method) {
if (clazz.equals(method.getDeclaringClass()))
return method;
try {
return getMethod(clazz, method.getName(), method.getParameterTypes());
} catch (NoSuchMethodException e) {
return null;
}
}
private static <T extends AccessibleObject> T accessible(T obj) {
if (obj == null)
return null;
obj.setAccessible(true);
return obj;
}
private static ClassLoader newClassLoader0(ClassLoader parent, List<Path> ps) {
try {
final List<URL> urls = new ArrayList<>(ps.size());
for (Path p : ps)
urls.add(p.toUri().toURL());
return new URLClassLoader(urls.toArray(new URL[urls.size()]), parent);
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
}
private static ClassLoader newClassLoader0(ClassLoader parent, Path... ps) {
return newClassLoader0(parent, asList(ps));
}
/**
* For internal use; subject to change/removal.
*
* @deprecated exclude from javadocs
*/
ClassLoader newClassLoader(ClassLoader parent, List<Path> ps) {
return newClassLoader0(parent, ps);
}
private ClassLoader newClassLoader(ClassLoader parent, Path... ps) {
return newClassLoader(parent, asList(ps));
}
private static boolean isStream(String className) {
return className.startsWith("java.util.stream") || className.contains("$$Lambda") || className.contains("Spliterator");
}
private static boolean isThrows(Method method, Throwable t) {
for (Class<?> etype : method.getExceptionTypes()) {
if (etype.isInstance(t))
return true;
}
return false;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Misc Utils">
/////////// Misc Utils ///////////////////////////////////
private static String propertyOrEnv(String propName, String envVar) {
String val = getProperty(propName);
if (val == null)
val = emptyToNull(getenv(envVar));
return val;
}
/**
* Returns a system property - should be used instead of {@link System#getProperty(java.lang.String) System.getProperty(propName)}.
*/
protected static final String getProperty(String propName) {
final String val = getProperty0(propName);
setContext("system property", propName, val);
return val;
}
private static String getProperty0(String propName) {
return propName != null ? PROPERTIES.getProperty(propName) : null;
}
/**
* Sets a system property.
*/
protected static final void setProperty(String propName, String value) {
PROPERTIES.setProperty(propName, value);
}
/**
* Returns the value of an environment variable - should be used instead of {@link System#getenv(java.lang.String) System.getenv(envName)}.
*/
protected static String getenv(String envName) {
final String val = envName != null ? System.getenv(envName) : null;
setContext("environment variable", envName, val);
return val;
}
private static boolean systemPropertyEmptyOrTrue(String property) {
return emptyOrTrue(getProperty0(property));
}
private static boolean emptyOrTrue(String value) {
if (value == null)
return false;
return value.isEmpty() || Boolean.parseBoolean(value);
}
private static boolean systemPropertyEmptyOrNotFalse(String property) {
final String value = getProperty0(property);
if (value == null)
return false;
return value.isEmpty() || !"false".equalsIgnoreCase(value);
}
private static Throwable deshadow(Throwable t) {
return deshadow("capsule", t);
}
private static Throwable deshadow(String prefix, Throwable t) {
prefix = prefix.endsWith(".") ? prefix : prefix + ".";
final StackTraceElement[] st = t.getStackTrace();
for (int i = 0; i < st.length; i++) {
String className = st[i].getClassName();
className = (className != null && className.startsWith(prefix) && className.lastIndexOf('.') > prefix.length())
? className.substring(prefix.length()) : className;
st[i] = new StackTraceElement(className, st[i].getMethodName(), st[i].getFileName(), st[i].getLineNumber());
}
t.setStackTrace(st);
if (t.getCause() != null)
deshadow(prefix, t.getCause());
return t;
}
private static RuntimeException rethrow(Throwable t) {
while (t instanceof InvocationTargetException)
t = ((InvocationTargetException) t).getTargetException();
if (t instanceof RuntimeException)
throw (RuntimeException) t;
if (t instanceof Error)
throw (Error) t;
throw new RuntimeException(t);
}
private static void close(Object c) {
try {
((AutoCloseable) c).close();
} catch (Exception ex) {
}
}
private static boolean isAlive(Process p) {
// return p.isAlive() // JDK8
try {
p.exitValue();
return true;
} catch (IllegalThreadStateException e) {
return false;
}
}
private static boolean waitFor(Process p, long millis) throws InterruptedException {
// return p.waitFor(millis, TimeUnit.MILLISECONDS); // JDK 8
final long deadline = System.nanoTime() + millis * 1_000_000;
for (;;) {
if (!isAlive(p))
return true;
long sleep = Math.min((deadline - System.nanoTime()) / 1_000_000, 20);
if (sleep <= 0)
return false;
Thread.sleep(sleep);
}
}
/**
* Executes a command and returns its output as a list of lines.
* The method will wait for the child process to terminate, and throw an exception if the command returns an exit value {@code != 0}.
* <br>Same as calling {@code exec(-1, cmd}}.
*
* @param cmd the command
* @return the lines output by the command
*/
protected static Iterable<String> exec(String... cmd) throws IOException {
return exec(-1, cmd);
}
/**
* Executes a command and returns its output as a list of lines.
* If the number of lines read is less than {@code numLines}, or if {@code numLines < 0}, then the method will wait for the child process
* to terminate, and throw an exception if the command returns an exit value {@code != 0}.
*
* @param numLines the maximum number of lines to read, or {@code -1} for an unbounded number
* @param cmd the command
* @return the lines output by the command
*/
protected static Iterable<String> exec(int numLines, String... cmd) throws IOException {
return exec(numLines, new ProcessBuilder(asList(cmd)));
}
/**
* Executes a command and returns its output as a list of lines.
* The method will wait for the child process to terminate, and throw an exception if the command returns an exit value {@code != 0}.
* <br>Same as calling {@code exec(-1, pb}}.
*
* @param pb the {@link ProcessBuilder} that will be used to launch the command
* @return the lines output by the command
*/
protected static Iterable<String> exec(ProcessBuilder pb) throws IOException {
return exec(-1, pb);
}
/**
* Executes a command and returns its output as a list of lines.
* If the number of lines read is less than {@code numLines}, or if {@code numLines < 0}, then the method will wait for the child process
* to terminate, and throw an exception if the command returns an exit value {@code != 0}.
*
* @param numLines the maximum number of lines to read, or {@code -1} for an unbounded number
* @param pb the {@link ProcessBuilder} that will be used to launch the command
* @return the lines output by the command
*/
protected static Iterable<String> exec(int numLines, ProcessBuilder pb) throws IOException {
return exec(numLines, false, pb);
}
private static Iterable<String> exec(int numLines, boolean error, ProcessBuilder pb) throws IOException {
final List<String> lines = new ArrayList<>();
final Process p = pb.start();
final InputStream in = error ? p.getErrorStream() : p.getInputStream();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, Charset.defaultCharset()))) {
for (int i = 0; numLines < 0 || i < numLines; i++) {
final String line = reader.readLine();
if (line == null)
break;
lines.add(line);
}
}
try {
if (numLines < 0 || lines.size() < numLines) {
final int exitValue = p.waitFor();
if (exitValue != 0)
throw new RuntimeException("Command '" + join(pb.command(), " ") + "' has returned " + exitValue);
}
return lines;
} catch (InterruptedException e) {
throw rethrow(e);
}
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Logging">
/////////// Logging ///////////////////////////////////
private static void setLogLevel(int level) {
LOG_LEVEL = level;
}
/**
* Capsule's log level
*/
protected static final int getLogLevel() {
final Integer level = LOG_LEVEL;
return level != null ? level : LOG_NONE;
}
/**
* Chooses and returns the capsules log level.
*/
protected int chooseLogLevel() {
return (_ct = getCallTarget(Capsule.class)) != null ? _ct.chooseLogLevel() : chooseLogLevel0();
}
private int chooseLogLevel0() {
String level = getProperty(PROP_LOG_LEVEL);
if (level == null && oc.manifest != null)
level = getAttribute(ATTR_LOG_LEVEL);
return getLogLevel(level);
}
private static int getLogLevel(String level) {
try {
int l = Integer.parseInt(level);
if (l < 0)
throw new IllegalArgumentException("Unrecognized log level: " + level);
return l;
} catch (NumberFormatException e) {
}
if (level == null || level.isEmpty())
level = "QUIET";
switch (level.toUpperCase()) {
case "NONE":
return LOG_NONE;
case "QUIET":
return LOG_QUIET;
case "VERBOSE":
return LOG_VERBOSE;
case "DEBUG":
case "ALL":
return LOG_DEBUG;
default:
throw new IllegalArgumentException("Unrecognized log level: " + level);
}
}
/**
* Tests if the given log level is currently being logged.
*/
protected static final boolean isLogging(int level) {
return level <= getLogLevel();
}
/**
* Prints a message to stderr if the given log-level is being logged.
*/
protected static final void log(int level, String str) {
if (isLogging(level))
STDERR.println((AGENT ? LOG_AGENT_PREFIX : LOG_PREFIX) + str);
}
/**
* Prints the given exception's stack-trace to stderr if the given log-level is being logged.
*/
protected static final void log(int level, Throwable t) {
if (t != null && isLogging(level))
t.printStackTrace(STDERR);
}
private static void println(String str) {
log(LOG_QUIET, str);
}
private static boolean hasContext() {
return contextType_ != null;
}
private static void clearContext() {
setContext(null, null, null);
}
private static void setContext(String type, String key, Object value) {
// STDERR.println("setContext: " + type + " " + key + " " + value);
// Thread.dumpStack();
if (contextType_ == null)
return;
contextType_.set(type);
contextKey_.set(key);
contextValue_.set(value != null ? value.toString() : null);
}
private static String getContext() {
return contextType_.get() + " " + contextKey_.get() + ": " + contextValue_.get();
}
private static long clock() {
return isLogging(PROFILE) ? System.nanoTime() : 0;
}
private static void time(String op, long start) {
time(op, start, isLogging(PROFILE) ? System.nanoTime() : 0);
}
private static void time(String op, long start, long stop) {
if (isLogging(PROFILE))
log(PROFILE, "PROFILE " + op + " " + ((stop - start) / 1_000_000) + "ms");
}
/**
* Called when an unhandled exception is thrown, to display error information to the user before shutting down.
*/
protected void onError(Throwable t) {
if ((_ct = getCallTarget(Capsule.class)) != null)
_ct.onError(t);
else
onError0(t);
}
private void onError0(Throwable t) {
printError(t, this);
}
/**
* Prints a value to {@code System.err} and returns it.
* Useful for debugging
*
* @param label a prefix label
* @param x the value to trace
* @return {@code x}
*/
protected static <T> T trace(String label, T x) {
final String s;
if (x == null)
s = "null";
else if (x.getClass().isArray()) {
if (!x.getClass().getComponentType().isPrimitive())
s = Arrays.deepToString((Object[]) x);
else if (x.getClass().getComponentType().equals(boolean.class))
s = Arrays.toString((boolean[]) x);
else if (x.getClass().getComponentType().equals(char.class))
s = Arrays.toString((char[]) x);
else if (x.getClass().getComponentType().equals(byte.class))
s = Arrays.toString((byte[]) x);
else if (x.getClass().getComponentType().equals(short.class))
s = Arrays.toString((short[]) x);
else if (x.getClass().getComponentType().equals(int.class))
s = Arrays.toString((int[]) x);
else if (x.getClass().getComponentType().equals(long.class))
s = Arrays.toString((long[]) x);
else if (x.getClass().getComponentType().equals(float.class))
s = Arrays.toString((float[]) x);
else if (x.getClass().getComponentType().equals(double.class))
s = Arrays.toString((double[]) x);
else
throw new AssertionError();
} else
s = x.toString();
System.err.println("TRACE " + label + ": " + s);
return x;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Windows">
/////////// Windows ///////////////////////////////////
@SuppressWarnings("unchecked")
private <T> T windowsAttributes(Entry<String, T> attr, T value) {
if (!isWindows())
return value;
if (ATTR_AGENT == attr) {
return (T) Boolean.TRUE;
}
return value;
}
//<editor-fold defaultstate="collapsed" desc="Long Classpath - Pathing JAR">
/////////// Long Classpath - Pathing JAR ///////////////////////////////////
private List<Path> handleLongClasspath(List<Path> cp, int extra, List<?>... args) {
if (!isWindows())
return cp; // why work hard if we know the problem only exists on Windows?
long len = extra + getStringsLength(cp) + cp.size();
for (List<?> list : args)
len += getStringsLength(list) + list.size();
if (len >= getMaxCommandLineLength()) {
log(LOG_DEBUG, "Command line length: " + len);
if (isTrampoline())
throw new RuntimeException("Command line too long and trampoline requested.");
final Path pathingJar = addTempFile(createPathingJar(getTempDir(), cp));
log(LOG_VERBOSE, "Writing classpath: " + cp + " to pathing JAR: " + pathingJar);
return singletonList(pathingJar);
} else
return cp;
}
// visible for testing
static Path createPathingJar(Path dir, List<Path> cp) {
try {
dir = dir.toAbsolutePath();
final List<String> paths = createPathingClassPath(dir, cp);
final Path pathingJar = Files.createTempFile(dir, "capsule_pathing_jar", ".jar");
final Manifest man = new Manifest();
man.getMainAttributes().putValue(ATTR_MANIFEST_VERSION, "1.0");
man.getMainAttributes().putValue(ATTR_CLASS_PATH, join(paths, " "));
new JarOutputStream(Files.newOutputStream(pathingJar), man).close();
return pathingJar;
} catch (IOException e) {
throw new RuntimeException("Pathing JAR creation failed", e);
}
}
private static List<String> createPathingClassPath(Path dir, List<Path> cp) {
boolean allPathsHaveSameRoot = true;
for (Path p : cp) {
if (!dir.getRoot().equals(p.getRoot()))
allPathsHaveSameRoot = false;
}
final List<String> paths = new ArrayList<>(cp.size());
for (Path p : cp) { // In order to use the Class-Path attribute, we must either relativize the paths, or specifiy them as file URLs
if (allPathsHaveSameRoot)
paths.add(dir.relativize(p).toString());
else
paths.add(p.toUri().toString());
}
return paths;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Pipe Streams (workaround for inheritIO bug)">
/////////// Pipe Streams (workaround for inheritIO bug) ///////////////////////////////////
private static boolean isInheritIoBug() {
return isWindows() && compareVersions(System.getProperty(PROP_JAVA_VERSION), "1.8.0") < 0;
}
private void pipeIoStreams() {
startThread("pipe-out", "pipeStreamOut");
startThread("pipe-err", "pipeStreamErr");
startThread("pipe-in", "pipeStreamIn");
}
private void pipeStreamOut() throws IOException {
pipe(child.getInputStream(), STDOUT);
}
private void pipeStreamErr() throws IOException {
pipe(child.getErrorStream(), STDERR);
}
private void pipeStreamIn() throws IOException {
pipe(System.in, child.getOutputStream());
}
private void pipe(InputStream in, OutputStream out) throws IOException {
try (OutputStream out1 = out) {
final byte[] buf = new byte[1024];
int read;
while (-1 != (read = in.read(buf))) {
out.write(buf, 0, read);
out.flush();
}
}
}
//</editor-fold>
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="POSIX">
/////////// POSIX ///////////////////////////////////
private static int getPid(Process p) {
try {
java.lang.reflect.Field pidField = p.getClass().getDeclaredField("pid");
pidField.setAccessible(true);
return pidField.getInt(p);
} catch (Exception e) {
return -1;
}
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="JMX">
/////////// JMX ///////////////////////////////////
/**
* The Capsule agent will invoke this method to start a JMX Server.
* The default implementation creates a local JMX connector.
*
* For internal use; subject to change/removal.
*
* @return The JMX service URL that the parent Capsule process will use to connect and proxy JMX commands.
*
* @deprecated marked deprecated to exclude from javadoc
*/
protected JMXServiceURL startJMXServer() {
/*
* https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/sun/management/Agent.java
* https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/sun/management/jmxremote/ConnectorBootstrap.java
*/
final String LOCAL_CONNECTOR_ADDRESS_PROP = "com.sun.management.jmxremote.localConnectorAddress";
try {
log(LOG_VERBOSE, "Starting JMXConnectorServer");
final JMXServiceURL url;
// final JMXConnectorServer jmxServer = JMXConnectorServerFactory.newJMXConnectorServer(new JMXServiceURL("rmi", null, 0), null, ManagementFactory.getPlatformMBeanServer());
// jmxServer.start(); // prevents the app from shutting down (requires jmxServer.stop()). See ConnectorBootstrap.PermanentExporter
// url = jmxServer.getAddress();
final Properties agentProps = sun.misc.VMSupport.getAgentProperties();
if (agentProps.get(LOCAL_CONNECTOR_ADDRESS_PROP) == null) {
log(LOG_VERBOSE, "Starting management agent");
sun.management.Agent.agentmain(null); // starts a JMXConnectorServer that does not prevent the app from shutting down
}
url = new JMXServiceURL((String) agentProps.get(LOCAL_CONNECTOR_ADDRESS_PROP));
log(LOG_VERBOSE, "JMXConnectorServer started JMX at " + url);
return url;
} catch (Exception e) {
log(LOG_VERBOSE, "JMXConnectorServer failed: " + e.getMessage());
log(LOG_VERBOSE, e);
return null;
}
}
private MBeanServerConnection connectToJMX(JMXServiceURL url) {
try {
log(LOG_VERBOSE, "Connecting to JMX server at: " + url);
final JMXConnector connect = JMXConnectorFactory.connect(url);
final MBeanServerConnection mbsc = connect.getMBeanServerConnection();
log(LOG_VERBOSE, "JMX Connection successful");
oc.jmxConnection = mbsc;
return mbsc;
} catch (Exception e) {
log(LOG_VERBOSE, "JMX Connection failed: " + e.getMessage());
log(LOG_VERBOSE, e);
return null;
}
}
/**
* Returns an {@link MBeanServerConnection} to the application's {@code MBeanServer}.
* This method may only be called within {@link #liftoff() }.
*/
protected final MBeanServerConnection getMBeanServerConnection() {
verifyAgent(false);
verifyAfterStage(STAGE_LAUNCH);
synchronized (oc) {
if (oc.jmxConnection == null) {
try {
send(MESSAGE_START_JMX, null);
receive();
} catch (IOException e) {
printError(LOG_QUIET, e);
}
}
return oc.jmxConnection;
}
}
private Object invokeMBeanServer(Method method, Object[] args) throws ReflectiveOperationException {
final MBeanServerConnection conn = lifecycleStage >= STAGE_LAUNCH ? getMBeanServerConnection() : null;
final MBeanServerConnection target = conn != null ? conn : origMBeanServer;
final Method m;
if ((m = getMethod(target.getClass(), method)) != null)
return m.invoke(target, args);
else if (method.getName().startsWith("getClassLoader"))
return MY_CLASSLOADER;
else
throw new UnsupportedOperationException();
}
private void overridePlatformMBeanServer() {
try {
MBeanServer platformMBeanServer = ManagementFactory.getPlatformMBeanServer();
if (platformMBeanServer instanceof com.sun.jmx.mbeanserver.JmxMBeanServer) {
final MBeanServer interceptor = (MBeanServer) Proxy.newProxyInstance(MY_CLASSLOADER, new Class<?>[]{MBeanServer.class}, this);
Field interceptorField = accessible(com.sun.jmx.mbeanserver.JmxMBeanServer.class.getDeclaredField("mbsInterceptor"));
// this.origMBeanServer = ((com.sun.jmx.mbeanserver.JmxMBeanServer) platformMBeanServer).getMBeanServerInterceptor();
this.origMBeanServer = (MBeanServer) interceptorField.get(platformMBeanServer);
// ((com.sun.jmx.mbeanserver.JmxMBeanServer) platformMBeanServer).setMBeanServerInterceptor(interceptor);
interceptorField.set(platformMBeanServer, interceptor);
}
// accessible(ManagementFactory.class.getDeclaredField("platformMBeanServer")).set(null, this);
} catch (Throwable e) { // JDK 9 throws an IllegalAccessError
log(LOG_VERBOSE, "Overriding platform MBeanServer failed: " + e.getMessage());
log(LOG_VERBOSE, e);
// throw rethrow(e);
}
}
/**
* @deprecated marked deprecated to exclude from javadoc
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
final Object res;
if (MBeanServerConnection.class.equals(method.getDeclaringClass()) || MBeanServer.class.equals(method.getDeclaringClass()))
res = invokeMBeanServer(method, args);
else
throw new UnsupportedOperationException();
if (isLogging(LOG_DEBUG))
log(LOG_DEBUG, "Invoke " + method + " with args: " + Arrays.toString(args) + " => " + res);
return res;
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
assert t instanceof RuntimeException || t instanceof Error || isThrows(method, t);
log(LOG_DEBUG, "Exception while running method " + method + " with args: " + Arrays.toString(args) + ": " + t);
log(LOG_DEBUG, t);
throw e;
} catch (Exception e) {
log(LOG_VERBOSE, "Exception while running method " + method + " with args: " + Arrays.toString(args) + ": " + e);
log(LOG_VERBOSE, e);
throw e;
}
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Object Methods">
/////////// Object Methods ///////////////////////////////////
/**
* Throws a {@link CloneNotSupportedException}
*
* @deprecated marked deprecated to exclude from javadoc
*/
@SuppressWarnings("CloneDoesntCallSuperClone")
@Override
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
/**
* @deprecated marked deprecated to exclude from javadoc
*/
@Override
public final int hashCode() {
return super.hashCode();
}
/**
* @deprecated marked deprecated to exclude from javadoc
*/
@Override
public final boolean equals(Object obj) {
return super.equals(obj);
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append(getClass().getName());
if (isLogging(LOG_DEBUG))
sb.append('@').append(Integer.toHexString(System.identityHashCode(this)));
if (cc != oc) {
sb.append('(');
for (Capsule c = cc; c != null; c = c.sup) {
sb.append(c.getClass().getName());
if (isLogging(LOG_DEBUG))
sb.append('@').append(Integer.toHexString(System.identityHashCode(c)));
sb.append(" ");
}
sb.delete(sb.length() - 1, sb.length());
sb.append(')');
}
sb.append('[');
sb.append(jarFile);
if (getAppId() != null) {
sb.append(", ").append(getAppId());
sb.append(", ").append(getAttribute(ATTR_APP_CLASS) != null ? getAttribute(ATTR_APP_CLASS) : getAttribute(ATTR_APP_ARTIFACT));
} else
sb.append(", ").append("empty");
if (getMode() != null)
sb.append(", ").append("mode: ").append(getMode());
sb.append(']');
return sb.toString();
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Capsule Loading and Launching">
/////////// Capsule Loading and Launching ///////////////////////////////////
/**
* Loads the wrapped capsule when this capsule is the wrapper.
* Caplets can override this method to provide security.
*
* @param parent the
*/
protected Capsule loadTargetCapsule(ClassLoader parent, Path jarFile) {
return (_ct = getCallTarget(Capsule.class)) != null ? _ct.loadTargetCapsule(parent, jarFile) : loadTargetCapsule0(parent, jarFile);
}
private Capsule loadTargetCapsule0(ClassLoader parent, Path jar) {
return newCapsule(newClassLoader(parent, jar), jar);
}
// visible for testing
static Capsule newCapsule(ClassLoader cl, Path jarFile) {
return (Capsule) newCapsule0(cl, jarFile);
}
private static Object newCapsule0(ClassLoader cl, Path jarFile) {
try {
final ClassLoader ccl = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(cl);
return accessible(loadCapsule(cl, jarFile).getDeclaredConstructor(Path.class)).newInstance(jarFile);
} finally {
Thread.currentThread().setContextClassLoader(ccl);
}
} catch (IncompatibleClassChangeError e) {
throw new RuntimeException("Caplet " + jarFile + " is not compatible with this capsule (" + VERSION + ")");
} catch (InvocationTargetException e) {
throw rethrow(e.getTargetException());
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Could not instantiate capsule.", e);
}
}
private Capsule newCapsule(Path jarFile, Capsule pred) {
try {
final ClassLoader ccl = Thread.currentThread().getContextClassLoader();
try {
final ClassLoader cl = newClassLoader(pred.getClass().getClassLoader(), jarFile);
Thread.currentThread().setContextClassLoader(cl);
return accessible(loadCapsule(cl, jarFile).getDeclaredConstructor(Path.class)).newInstance(jarFile);
} finally {
Thread.currentThread().setContextClassLoader(ccl);
}
} catch (IncompatibleClassChangeError e) {
throw new RuntimeException("Caplet " + jarFile + " is not compatible with this capsule (" + VERSION + ")");
} catch (InvocationTargetException e) {
throw rethrow(e.getTargetException());
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Could not instantiate capsule.", e);
}
}
private static Capsule newCapsule(String capsuleClass, Capsule pred) {
try {
final Class<? extends Capsule> clazz = loadCapsule(Thread.currentThread().getContextClassLoader(), capsuleClass, capsuleClass);
assert getActualCapsuleClass(clazz) == Capsule.class;
return accessible(clazz.getDeclaredConstructor(Capsule.class)).newInstance(pred);
} catch (IncompatibleClassChangeError e) {
throw new RuntimeException("Caplet " + capsuleClass + " is not compatible with this capsule (" + VERSION + ")");
} catch (InvocationTargetException e) {
throw rethrow(e.getTargetException());
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Could not instantiate capsule " + capsuleClass, e);
}
}
private static Class<? extends Capsule> loadCapsule(ClassLoader cl, Path jarFile) {
final String mainClassName = getMainClass(jarFile);
if (mainClassName != null)
return loadCapsule(cl, mainClassName, jarFile.toString());
throw new RuntimeException(jarFile + " does not appear to be a valid capsule.");
}
@SuppressWarnings("unchecked")
private static Class<? extends Capsule> loadCapsule(ClassLoader cl, String capsuleClass, String name) {
try {
log(LOG_DEBUG, "Loading capsule class " + capsuleClass + " using class loader " + toString(cl));
final Class<?> clazz = cl.loadClass(capsuleClass);
final Class<Capsule> c = getActualCapsuleClass(clazz);
if (c == null)
throw new RuntimeException(name + " does not appear to be a valid capsule.");
if (c != Capsule.class) // i.e. it's the Capsule class but in a different classloader
accessible(c.getDeclaredField("PROPERTIES")).set(null, new Properties(PROPERTIES));
return (Class<? extends Capsule>) clazz;
} catch (ClassNotFoundException e) {
throw new RuntimeException("Caplet " + capsuleClass + " not found.", e);
} catch (NoSuchFieldException e) {
throw new RuntimeException(name + " does not appear to be a valid capsule.");
} catch (IncompatibleClassChangeError | ClassCastException e) {
throw new RuntimeException("Caplet " + capsuleClass + " is not compatible with this capsule (" + VERSION + ")");
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
private static Class<Capsule> getActualCapsuleClass(Class<?> clazz) {
Class<?> c = clazz;
while (c != null && !Capsule.class.getName().equals(c.getName()))
c = c.getSuperclass();
return (Class<Capsule>) c;
}
private static String getCapsuleVersion(Class<?> cls) {
while (cls != null && !cls.getName().equals(Capsule.class.getName()))
cls = cls.getSuperclass();
return getClassVersion(cls);
}
private static String getClassVersion(Class<?> cls) {
if (cls == null)
return null;
try {
final Field f = cls.getDeclaredField("VERSION");
return (String) f.get(null);
} catch (Exception e) {
return null;
}
}
private static String toString(ClassLoader cl) {
return cl == null ? "null"
: cl.toString() + (cl instanceof URLClassLoader ? ("{" + Arrays.toString(((URLClassLoader) cl).getURLs()) + "}") : "")
+ " --> " + toString(cl.getParent());
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Security">
/////////// Security ///////////////////////////////////
private Capsule unsafe(Capsule target) {
if (target != null) {
final SecurityManager security = System.getSecurityManager();
if (security != null && !target.getClass().getProtectionDomain().implies(PERM_UNSAFE_OVERRIDE)) {
log(LOG_DEBUG, "Unsafe target " + target + " skipped");
target = null;
}
}
return target;
}
//</editor-fold>
}