/*
* Capsule
* Copyright (c) 2014-2015, Parallel Universe Software Co. 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
*/
package co.paralleluniverse.capsule;
import static co.paralleluniverse.common.Exceptions.rethrow;
import co.paralleluniverse.common.JarClassLoader;
import co.paralleluniverse.common.JarInputStream;
import java.io.IOException;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.jar.Manifest;
/**
* Provides methods for loading, inspecting, and launching capsules.
*
* @author pron
*/
public final class CapsuleLauncher {
private static final String CAPSULE_CLASS_NAME = "Capsule";
private static final String OPT_JMX_REMOTE = "com.sun.management.jmxremote";
private static final String ATTR_MAIN_CLASS = "Main-Class";
private static final String PROP_MODE = "capsule.mode";
private final Path jarFile;
private final Class capsuleClass;
private Properties properties;
public CapsuleLauncher(Path jarFile) throws IOException {
this.jarFile = jarFile;
this.capsuleClass = loadCapsuleClass(jarFile);
setProperties(null);
}
/**
* Sets the Java homes that will be used by the capsules created by {@code newCapsule}.
*
* @param javaHomes a map from Java version strings to their respective JVM installation paths
* @return {@code this}
*/
public CapsuleLauncher setJavaHomes(Map<String, List<Path>> javaHomes) {
final Field homes = getCapsuleField("JAVA_HOMES");
if (homes != null)
set(null, homes, javaHomes);
return this;
}
/**
* Sets the properties for the capsules created by {@code newCapsule}
*
* @param properties the properties
* @return {@code this}
*/
public CapsuleLauncher setProperties(Properties properties) {
this.properties = properties != null ? properties : new Properties(System.getProperties());
set(null, getCapsuleField("PROPERTIES"), this.properties);
return this;
}
/**
* Sets a property for the capsules created by {@code newCapsule}
*
* @param property the name of the property
* @param value the property's value
* @return {@code this}
*/
public CapsuleLauncher setProperty(String property, String value) {
if (value != null)
properties.setProperty(property, value);
else
properties.remove(property);
return this;
}
/**
* Sets the location of the cache directory for the capsules created by {@code newCapsule}
*
* @param dir the cache directory
* @return {@code this}
*/
public CapsuleLauncher setCacheDir(Path dir) {
set(null, getCapsuleField("CACHE_DIR"), dir);
return this;
}
/**
* Creates a new capsule.
*/
public Capsule newCapsule() {
return newCapsule(null, null);
}
/**
* Creates a new capsule
*
* @param mode the capsule mode
* @return the capsule.
*/
public Capsule newCapsule(String mode) {
return newCapsule(mode, null);
}
/**
* Creates a new capsule
*
* @param wrappedJar a path to a capsule JAR that will be launched (wrapped) by the empty capsule in {@code jarFile}
* or {@code null} if no wrapped capsule is wanted
* @return the capsule.
*/
public Capsule newCapsule(Path wrappedJar) {
return newCapsule(null, wrappedJar);
}
/**
* Creates a new capsule
*
* @param mode the capsule mode, or {@code null} for the default mode
* @param wrappedJar a path to a capsule JAR that will be launched (wrapped) by the empty capsule in {@code jarFile}
* or {@code null} if no wrapped capsule is wanted
* @return the capsule.
*/
public Capsule newCapsule(String mode, Path wrappedJar) {
final String oldMode = properties.getProperty(PROP_MODE);
final ClassLoader oldCl = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(capsuleClass.getClassLoader());
try {
setProperty(PROP_MODE, mode);
final Constructor<?> ctor = accessible(capsuleClass.getDeclaredConstructor(Path.class));
final Object capsule = ctor.newInstance(jarFile);
if (wrappedJar != null) {
final Method setTarget = accessible(capsuleClass.getDeclaredMethod("setTarget", Path.class));
setTarget.invoke(capsule, wrappedJar);
}
return wrap(capsule);
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Could not create capsule instance.", e);
} finally {
setProperty(PROP_MODE, oldMode);
Thread.currentThread().setContextClassLoader(oldCl);
}
}
private static Class<?> loadCapsuleClass(Path jarFile) throws IOException {
final Manifest mf;
try (JarInputStream jis = new JarInputStream(Files.newInputStream(jarFile))) {
mf = jis.getManifest();
}
final ClassLoader cl = new JarClassLoader(jarFile, true);
final Class<?> clazz = loadCapsuleClass(mf, cl);
if (clazz == null)
throw new RuntimeException(jarFile + " does not appear to be a valid capsule.");
return clazz;
}
private static Class<?> loadCapsuleClass(Manifest mf, ClassLoader cl) {
final String mainClass = mf.getMainAttributes() != null ? mf.getMainAttributes().getValue(ATTR_MAIN_CLASS) : null;
if (mainClass == null)
return null;
try {
Class<?> clazz = cl.loadClass(mainClass);
if (!isCapsuleClass(clazz))
clazz = null;
return clazz;
} catch (ClassNotFoundException e) {
return null;
}
}
private static boolean isCapsuleClass(Class<?> clazz) {
if (clazz == null)
return false;
return getActualCapsuleClass(clazz) != null;
}
private static Capsule wrap(Object capsule) {
return (Capsule) Proxy.newProxyInstance(CapsuleLauncher.class.getClassLoader(), new Class<?>[]{Capsule.class}, new CapsuleAccess(capsule));
}
private static class CapsuleAccess implements InvocationHandler {
private final Object capsule;
private final Class<?> clazz;
public CapsuleAccess(Object capsule) {
this.capsule = capsule;
this.clazz = capsule.getClass();
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getDeclaringClass().equals(Object.class) && method.getName().equals("equals")) {
final Object other = args[0];
if (other == this)
return true;
if (!(other instanceof CapsuleAccess))
return false;
return call(method, args);
}
try {
return call(method, args);
} catch (NoSuchMethodException e) {
switch (method.getName()) {
case "getVersion":
return get("VERSION");
case "getProperties":
return get("PROPERTIES");
case "getAttribute":
return getMethod(clazz, "getAttribute", Map.Entry.class).invoke(capsule, ((Attribute) args[0]).toEntry());
case "hasAttribute":
return getMethod(clazz, "hasAttribute", Map.Entry.class).invoke(capsule, ((Attribute) args[0]).toEntry());
default:
throw new UnsupportedOperationException("Capsule " + clazz + " does not support this operation");
}
}
}
private Object call(Method method, Object[] args) throws NoSuchMethodException {
final Method m = getMethod(clazz, method.getName(), method.getParameterTypes());
if (m == null)
throw new NoSuchMethodException();
return CapsuleLauncher.invoke(capsule, m, args);
}
private Object get(String field) {
final Field f = getField(clazz, field);
if (f == null)
throw new UnsupportedOperationException("Capsule " + clazz + " does not contain the field " + field);
return CapsuleLauncher.get(capsule, f);
}
}
/**
* Returns all known Java installations
*
* @return a map from the version strings to their respective paths of the Java installations.
*/
@SuppressWarnings("unchecked")
public static Map<String, List<Path>> findJavaHomes() {
try {
return (Map<String, List<Path>>) accessible(Class.forName(CAPSULE_CLASS_NAME).getDeclaredMethod("getJavaHomes")).invoke(null);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
}
/**
* Adds an option to the JVM arguments to enable JMX connection
*
* @param jvmArgs the JVM args
* @return a new list of JVM args
*/
public static List<String> enableJMX(List<String> jvmArgs) {
final String arg = "-D" + OPT_JMX_REMOTE;
if (jvmArgs.contains(arg))
return jvmArgs;
final List<String> cmdLine2 = new ArrayList<>(jvmArgs);
cmdLine2.add(arg);
return cmdLine2;
}
private Field getCapsuleField(String name) {
return getField(getActualCapsuleClass(capsuleClass), name);
}
//<editor-fold defaultstate="collapsed" desc="Reflection">
/////////// Reflection ///////////////////////////////////
private static Method getMethod(Class<?> clazz, String name, Class<?>... paramTypes) {
try {
return clazz.getMethod(name, paramTypes);
} catch (NoSuchMethodException e) {
return getMethod0(clazz, name, paramTypes);
}
}
private static Method getMethod0(Class<?> clazz, String name, Class<?>... paramTypes) {
try {
return accessible(clazz.getDeclaredMethod(name, paramTypes));
} catch (NoSuchMethodException e) {
return clazz.getSuperclass() != null ? getMethod0(clazz.getSuperclass(), name, paramTypes) : null;
}
}
private static Field getField(Class<?> clazz, String name) {
try {
return accessible(clazz.getDeclaredField(name));
} catch (NoSuchFieldException e) {
return clazz.getSuperclass() != null ? getField(clazz.getSuperclass(), name) : null;
}
}
private static Class<?> getActualCapsuleClass(Class<?> clazz) {
while (clazz != null && !clazz.getName().equals(CAPSULE_CLASS_NAME))
clazz = clazz.getSuperclass();
return clazz;
}
private static Object invoke(Object obj, Method method, Object... params) {
try {
return method.invoke(obj, params);
} catch (Exception e) {
throw rethrow(e);
}
}
private static <T extends AccessibleObject> T accessible(T x) {
if (!x.isAccessible())
x.setAccessible(true);
return x;
}
private static Object get(Object obj, Field field) {
try {
return field.get(obj);
} catch (Exception e) {
throw rethrow(e);
}
}
private static void set(Object obj, Field field, Object value) {
try {
field.set(obj, value);
} catch (Exception e) {
throw rethrow(e);
}
}
//</editor-fold>
}