diff --git a/src/main/java/org/codejive/jpm/Jpm.java b/src/main/java/org/codejive/jpm/Jpm.java index 5d04dde..55f6e38 100644 --- a/src/main/java/org/codejive/jpm/Jpm.java +++ b/src/main/java/org/codejive/jpm/Jpm.java @@ -3,7 +3,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.*; -import org.codejive.jpm.json.AppInfo; +import org.codejive.jpm.config.AppInfo; import org.codejive.jpm.util.*; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.resolution.DependencyResolutionException; @@ -91,7 +91,22 @@ public Jpm build() { */ public SyncStats copy(String[] artifactNames, boolean sync) throws IOException, DependencyResolutionException { - List files = Resolver.create(artifactNames).resolvePaths(); + return copy(artifactNames, Collections.emptyMap(), sync); + } + + /** + * Copies the given artifacts to the target directory. + * + * @param artifactNames The artifacts to copy. + * @param repos A map of additional repository names to URLs where artifacts can be found. + * @param sync Whether to sync the target directory or not. + * @return An instance of {@link SyncStats} containing the statistics of the copy operation. + * @throws IOException If an error occurred during the copy operation. + * @throws DependencyResolutionException If an error occurred during the dependency resolution. + */ + public SyncStats copy(String[] artifactNames, Map repos, boolean sync) + throws IOException, DependencyResolutionException { + List files = Resolver.create(artifactNames, repos).resolvePaths(); return FileUtils.syncArtifacts(files, directory, noLinks, !sync); } @@ -131,10 +146,28 @@ private static String artifactGav(Artifact artifact) { */ public SyncStats install(String[] artifactNames) throws IOException, DependencyResolutionException { + return install(artifactNames, Collections.emptyMap()); + } + + /** + * Installs the given artifacts to the target directory while also registering them as + * dependencies in the app.yml file in the current directory. If no artifacts are given, all + * dependencies in the app.yml file will be installed. NB: "installation" in this context + * basically means sync-copying the artifacts to the target directory. + * + * @param artifactNames The artifacts to install. + * @param extraRepos A map of additional repository names to URLs where artifacts can be found. + * @return An instance of {@link SyncStats} containing the statistics of the install operation. + * @throws IOException If an error occurred during the install operation. + * @throws DependencyResolutionException If an error occurred during the dependency resolution. + */ + public SyncStats install(String[] artifactNames, Map extraRepos) + throws IOException, DependencyResolutionException { AppInfo appInfo = AppInfo.read(); String[] artifacts = getArtifacts(artifactNames, appInfo); + Map repos = getRepositories(extraRepos, appInfo); if (artifacts.length > 0) { - List files = Resolver.create(artifacts).resolvePaths(); + List files = Resolver.create(artifacts, repos).resolvePaths(); SyncStats stats = FileUtils.syncArtifacts(files, directory, noLinks, true); if (artifactNames.length > 0) { for (String dep : artifactNames) { @@ -162,10 +195,26 @@ public SyncStats install(String[] artifactNames) */ public List path(String[] artifactNames) throws DependencyResolutionException, IOException { + return path(artifactNames, Collections.emptyMap()); + } + + /** + * Returns the paths of the given artifacts. If no artifacts are given, the paths for all + * dependencies in the app.yml file will be returned instead. + * + * @param artifactNames The artifacts to get the paths for. + * @param extraRepos A map of additional repository names to URLs where artifacts can be found. + * @return A list of paths. + * @throws DependencyResolutionException If an error occurred during the dependency resolution. + * @throws IOException If an error occurred during the operation. + */ + public List path(String[] artifactNames, Map extraRepos) + throws DependencyResolutionException, IOException { AppInfo appInfo = AppInfo.read(); String[] deps = getArtifacts(artifactNames, appInfo); + Map repos = getRepositories(extraRepos, appInfo); if (deps.length > 0) { - return Resolver.create(deps).resolvePaths(); + return Resolver.create(deps, repos).resolvePaths(); } else { return Collections.emptyList(); } @@ -181,6 +230,12 @@ private static String[] getArtifacts(String[] artifactNames, AppInfo appInfo) { return deps; } + private Map getRepositories(Map extraRepos, AppInfo appInfo) { + Map repos = new HashMap<>(appInfo.repositories); + repos.putAll(extraRepos); + return repos; + } + /** * Executes an action defined in app.yml file. * diff --git a/src/main/java/org/codejive/jpm/Main.java b/src/main/java/org/codejive/jpm/Main.java index f401700..ff35649 100644 --- a/src/main/java/org/codejive/jpm/Main.java +++ b/src/main/java/org/codejive/jpm/Main.java @@ -4,13 +4,15 @@ //DEPS org.yaml:snakeyaml:2.4 //DEPS org.jline:jline-console-ui:3.30.5 org.jline:jline-terminal-jni:3.30.5 //DEPS org.slf4j:slf4j-api:2.0.17 org.slf4j:slf4j-simple:2.0.17 -//SOURCES Jpm.java json/AppInfo.java util/FileUtils.java util/Resolver.java util/ScriptUtils.java +//SOURCES Jpm.java config/AppInfo.java util/FileUtils.java util/Resolver.java util/ScriptUtils.java //SOURCES util/SearchResult.java util/SearchUtils.java util/SyncStats.java util/Version.java // spotless:on package org.codejive.jpm; import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; import java.nio.file.Path; import java.util.*; import java.util.concurrent.Callable; @@ -80,7 +82,10 @@ public Integer call() throws Exception { .directory(artifactsMixin.depsMixin.directory) .noLinks(artifactsMixin.depsMixin.noLinks) .build() - .copy(artifactsMixin.artifactNames, sync); + .copy( + artifactsMixin.artifactNames, + artifactsMixin.getRepositoryMap(), + sync); if (!quietMixin.quiet) { printStats(stats); } @@ -280,7 +285,9 @@ public Integer call() throws Exception { .directory(optionalArtifactsMixin.depsMixin.directory) .noLinks(optionalArtifactsMixin.depsMixin.noLinks) .build() - .install(optionalArtifactsMixin.artifactNames); + .install( + optionalArtifactsMixin.artifactNames, + optionalArtifactsMixin.getRepositoryMap()); if (!quietMixin.quiet) { printStats(stats); } @@ -305,7 +312,9 @@ public Integer call() throws Exception { .directory(optionalArtifactsMixin.depsMixin.directory) .noLinks(optionalArtifactsMixin.depsMixin.noLinks) .build() - .path(optionalArtifactsMixin.artifactNames); + .path( + optionalArtifactsMixin.artifactNames, + optionalArtifactsMixin.getRepositoryMap()); if (!files.isEmpty()) { String classpath = files.stream() @@ -492,9 +501,37 @@ static class DepsMixin { boolean noLinks; } - static class ArtifactsMixin { + static class BaseArtifactsMixin { @Mixin DepsMixin depsMixin; + @Option( + names = {"-r", "--repo"}, + description = + "URL to additional repository to use when resolving artifacts. Can be preceded by a name and an equals sign, e.g. -r myrepo=https://my.repo.com/maven2. When needing to pass user and password you can set JPM_REPO__USER and JPM_REPO__PASSWORD environment variables.") + List repositories = new ArrayList<>(); + + Map getRepositoryMap() { + Map repoMap = new HashMap<>(); + for (String repo : repositories) { + int eq = repo.indexOf('='); + if (eq > 0) { + repoMap.put(repo.substring(0, eq), repo.substring(eq + 1)); + } else { + String name = repo; + try { + URL url = new URL(repo); + name = url.getHost(); + } catch (MalformedURLException e) { + // Ignore + } + repoMap.put(name, repo); + } + } + return repoMap; + } + } + + static class ArtifactsMixin extends BaseArtifactsMixin { @Parameters( paramLabel = "artifacts", description = @@ -503,9 +540,7 @@ static class ArtifactsMixin { private String[] artifactNames = {}; } - static class OptionalArtifactsMixin { - @Mixin DepsMixin depsMixin; - + static class OptionalArtifactsMixin extends BaseArtifactsMixin { @Parameters( paramLabel = "artifacts", description = diff --git a/src/main/java/org/codejive/jpm/json/AppInfo.java b/src/main/java/org/codejive/jpm/config/AppInfo.java similarity index 77% rename from src/main/java/org/codejive/jpm/json/AppInfo.java rename to src/main/java/org/codejive/jpm/config/AppInfo.java index 83ccbfe..74f72f7 100644 --- a/src/main/java/org/codejive/jpm/json/AppInfo.java +++ b/src/main/java/org/codejive/jpm/config/AppInfo.java @@ -1,4 +1,4 @@ -package org.codejive.jpm.json; +package org.codejive.jpm.config; import java.io.IOException; import java.io.Reader; @@ -17,7 +17,9 @@ */ public class AppInfo { private Map yaml = new TreeMap<>(); + public Map dependencies = new TreeMap<>(); + public Map repositories = new TreeMap<>(); public Map actions = new TreeMap<>(); /** The official name of the app.yml file. */ @@ -41,7 +43,7 @@ public String[] getDependencyGAVs() { * @return The action command or null if not found */ public String getAction(String actionName) { - return actions.get(actionName); + return actions != null ? actions.get(actionName) : null; } /** @@ -50,7 +52,7 @@ public String getAction(String actionName) { * @return A set of action names */ public java.util.Set getActionNames() { - return actions.keySet(); + return actions != null ? actions.keySet() : java.util.Collections.emptySet(); } /** @@ -80,6 +82,14 @@ public static AppInfo read() throws IOException { appInfo.dependencies.put(entry.getKey(), entry.getValue().toString()); } } + // Parse repositories section + if (appInfo.yaml.containsKey("repositories") + && appInfo.yaml.get("repositories") instanceof Map) { + Map deps = (Map) appInfo.yaml.get("repositories"); + for (Map.Entry entry : deps.entrySet()) { + appInfo.repositories.put(entry.getKey(), entry.getValue().toString()); + } + } // Parse actions section if (appInfo.yaml.containsKey("actions") && appInfo.yaml.get("actions") instanceof Map) { Map actions = (Map) appInfo.yaml.get("actions"); @@ -105,7 +115,16 @@ public static void write(AppInfo appInfo) throws IOException { Yaml yaml = new Yaml(dopts); // WARNING awful code ahead appInfo.yaml.put("dependencies", (Map) (Map) appInfo.dependencies); - appInfo.yaml.put("actions", (Map) (Map) appInfo.actions); + if (!appInfo.repositories.isEmpty()) { + appInfo.yaml.put("repositories", (Map) (Map) appInfo.repositories); + } else { + appInfo.yaml.remove("repositories"); + } + if (!appInfo.actions.isEmpty()) { + appInfo.yaml.put("actions", (Map) (Map) appInfo.actions); + } else { + appInfo.yaml.remove("actions"); + } yaml.dump(appInfo.yaml, out); } } diff --git a/src/main/java/org/codejive/jpm/util/Resolver.java b/src/main/java/org/codejive/jpm/util/Resolver.java index 2f2e6a7..fa1ca0b 100644 --- a/src/main/java/org/codejive/jpm/util/Resolver.java +++ b/src/main/java/org/codejive/jpm/util/Resolver.java @@ -6,12 +6,15 @@ import eu.maveniverse.maven.mima.context.Runtimes; import java.nio.file.Path; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.collection.CollectRequest; import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.resolution.ArtifactResult; import org.eclipse.aether.resolution.DependencyRequest; import org.eclipse.aether.resolution.DependencyResolutionException; @@ -20,20 +23,22 @@ public class Resolver { private final List artifacts; + private final List repositories; private List resolvedArtifacts; - public static Resolver create(String[] artifactNames) { - return new Resolver(artifactNames); + public static Resolver create(String[] artifactNames, Map repositories) { + return new Resolver(artifactNames, repositories); } - private Resolver(String[] artifactNames) { + private Resolver(String[] artifactNames, Map repos) { artifacts = parseArtifacts(artifactNames); + repositories = parseRepositories(repos); } public List resolve() throws DependencyResolutionException { if (resolvedArtifacts == null) { - resolvedArtifacts = resolveArtifacts(artifacts); + resolvedArtifacts = resolveArtifacts(artifacts, repositories); } return resolvedArtifacts; } @@ -45,17 +50,6 @@ public List resolvePaths() throws DependencyResolutionException { .collect(Collectors.toList()); } - /** - * Parses the given artifact names into a list of {@link Artifact} instances. - * - * @param artifactNames the artifact names to parse as an array of strings in the format - * "groupId:artifactId:version" - * @return a list of {@link Artifact} instances - */ - public static List parseArtifacts(String[] artifactNames) { - return Arrays.stream(artifactNames).map(DefaultArtifact::new).collect(Collectors.toList()); - } - /** * Resolves the given artifacts. * @@ -63,13 +57,15 @@ public static List parseArtifacts(String[] artifactNames) { * @return the resolved artifacts as a list of {@link ArtifactResult} instances * @throws DependencyResolutionException if an error occurs while resolving the artifacts */ - public static List resolveArtifacts(List artifacts) + public static List resolveArtifacts( + List artifacts, List repositories) throws DependencyResolutionException { List dependencies = artifacts.stream() .map(a -> new Dependency(a, JavaScopes.RUNTIME)) .collect(Collectors.toList()); - ContextOverrides overrides = ContextOverrides.create().build(); + ContextOverrides overrides = + ContextOverrides.create().withUserSettings(true).repositories(repositories).build(); Runtime runtime = Runtimes.INSTANCE.getRuntime(); try (Context context = runtime.create(overrides)) { CollectRequest collectRequest = @@ -86,4 +82,17 @@ public static List resolveArtifacts(List artifacts) return dependencyResult.getArtifactResults(); } } + + private static List parseArtifacts(String[] artifactNames) { + return Arrays.stream(artifactNames).map(DefaultArtifact::new).collect(Collectors.toList()); + } + + private static List parseRepositories(Map repositories) { + if (repositories == null) { + return Collections.emptyList(); + } + return repositories.entrySet().stream() + .map(e -> new RemoteRepository.Builder(e.getKey(), "default", e.getValue()).build()) + .collect(Collectors.toList()); + } } diff --git a/src/test/java/org/codejive/jpm/json/AppInfoTest.java b/src/test/java/org/codejive/jpm/config/AppInfoTest.java similarity index 99% rename from src/test/java/org/codejive/jpm/json/AppInfoTest.java rename to src/test/java/org/codejive/jpm/config/AppInfoTest.java index d269112..b02c8cf 100644 --- a/src/test/java/org/codejive/jpm/json/AppInfoTest.java +++ b/src/test/java/org/codejive/jpm/config/AppInfoTest.java @@ -1,4 +1,4 @@ -package org.codejive.jpm.json; +package org.codejive.jpm.config; import static org.assertj.core.api.Assertions.*;