diff --git a/src/main/groovy/org/codehaus/groovy/tools/GrapeMain.groovy b/src/main/groovy/org/codehaus/groovy/tools/GrapeMain.groovy index 491757e6c6f..a0009a9944b 100644 --- a/src/main/groovy/org/codehaus/groovy/tools/GrapeMain.groovy +++ b/src/main/groovy/org/codehaus/groovy/tools/GrapeMain.groovy @@ -150,18 +150,19 @@ class GrapeMain implements Runnable { } @Command(name = 'install', header = 'Installs a particular grape', - description = 'Installs the specified groovy module or maven artifact. If a version is specified that specific version will be installed, otherwise the most recent version will be used (as if `*` was passed in).') + description = ['Installs the specified groovy module or maven artifact. If a version is specified that specific version will be installed, otherwise the most recent version will be used (as if `*` was passed in).', + 'Accepts three coordinate forms: `group module [version] [classifier]` (positional), `group:module:version[:classifier][@ext]` (Maven shorthand), or `group#module;version` (Ivy shorthand).']) private static class Install implements Runnable { /** - * Module group to install. + * Module group to install, or a full coordinate in Maven/Ivy shorthand. */ - @Parameters(index = '0', arity = '1', description = 'Which module group the module comes from. Translates directly to a Maven groupId or an Ivy Organization. Any group matching /groovy[x][\\..*]^/ is reserved and may have special meaning to the groovy endorsed modules.') + @Parameters(index = '0', arity = '1', description = 'Either the module group (Maven groupId / Ivy organisation), or a full coordinate in Maven shorthand `g:m:v[:c][@e]` or Ivy shorthand `g#m;v`. Any group matching /groovy[x][\\..*]^/ is reserved and may have special meaning to the groovy endorsed modules.') String group /** - * Module name to install. + * Module name to install. Optional when a shorthand coordinate is supplied as the first parameter. */ - @Parameters(index = '1', arity = '1', description = 'The name of the module to load. Translated directly to a Maven artifactId or an Ivy artifact.') + @Parameters(index = '1', arity = '0..1', description = 'The name of the module to load. Translated directly to a Maven artifactId or an Ivy artifact. Omit when supplying a shorthand coordinate.') String module /** @@ -203,7 +204,24 @@ class GrapeMain implements Runnable { throw new CommandLine.ExecutionException(new CommandLine(this), "Grape engine not initialized") } - def result = engine.grab(autoDownload: true, group: group, module: module, version: version, classifier: classifier, noExceptions: true) + // If the first positional carries Maven/Ivy shorthand separators, parse it and let any + // explicit later positionals override what the shorthand supplied. + String g = group, m = module, v = version, c = classifier + if (g != null && (g.indexOf(':') >= 0 || g.indexOf('#') >= 0)) { + Map parts = GrapeUtil.getIvyParts(g) + if (parts.get('group') && parts.get('module')) { + g = parts.group + if (!m) m = parts.module as String + if (v == '*' && parts.version) v = parts.version as String + if (!c && parts.classifier) c = parts.classifier as String + } + } + if (!m) { + System.err.println "Missing module: pass three positionals or a shorthand like 'g:m:v'" + throw new CommandLine.ExecutionException(new CommandLine(this), "Missing module") + } + + def result = engine.grab(autoDownload: true, group: g, module: m, version: v, classifier: c, noExceptions: true) if (result instanceof Exception) { System.err.println "Error grabbing Grapes -- ${result.message}" throw new CommandLine.ExecutionException(new CommandLine(this), "Failed to install grape", result) diff --git a/src/main/java/groovy/grape/Grape.java b/src/main/java/groovy/grape/Grape.java index cde00194b34..654696c241c 100644 --- a/src/main/java/groovy/grape/Grape.java +++ b/src/main/java/groovy/grape/Grape.java @@ -18,6 +18,8 @@ */ package groovy.grape; +import org.codehaus.groovy.tools.GrapeUtil; + import java.net.URI; import java.util.Collections; import java.util.LinkedHashMap; @@ -226,14 +228,29 @@ private static GrapeEngine createEngineFromProvider(final ServiceLoader.Provider } /** - * Grabs a dependency expressed using the endorsed module shorthand. + * Grabs a dependency expressed as a single string. + *

+ * Recognized forms: + *

* - * @param endorsed the endorsed module notation + * @param endorsed the dependency notation */ public static void grab(String endorsed) { if (enableGrapes) { GrapeEngine instance = getInstance(); if (instance != null) { + if (endorsed != null && (endorsed.indexOf(':') >= 0 || endorsed.indexOf('#') >= 0)) { + Map parts = GrapeUtil.getIvyParts(endorsed); + if (parts.get("group") != null && parts.get("module") != null) { + grab(parts); + return; + } + } instance.grab(endorsed); } } diff --git a/src/main/java/org/codehaus/groovy/tools/GrapeUtil.java b/src/main/java/org/codehaus/groovy/tools/GrapeUtil.java index 7a0ed4f7933..3289289a8c1 100644 --- a/src/main/java/org/codehaus/groovy/tools/GrapeUtil.java +++ b/src/main/java/org/codehaus/groovy/tools/GrapeUtil.java @@ -26,8 +26,14 @@ */ public class GrapeUtil { /** - * Parses a dependency coordinate in Ivy/Grape shorthand form into its + * Parses a dependency coordinate in Maven or Ivy shorthand form into its * component parts. + *

+ * Recognized forms: + *

* * @param allstr the dependency coordinate to parse * @return a map containing any parsed {@code group}, {@code module}, @@ -35,6 +41,12 @@ public class GrapeUtil { */ public static Map getIvyParts(String allstr) { Map result = new LinkedHashMap(); + if (allstr == null) return result; + // Accept the Ivy shorthand "group#module;version" by translating its separators to + // the Maven form before parsing. Neither '#' nor ';' is legal in a groupId, artifactId, + // or version (Ivy ranges use '[]()' not '#;'), so a straight replace cannot collide + // with the Maven form. + allstr = allstr.replace('#', ':').replace(';', ':'); String ext = ""; String[] parts; if (allstr.contains("@")) { diff --git a/src/test/groovy/org/codehaus/groovy/tools/GrapeUtilTest.java b/src/test/groovy/org/codehaus/groovy/tools/GrapeUtilTest.java index 6596f66ea42..ebd81346481 100644 --- a/src/test/groovy/org/codehaus/groovy/tools/GrapeUtilTest.java +++ b/src/test/groovy/org/codehaus/groovy/tools/GrapeUtilTest.java @@ -47,4 +47,52 @@ public void testGetIvyParts4(){ assert ivyParts.size() == 4; } + public void testMavenShorthand_groupModuleVersion(){ + Map parts = GrapeUtil.getIvyParts("com.example:foo:1.2.3"); + assert "com.example".equals(parts.get("group")); + assert "foo".equals(parts.get("module")); + assert "1.2.3".equals(parts.get("version")); + } + + public void testMavenShorthand_versionDefaultsToWildcard(){ + Map parts = GrapeUtil.getIvyParts("com.example:foo"); + assert "com.example".equals(parts.get("group")); + assert "foo".equals(parts.get("module")); + assert "*".equals(parts.get("version")); + } + + public void testMavenShorthand_withClassifierAndExt(){ + Map parts = GrapeUtil.getIvyParts("com.example:foo:1.2.3:jdk15@zip"); + assert "com.example".equals(parts.get("group")); + assert "foo".equals(parts.get("module")); + assert "1.2.3".equals(parts.get("version")); + assert "jdk15".equals(parts.get("classifier")); + assert "zip".equals(parts.get("ext")); + } + + public void testIvyShorthand_groupModuleVersion(){ + Map parts = GrapeUtil.getIvyParts("com.example#foo;1.2.3"); + assert "com.example".equals(parts.get("group")); + assert "foo".equals(parts.get("module")); + assert "1.2.3".equals(parts.get("version")); + } + + public void testIvyShorthand_dottedAndHyphenatedNames(){ + Map parts = GrapeUtil.getIvyParts("org.apache.commons#commons-lang3;3.9"); + assert "org.apache.commons".equals(parts.get("group")); + assert "commons-lang3".equals(parts.get("module")); + assert "3.9".equals(parts.get("version")); + } + + public void testIvyShorthand_versionRange(){ + Map parts = GrapeUtil.getIvyParts("com.example#foo;[1.0,2.0)"); + assert "com.example".equals(parts.get("group")); + assert "foo".equals(parts.get("module")); + assert "[1.0,2.0)".equals(parts.get("version")); + } + + public void testNullInput_returnsEmpty(){ + Map parts = GrapeUtil.getIvyParts(null); + assert parts.isEmpty(); + } }