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