/* * 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> }