Skip to content

Instantly share code, notes, and snippets.

@lwr
Created August 27, 2021 09:33
Show Gist options
  • Save lwr/6829cefaab2261cec1cc733f6398b9ac to your computer and use it in GitHub Desktop.
Save lwr/6829cefaab2261cec1cc733f6398b9ac to your computer and use it in GitHub Desktop.
cn.mailtech.maven.resolver
/*
* Copyright (c) 2020 Mailtech.cn, Ltd. All Rights Reserved.
*/
package cn.mailtech.maven.resolver;
import java.io.*;
import java.lang.reflect.Method;
import java.net.*;
import java.util.*;
/**
* LaunchHelper.
*
* @author <a href="mailto:lwr@coremail.cn">William Leung</a>
*/
public abstract class Launcher {
private static String mavenHome;
@SuppressWarnings("SpellCheckingInspection")
private static final String CLASSWORLDS_LAUNCHER = "org.codehaus.plexus.classworlds.launcher.Launcher";
@SuppressWarnings("SpellCheckingInspection") // ${MAVEN_HOME}/boot/plexus-classworlds-*.jar
private static ClassLoader classworldsLoader(String mavenHome) throws IOException {
List<URL> urls = new ArrayList<>();
String[] names = new File(mavenHome, "boot").list();
if (names == null) {
throw new FileNotFoundException(new File(mavenHome, "boot").toString());
}
for (String filename : names) {
if (filename.toLowerCase().endsWith(".jar")) {
urls.add(new File(mavenHome, "boot/" + filename).toURI().toURL());
}
}
return new URLClassLoader(urls.toArray(new URL[0]));
}
public static void setMavenHome(String mavenHome) {
Launcher.mavenHome = mavenHome;
}
@SuppressWarnings("SpellCheckingInspection")
static void mvnLaunch(Properties props, String... args) throws Exception {
ClassLoader loader = classworldsLoader(mavenHome);
Method main = loader.loadClass(CLASSWORLDS_LAUNCHER).getDeclaredMethod("mainWithExitCode", String[].class);
Properties systemProperties = System.getProperties();
Properties mavenOpts = (Properties) systemProperties.clone();
mavenOpts.putAll(props);
if (mavenHome == null) {
mavenHome = System.getenv("MAVEN_HOME");
if (mavenHome == null) {
throw new Exception("mavenHome not set");
}
}
// "-Dclassworlds.conf=${MAVEN_HOME}/bin/m2.conf" \
// "-Dmaven.home=${MAVEN_HOME}" \
// "-Dlibrary.jansi.path=${MAVEN_HOME}/lib/jansi-native" \
// "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
mavenOpts.put("classworlds.conf", mavenHome + "/bin/m2.conf");
mavenOpts.put("maven.home", mavenHome);
if (new File(mavenHome + "/lib/jansi-native").exists()) {
mavenOpts.put("library.jansi.path", mavenHome + "/lib/jansi-native");
}
mavenOpts.put("maven.multiModuleProjectDirectory", "");
System.setProperties(mavenOpts);
try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(loader);
try {
int exitCode = (int) main.invoke(null, new Object[]{args});
if (exitCode != 0) {
throw new Exception("maven launcher exit code: " + exitCode);
}
} finally {
Thread.currentThread().setContextClassLoader(contextClassLoader);
}
} finally {
System.setProperties(systemProperties);
}
}
}
/*
* Copyright (c) 2020 Mailtech.cn, Ltd. All Rights Reserved.
*/
package cn.mailtech.maven.resolver;
import java.io.*;
import java.util.*;
/**
* ResolveParams.
*
* @author <a href="mailto:lwr@coremail.cn">William Leung</a>
*/
public class ResolveOptions {
private Collection<File> pomFiles;
private String coords = "";
private String scope = "";
private String mavenCliOpts = "";
private String mavenResolveParams = "";
public Collection<File> getPomFiles() {
return pomFiles;
}
public void setPomFiles(Collection<File> pomFiles) {
this.pomFiles = pomFiles;
}
public String getCoords() {
return coords;
}
public void setCoords(String coords) {
this.coords = coords;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public String getMavenCliOpts() {
return mavenCliOpts;
}
public void setMavenCliOpts(String mavenCliOpts) {
this.mavenCliOpts = mavenCliOpts;
}
public String getMavenResolveParams() {
return mavenResolveParams;
}
public void setMavenResolveParams(String mavenResolveParams) {
this.mavenResolveParams = mavenResolveParams;
}
}
/*
* Copyright (c) 2020 Mailtech.cn, Ltd. All Rights Reserved.
*/
package cn.mailtech.maven.resolver;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;
/**
* Resolver.
*
* @author <a href="mailto:lwr@coremail.cn">William Leung</a>
*/
public class Resolver {
private static String settingsFile;
private static Properties mavenProps() {
Properties result = new Properties();
// suppress messages: 'canning for projects ...' / 'Building ...' / ...
result.put("org.slf4j.simpleLogger.log.org.apache.maven.cli", "WARN");
// suppress messages: 'Not compiling main sources' / 'Not compiling test sources'
result.put("org.slf4j.simpleLogger.log.org.apache.maven.plugin.compiler.CompilerMojo", "WARN");
result.put("org.slf4j.simpleLogger.log.org.apache.maven.plugin.compiler.TestCompilerMojo", "WARN");
// The copying of resource files would not be bypassed until maven-resources-plugin@3.0.0
// see https://maven.apache.org/plugins/maven-resources-plugin/resources-mojo.html#skip
// suppress messages: [INFO] Skipping the execution
// [INFO] Using ... encoding to copy filtered resources.
// [INFO] Copying ... resource(s)
// [INFO] Not copying test resources
result.put("org.slf4j.simpleLogger.log.org.apache.maven.plugins.resources", "WARN"); // for maven-resources-plugin-3
result.put("org.slf4j.simpleLogger.log.org.apache.maven.plugin.resources", "WARN"); // for maven-resources-plugin-2.6
// suppress messages: [WARNING] Using platform encoding (GB18030 actually) to copy filtered resources, i.e. build is platform dependent!
// [INFO] skip non existing resourceDirectory ...
result.put("org.slf4j.simpleLogger.log.org.apache.maven.shared.filtering.DefaultMavenResourcesFiltering", "ERROR");
// enable message: 'Downloading from ...' / 'Downloaded from ...'
result.put("org.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener", "INFO");
// Other properties
result.put("java.awt.headless", "true");
result.put("user.language", "en");
return result;
}
public static void setSettingsFile(String settingsFile) {
Resolver.settingsFile = settingsFile;
}
private Set<String> scopes = Collections.emptySet();
private void generatedTempPom(String coords, File target) throws IOException {
String template;
try (InputStream in = getClass().getResourceAsStream("template.pom")) {
assert in != null;
// stupid way but no better choice
// see https://stackoverflow.com/questions/309424/how-do-i-read-convert-an-inputstream-into-a-string-in-java
java.util.Scanner s = new java.util.Scanner(in, "utf8").useDelimiter("\\A");
template = s.hasNext() ? s.next() : "";
}
// --coords=coords="g1:a1:v1 g2:a2:v2!g:a"
StringBuilder builder = new StringBuilder("<dependencies>");
for (String item : coords.split("[\\s;]+")) {
String[] withExclusions = item.split("!");
String[] arr = withExclusions[0].split(":", 4);
if (arr.length == 3 && arr[0].length() > 0 && arr[1].length() > 0 && arr[2].length() > 0) {
builder.append("<dependency>");
builder.append(String.format(""
+ "<groupId>%s</groupId>"
+ "<artifactId>%s</artifactId>"
+ "<version>%s</version>",
(Object[]) arr));
for (String exclusion : Arrays.asList(withExclusions).subList(1, withExclusions.length)) {
String[] arrExclusion = exclusion.split(":", 3);
if (arrExclusion.length == 2 && arrExclusion[0].length() > 0 && arrExclusion[1].length() > 0) {
builder.append(String.format(""
+ "<exclusions>"
+ "<exclusion>"
+ "<groupId>%s</groupId>"
+ "<artifactId>%s</artifactId>"
+ "</exclusion>"
+ "</exclusions>",
(Object[]) arrExclusion));
} else {
throw new IllegalArgumentException("Invalid coords: " + coords);
}
}
builder.append("</dependency>");
} else {
throw new IllegalArgumentException("Invalid coords: " + coords);
}
}
builder.append("</dependencies>");
Files.write(target.toPath(), template.replace("<!-- content -->", builder).getBytes(StandardCharsets.UTF_8));
}
// scope = "compile" <==> scope = "[compile, provided, system]" (default)
// scope = "runtime" <==> scope = "[compile, runtime]"
// scope = "test" <==> scope = "[compile, provided, system, runtime, test]" (all)
private String parseScope(String scope) {
String[] scopes;
if (scope.startsWith("[") && scope.endsWith("]")) {
scopes = scope.substring(1, scope.length() - 1).split("\\s*,\\s*");
scope = "test";
} else if (scope.equals("compile") || scope.isEmpty()) {
scopes = new String[]{"compile", "provided", "system"};
} else if (scope.equals("runtime")) {
scopes = new String[]{"compile", "runtime"};
} else if (scope.equals("test")) {
scopes = new String[]{"compile", "provided", "system", "runtime", "test"};
} else {
scopes = new String[]{scope};
}
this.scopes = new HashSet<>(Arrays.asList(scopes));
return scope;
}
public Map<String, String> execute(ResolveOptions options) throws Exception {
Map<String, String> result = new LinkedHashMap<>();
File generatedPomFile = File.createTempFile("generated", ".pom");
File outputFile = File.createTempFile("output", ".txt");
try {
Collection<File> pomFiles;
if (options.getPomFiles() != null && options.getPomFiles().size() > 0) {
pomFiles = options.getPomFiles();
// System.out.printf("Resolving %s", options.getPomFiles());
} else if (options.getCoords().length() > 0) {
generatedTempPom(options.getCoords(), generatedPomFile);
pomFiles = Collections.singleton(generatedPomFile);
// System.out.printf("Resolving %s", options.getCoords());
} else {
throw new Exception("Arg not found: pomFile or coords");
}
for (File pomFile : pomFiles) {
doResolve(options, pomFile, outputFile);
for (Map.Entry<String, String> e : parseOutput(outputFile).entrySet()) {
result.putIfAbsent(e.getKey(), e.getValue());
}
}
return result;
} finally {
// noinspection ResultOfMethodCallIgnored
generatedPomFile.delete();
// noinspection ResultOfMethodCallIgnored
outputFile.delete();
}
}
private void doResolve(ResolveOptions options, File pomFile, File outputFile) throws Exception {
// Resolve maven dependencies:
// - @see https://stackoverflow.com/questions/28835418/mvn-dependency-fails-on-trivial-project/39565742#39565742
// - @see https://stackoverflow.com/questions/15249345/disable-phases-in-maven-lifecycle/15250450#15250450
//
// Kotlin compile mojo does not have a skip configuration parameter now (but have for test-compile).
// For workaround, we now set parse to none to disable the kotlin compiler (same for resources plugin).
List<String> args = new ArrayList<>(Arrays.asList("test-compile", "dependency:resolve"));
args.add("-f");
args.add(pomFile.getAbsolutePath());
// MAVEN_CLI_OPTS
args.add("--batch-mode");
if (settingsFile != null) {
args.add("--settings");
args.add(settingsFile);
}
// --mavenCliOpts="-am -pl relativePath|:artifactId"
if (!options.getMavenCliOpts().matches("-pl |--projects ")) {
args.add("-pl");
args.add(".");
}
if (options.getMavenCliOpts().length() > 0) {
Collections.addAll(args, options.getMavenCliOpts().split("\\s+"));
}
Map<String, String> props = new HashMap<>();
props.put("maven.main.skip", null);
props.put("maven.test.skip", null);
props.put("maven.resources.skip", null);
props.put("kotlin.main.compile.phase", "none");
props.put("kotlin.test.compile.phase", "none");
props.put("includeScope", parseScope(options.getScope()));
props.put("outputAbsoluteArtifactFilename", null);
props.put("outputFile", outputFile.getAbsolutePath());
for (String param : options.getMavenResolveParams().split("\\s+")) {
String[] arr = param.split("=", 2);
if (arr[0].length() > 0) {
props.putIfAbsent(arr[0], arr.length > 1 ? arr[1] : ""); // dont override
}
}
props.forEach((key, val) -> args.add("-D" + key + (val == null ? "" : "=" + val)));
Launcher.mvnLaunch(mavenProps(), args.toArray(new String[0]));
}
private Map<String, String> parseOutput(File outputFile) throws IOException {
Map<String, String> result = new LinkedHashMap<>();
for (String line : Files.readAllLines(outputFile.toPath())) {
line = line.trim();
// skip the first line "The following files have been resolved:"
if (line.isEmpty() || line.equals("none") || line.startsWith("The following files")) {
continue;
}
// groupId:artifactId:type:version:scope:absoluteArtifactFilename (optional)
String[] arr = line.split(":", 6);
if (arr.length != 6) {
throw new RuntimeException("Unknown output: " + line);
}
String groupId = arr[0];
String artifactId = arr[1];
String type = arr[2];
// String version = arr[3];
String scope = arr[4];
String value = arr[5]; // absoluteArtifactFilename (optional)
if (scopes.contains(scope)) {
result.put(groupId + ':' + artifactId + ':' + type, value.endsWith(" (optional)")
? value.substring(0, value.length() - " (optional)".length())
: value);
}
}
return result;
}
}
@lwr
Copy link
Author

lwr commented Aug 27, 2021

usage

// preparing for environments
Launcher.setMavenHome(getProject().getProperty("maven.home"));
Resolver.setSettingsFile(getProject().getProperty("maven.settingsFile"));

ResolveOptions options = new ResolveOptions()
// pomFile="foo.pom; bar.pom; foo/**/bar/pom.xml" // multiple files are also supported
options.setPomFiles(pomFiles);
// coords="g1:a1:v1 g2:a2:v2!g:a" // multiple coords and exclusion are also supported
options.setCoords(coords);
// "compile" | "runtime" | "test"
options.setScope(scope);
// if you liked
options.setMavenCliOpts(...);
// if you liked
options.setMavenResolveParams(...);

// for each entry: {key -> "g1:a1:v1:jar", value -> "/path/to/.m2/repository/g1/a1/v1/a1-v1.jar"}
Map<String, String> properties = new Resolver().execute(options);

@lwr
Copy link
Author

lwr commented Aug 27, 2021

and this is the ant task class

/*
 * Copyright (c) 2020 Mailtech.cn, Ltd. All Rights Reserved.
 */

package cn.mailtech.maven.resolver.ant.tasks;

import cn.mailtech.maven.resolver.Launcher;
import cn.mailtech.maven.resolver.ResolveOptions;
import cn.mailtech.maven.resolver.Resolver;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.resources.FileResource;

import java.io.*;
import java.util.*;

/**
 * Resolve.
 *
 * @author <a href="mailto:lwr@coremail.cn">William Leung</a>
 */
public class Resolve extends Task {

