/* * Copyright (c) 2015-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 capsule.DependencyManager; import capsule.Pom; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.AccessibleObject; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Map.Entry; import static java.util.Arrays.asList; import java.util.Collection; import static java.util.Collections.emptyList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.eclipse.aether.graph.Dependency; /** * * @author pron */ public class MavenCapsule extends Capsule implements capsule.MavenCapsule { @SuppressWarnings("FieldNameHidesFieldInSuperclass") public static final String VERSION = "1.0.4"; private static final String CAPLET_NAME = MavenCapsule.class.getName(); static { requireCapsuleVersion("1.0.4"); } private static final String PROP_TREE = OPTION("capsule.tree", "false", "printDependencyTree", "Prints the capsule's dependency tree."); private static final String PROP_RESOLVE = OPTION("capsule.resolve", "false", "resolve", "Downloads all un-cached dependencies."); private static final String PROP_USE_LOCAL_REPO = OPTION("capsule.local", null, null, "Sets the path of the local Maven repository to use."); private static final String PROP_RESET = "capsule.reset"; private static final String PROP_USER_HOME = "user.home"; private static final String PROP_PROFILE = "capsule.profile"; private static final int PROFILE = emptyOrTrue(System.getProperty(PROP_PROFILE)) ? LOG_QUIET : LOG_DEBUG; private static final Entry<String, List<String>> ATTR_REPOSITORIES = ATTRIBUTE("Repositories", T_LIST(T_STRING()), asList("central"), true, "A list of Maven repositories, each formatted as URL or NAME(URL)"); private static final Entry<String, List<String>> ATTR_MANAGED_DEPENDENCIES = ATTRIBUTE("Managed-Dependencies", T_LIST(T_STRING()), null, true, "A list of managed dependencies, forcing versions in transitive dependencies, each formatted as group:artifact:type:classifier:version"); private static final Entry<String, Boolean> ATTR_ALLOW_SNAPSHOTS = ATTRIBUTE("Allow-Snapshots", T_BOOL(), false, true, "Whether or not SNAPSHOT dependencies are allowed"); private static final String ENV_CAPSULE_REPOS = "CAPSULE_REPOS"; private static final String ENV_CAPSULE_LOCAL_REPO = "CAPSULE_LOCAL_REPO"; private static final String POM_FILE = "pom.xml"; private static final String DEPS_CACHE_NAME = "deps"; private DependencyManager dependencyManager; private Pom pom; private Path localRepo; private String version; // app version cache private static final List<Path> UNRESOLVED = new ArrayList<>(); private final Map<Dependency, List<Path>> dependencies = new HashMap<>(); //<editor-fold defaultstate="collapsed" desc="Constructors"> /////////// Constructors /////////////////////////////////// public MavenCapsule(Path jarFile) { super(jarFile); } public MavenCapsule(Capsule pred) { super(pred); } @Override protected void finalizeCapsule() { this.pom = createPomReader(getJarFile(), POM_FILE, null); if (dependencyManager != null) { setDependencyRepositories(getAttribute(ATTR_REPOSITORIES)); setManagedDependencies(); } super.finalizeCapsule(); } //</editor-fold> //<editor-fold defaultstate="collapsed" desc="Main Operations"> /////////// Main Operations /////////////////////////////////// void printDependencyTree(List<String> args) { verifyNonEmpty("Cannot print dependencies of a wrapper capsule."); STDOUT.println("Dependencies for " + getAppId()); lookupAllDependencies(); if (dependencies.isEmpty()) STDOUT.println("No external dependencies."); else getDependencyManager().printDependencyTree(getUnresolved(), STDOUT); } void resolve(List<String> args) throws IOException, InterruptedException { verifyNonEmpty("Cannot resolve a wrapper capsule."); lookupAllDependencies(); getDependencyManager().resolveDependencies(getUnresolved()); log(LOG_QUIET, "Capsule resolved"); } private void verifyNonEmpty(String message) { if (isEmptyCapsule()) throw new IllegalArgumentException(message); } private void lookupAllDependencies() { try { accessible(Capsule.class.getDeclaredMethod("lookupAllDependencies")).invoke(this); } catch (ReflectiveOperationException e) { throw new AssertionError(e); } // for (Map.Entry<String, ?> attr : asList( // ATTR_DEPENDENCIES, // ATTR_NATIVE_DEPENDENCIES, // ATTR_APP_CLASS_PATH, // ATTR_BOOT_CLASS_PATH, // ATTR_BOOT_CLASS_PATH_P, // ATTR_BOOT_CLASS_PATH_A, // ATTR_JAVA_AGENTS, // ATTR_NATIVE_AGENTS)) // getAttribute(attr); } private List<Dependency> getUnresolved() { final List<Dependency> unresolved = new ArrayList<>(); for (Map.Entry<Dependency, List<Path>> e : dependencies.entrySet()) { if (e.getValue() == UNRESOLVED) unresolved.add(e.getKey()); } return unresolved; } //</editor-fold> //<editor-fold desc="Capsule Overrides"> /////////// Capsule Overrides /////////////////////////////////// @Override @SuppressWarnings("unchecked") protected <T> T attribute(Entry<String, T> attr) { if (ATTR_APP_ID.equals(attr)) { String id = super.attribute(ATTR_APP_ID); if (id == null && pom != null) id = pom.getGroupId() + "." + pom.getArtifactId(); return (T) id; } if (ATTR_APP_VERSION.equals(attr)) { String ver = super.attribute(ATTR_APP_VERSION); if (ver == null && version != null) ver = version; if (ver == null && hasAttribute(ATTR_APP_ARTIFACT) && isDependency(getAttribute(ATTR_APP_ARTIFACT))) ver = getAppArtifactVersion(getDependencyManager().getLatestVersion(getAttribute(ATTR_APP_ARTIFACT), "jar")); if (ver == null && pom != null) ver = pom.getVersion(); this.version = ver; // cache return (T) ver; } if (ATTR_DEPENDENCIES.equals(attr)) { List<Object> deps = super.attribute(ATTR_DEPENDENCIES); // find deps in POM if not in manifest if ((deps == null || deps.isEmpty()) && pom != null) { deps = new ArrayList<>(); for (String d : pom.getDependencies("jar")) deps.add(d); // deps.add(lookup(pom.resolve(d), "jar", ATTR_DEPENDENCIES, null)); } return (T) deps; } if (ATTR_NATIVE_DEPENDENCIES.equals(attr)) { Map<Object, String> deps = (Map<Object, String>) super.attribute(ATTR_NATIVE_DEPENDENCIES); // find deps in POM if not in manifest if ((deps == null || deps.isEmpty()) && pom != null) { deps = new LinkedHashMap<>(); for (String d : pom.getDependencies(getNativeLibExtension())) deps.put(d, ""); } return (T) deps; } if (ATTR_MANAGED_DEPENDENCIES.equals(attr)) { List<String> deps = super.attribute(ATTR_MANAGED_DEPENDENCIES); // find deps in POM if not in manifest if ((deps == null || deps.isEmpty()) && pom != null) deps = pom.getManagedDependencies(); return (T) deps; } if (ATTR_REPOSITORIES.equals(attr)) { final List<String> repos = new ArrayList<>(); repos.addAll(nullToEmpty(split(getenv(ENV_CAPSULE_REPOS), "[,\\s]\\s*"))); repos.addAll(super.attribute(ATTR_REPOSITORIES)); if (pom != null) addAllIfAbsent(repos, nullToEmpty(pom.getRepositories())); return (T) repos; } return super.attribute(attr); } @Override protected Object lookup0(Object x, String type, Entry<String, ?> attrContext, Object context) { final Object res = super.lookup0(x, type, attrContext, context); if (res == null && x instanceof String) { final String s = (String) x; if (isDependency(s)) { final Dependency dep = DependencyManager.toDependency(s, type.isEmpty() ? "jar" : type); if (!dependencies.containsKey(dep)) dependencies.put(dep, UNRESOLVED); return super.lookup0(dep, type, attrContext, context); } } else if (x instanceof String && res instanceof Path && ATTR_DEPENDENCIES.equals(attrContext)) { // If found also lookup its transitive deps, see #14 final String s = (String) x; final List<Object> ret = new ArrayList<>(); ret.add(res); if (isDependency(s)) { type = type.isEmpty() ? "jar" : type; final Dependency dep = DependencyManager.toDependency(s, type); final Pom pom1 = createPomReader(getWritableAppCache().resolve((Path) res), getPomJarEntryName(dep), pom); if (pom1 != null) { for (String d : pom1.getDependencies(type)) addFlat(lookup0(d, type, attrContext, null), ret); } } return ret; } return res; } @Override protected List<Path> resolve0(final Object x) { if (x instanceof Dependency) { final Dependency d = (Dependency) x; if (dependencies.get(d) == UNRESOLVED) { long start = clock(); Map<Dependency, List<Path>> resolved = getDependencyManager().resolveDependencies(getUnresolved()); log(LOG_DEBUG, "Maven resolved: " + resolved); dependencies.putAll(resolved); time("resolveAll", start); } assert dependencies.get(d) != UNRESOLVED : d; final Object y = dependencies.get(d); if (y == null) // there's another MavenCapsule in the chain return super.resolve0(x); return resolve(y); } return super.resolve0(x); } //</editor-fold> //<editor-fold defaultstate="collapsed" desc="Internal Methods"> /////////// Internal Methods /////////////////////////////////// @Override public List<Path> lookupAndResolve(String x, String type) { return resolve(lookup(x, type, null, null)); } @Override public boolean isLogging1(int level) { return isLogging(level); } @Override public void log1(int level, String str) { log(level, str); } @Override public void log1(int level, Throwable t) { log(level, t); } private Pom createPomReader(Path jarFile, String entry, Pom root) { try (InputStream is = getEntryInputStream(jarFile, entry)) { return is != null ? new Pom(is, root, this) : null; } catch (IOException e) { throw new RuntimeException("Could not read " + entry, e); } } private DependencyManager getDependencyManager() { final DependencyManager dm = initDependencyManager(); if (dm == null) throw new RuntimeException("Capsule " + getJarFile() + " uses dependencies, while the necessary dependency management classes are not found in the capsule JAR"); return dm; } private DependencyManager initDependencyManager() { if (dependencyManager == null) { dependencyManager = createDependencyManager(); if (dependencyManager != null) { setDependencyRepositories(getAttribute(ATTR_REPOSITORIES)); setManagedDependencies(); } } return dependencyManager; } private DependencyManager createDependencyManager() { final boolean reset = systemPropertyEmptyOrTrue(PROP_RESET); return createDependencyManager(getLocalRepo().toAbsolutePath(), reset, getLogLevel()); } protected DependencyManager createDependencyManager(Path localRepo, boolean reset, int logLevel) { MavenCapsule ct; return (ct = getCallTarget(MavenCapsule.class)) != null ? ct.createDependencyManager(localRepo, reset, logLevel) : createDependencyManager0(localRepo, reset, logLevel); } private DependencyManager createDependencyManager0(Path localRepo, boolean reset, int logLevel) { return new DependencyManager(localRepo, reset, logLevel); } private void setDependencyRepositories(List<String> repositories) { getDependencyManager().setRepositories(repositories, getAttribute(ATTR_ALLOW_SNAPSHOTS)); } private void setManagedDependencies() { getDependencyManager().setManagedDependencies(getAttribute(ATTR_MANAGED_DEPENDENCIES)); } private Path getLocalRepo() { if (localRepo == null) { Path repo; final String local = emptyToNull(expandCommandLinePath(propertyOrEnv(PROP_USE_LOCAL_REPO, ENV_CAPSULE_LOCAL_REPO))); if (local != null) repo = toAbsolutePath(Paths.get(local)); else { repo = getCacheDir().resolve(DEPS_CACHE_NAME); try { if (!Files.exists(repo)) Files.createDirectory(repo, getPermissions(repo.getParent())); return repo; } catch (IOException e) { log(LOG_VERBOSE, "Could not create local repo at " + repo); if (isLogging(LOG_VERBOSE)) e.printStackTrace(STDERR); repo = null; } } localRepo = repo; } return localRepo; } private static String getPomJarEntryName(Dependency dep) { return "META-INF/maven/" + dep.getArtifact().getGroupId() + "/" + dep.getArtifact().getArtifactId() + "/" + POM_FILE; } private static void addFlat(Object o, List<Object> ret) { if (o instanceof Collection) ret.addAll((Collection) o); else ret.add(o); } private static boolean isDependency(String lib) { return lib.contains(":") && !lib.contains(":\\"); } //</editor-fold> //<editor-fold defaultstate="collapsed" desc="Utils"> /////////// Utils /////////////////////////////////// private static boolean systemPropertyEmptyOrTrue(String property) { return emptyOrTrue(getProperty(property)); } private static boolean emptyOrTrue(String value) { if (value == null) return false; return value.isEmpty() || Boolean.parseBoolean(value); } private static String propertyOrEnv(String propName, String envVar) { String val = getProperty(propName); if (val == null) val = emptyToNull(getenv(envVar)); return val; } private static String expandCommandLinePath(String str) { if (str == null) return null; // if (isWindows()) // return str; // else return str.startsWith("~/") ? str.replace("~", getProperty(PROP_USER_HOME)) : str; } private static Path toAbsolutePath(Path p) { return p != null ? p.toAbsolutePath().normalize() : null; } private static <C extends Collection<T>, T> C addAllIfAbsent(C c, Collection<T> c1) { for (T e : c1) { if (!c.contains(e)) c.add(e); } return c; } @SuppressWarnings("unchecked") private static <T> List<T> nullToEmpty(List<T> list) { return list != null ? list : (List<T>) emptyList(); } private static <T extends Collection<?>> T emptyToNull(T c) { return (c == null || c.isEmpty()) ? null : c; } private static String emptyToNull(String s) { if (s == null) return null; s = s.trim(); return s.isEmpty() ? null : 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 <T extends AccessibleObject> T accessible(T obj) { if (obj == null) return null; obj.setAccessible(true); return obj; } 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"); } private static void requireCapsuleVersion(String minVer) { try { final String capsuleVersion = (String) Capsule.class.getField("VERSION").get(null); // don't use static constant as it's inlined if (Capsule.compareVersions(capsuleVersion, minVer) < 0) throw new IllegalStateException("This version of the " + CAPLET_NAME + " caplet, " + VERSION + ", requires a minimal Capsule version of " + minVer + " but it is " + capsuleVersion); } catch (ReflectiveOperationException e) { throw new IllegalStateException("Caplet " + CAPLET_NAME + " could not obtain Capsule's version"); } } //</editor-fold> }