    private final ResolveOptions options = new ResolveOptions();

    private String pomFile;
    private String pathRef;
    private String propertyPrefix;

    public void setPomFile(String pomFile) {
        this.pomFile = pomFile;
    }

    public void setCoords(String coords) {
        options.setCoords(coords);
    }

    public void setScope(String scope) {
        options.setScope(scope);
    }

    public void setMavenCliOpts(String mavenCliOpts) {
        options.setMavenCliOpts(mavenCliOpts);
    }

    public void setMavenResolveParams(String mavenResolveParams) {
        options.setMavenResolveParams(mavenResolveParams);
    }

    public void setPathRef(String pathRef) {
        this.pathRef = pathRef;
    }

    public void setPropertyPrefix(String propertyPrefix) {
        this.propertyPrefix = propertyPrefix;
    }

    private boolean inited;

    @Override
    public void execute() throws BuildException {
        if (!inited) {
            Launcher.setMavenHome(getProject().getProperty("maven.home"));
            Resolver.setSettingsFile(getProject().getProperty("maven.settingsFile"));
            inited = true;
        }

        // pomFile="foo.pom; bar.pom; foo/**/bar/pom.xml"
        Set<File> pomFiles = new LinkedHashSet<>();
        if (pomFile != null && pomFile.length() > 0) {
            for (String pomFile : pomFile.split("\\s*;\\s")) {
                // foo/**/bar/pom.xml -> ['foo', '**/bar/pom.xml']
                int asteriskPosition = pomFile.indexOf('*');
                if (asteriskPosition != -1 && pomFile.charAt(asteriskPosition - 1) == '/') {
                    FileSet fileset = (FileSet) getProject().createDataType("fileset");
                    fileset.setDir(getProject().resolveFile(pomFile.substring(0, asteriskPosition - 1)));
                    fileset.setIncludes(pomFile.substring(asteriskPosition));

                    Iterator<Resource> iterator = fileset.iterator();
                    if (!iterator.hasNext()) {
                        throw new BuildException("pomFile not found: " + pomFile);
                    }
                    while (iterator.hasNext()) {
                        pomFiles.add(((FileResource) iterator.next()).getFile());
                    }
                } else if (pomFile.length() > 0) {
                    pomFiles.add(getProject().resolveFile(pomFile));
                }
            }
        }
        options.setPomFiles(pomFiles);

        Map<String, String> properties;
        try {
            properties = new Resolver().execute(options);
        } catch (Exception e) {
            throw new BuildException(e);
        }

        Path path = new Path(getProject());
        String propPrefix = propertyPrefix != null ? propertyPrefix + (propertyPrefix.matches("[./:#]$|^$") ? "" : ".") : null;
        properties.forEach((key, value) -> {
            if (propPrefix != null) {
                getProject().setNewProperty(propPrefix + key, value);
            }
            path.createPathElement().setPath(value);
        });

        if (pathRef != null) {
            getProject().addReference(pathRef, path);
        }
    }
}

and examples

<!-- simple pom -->
<resolve pomFile="./pom.xml" scope="provided" pathRef="cp" />

<!-- simple coords -->
<resolve coords="org.apache.ant:ant:1.9.4" pathRef="cp" />

<!-- coords-exclusion -->
<resolve coords="org.apache.ant:ant:1.9.4!org.apache.ant:ant-launcher" pathRef="cp" />

<!-- multiple poms with fileset -->
<resolve pomFile="./resolver/*.pom" pathRef="cp" />

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment