From 3087feb9f04056d6e1d7168da01e58d3bf63f529 Mon Sep 17 00:00:00 2001 From: Max Rydahl Andersen Date: Fri, 1 Aug 2025 12:07:10 +0200 Subject: [PATCH 1/5] feat: initial parsing of dependency attributes --- build.gradle | 3 + .../dependencies/DependencyAttributes.java | 29 ++++ .../jbang/dependencies/MavenCoordinate.java | 27 +++- .../java/dev/jbang/util/AttributeParser.java | 112 ++++++++++++++ .../dependencies/DependencyResolverTest.java | 145 +++++++++++------- .../dev/jbang/util/AttributeParserTest.java | 86 +++++++++++ 6 files changed, 341 insertions(+), 61 deletions(-) create mode 100644 src/main/java/dev/jbang/dependencies/DependencyAttributes.java create mode 100644 src/main/java/dev/jbang/util/AttributeParser.java create mode 100644 src/test/java/dev/jbang/util/AttributeParserTest.java diff --git a/build.gradle b/build.gradle index d2024f1e6..94f2e477e 100644 --- a/build.gradle +++ b/build.gradle @@ -346,6 +346,9 @@ testing { suites { test { useJUnitJupiter() + dependencies { + implementation "org.assertj:assertj-core:3.24.2" + } } integrationTest(JvmTestSuite) { diff --git a/src/main/java/dev/jbang/dependencies/DependencyAttributes.java b/src/main/java/dev/jbang/dependencies/DependencyAttributes.java new file mode 100644 index 000000000..92cabfb19 --- /dev/null +++ b/src/main/java/dev/jbang/dependencies/DependencyAttributes.java @@ -0,0 +1,29 @@ +package dev.jbang.dependencies; + +import java.util.*; + +public class DependencyAttributes { + + private final Map> attributes; + + // Java 8 Set.of() is sad :) + static Set defaultScopes = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("build", "run"))); + Set scopes; + + public DependencyAttributes(Map> attributes) { + this.attributes = attributes; + } + + // lazy computed; dont store hashset unless truly needed + public boolean includeInScope(String scope) { + if (scopes == null) { + if (attributes.containsKey("scope")) { + scopes = new HashSet<>(attributes.get("scope")); + } else { + scopes = defaultScopes; + } + } + return scopes.contains(scope); + } + +} diff --git a/src/main/java/dev/jbang/dependencies/MavenCoordinate.java b/src/main/java/dev/jbang/dependencies/MavenCoordinate.java index 4184bf901..26a9037d5 100644 --- a/src/main/java/dev/jbang/dependencies/MavenCoordinate.java +++ b/src/main/java/dev/jbang/dependencies/MavenCoordinate.java @@ -1,5 +1,8 @@ package dev.jbang.dependencies; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -7,6 +10,8 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import dev.jbang.util.AttributeParser; + /** * * MavenCoordinate should not implement euqals as it is not meaningfull to use @@ -25,12 +30,13 @@ public class MavenCoordinate { private final String version; private final String classifier; private final String type; + private final DependencyAttributes attributes; public static final String DUMMY_GROUP = "group"; public static final String DEFAULT_VERSION = "999-SNAPSHOT"; private static final Pattern gavPattern = Pattern.compile( - "^(?[^:]*):(?[^:]*)(:(?[^:@]*))?(:(?[^@]*))?(@(?.*))?$"); + "^(?[^:]*):(?[^:]*)(:(?[^:@{]*))?(:(?[^@]*))?(@(?.*))?(\\{(?.*)})?$"); private static final Pattern canonicalPattern = Pattern.compile( "^(?[^:]*):(?[^:]*)((:(?.*)(:(?[^@]*))?)?:(?[^:@]*))?$"); @@ -56,6 +62,10 @@ public String getType() { return type; } + public DependencyAttributes getAttributes() { + return attributes; + } + public static MavenCoordinate fromString(String depId) { return parse(depId, gavPattern); } @@ -79,7 +89,7 @@ public static MavenCoordinate fromCanonicalString(String depId) { private static MavenCoordinate parse(String depId, Pattern pattern) { Matcher gav = pattern.matcher(depId); - gav.find(); + // gav.find(); if (!gav.matches()) { throw new IllegalStateException(String.format( @@ -92,8 +102,9 @@ private static MavenCoordinate parse(String depId, Pattern pattern) { String version = DependencyUtil.formatVersion(gav.group("version")); String classifier = gav.group("classifier"); String type = Optional.ofNullable(gav.group("type")).orElse("jar"); - - return new MavenCoordinate(groupId, artifactId, version, classifier, type); + String propString = gav.group("properties"); + Map> properties = AttributeParser.parseAttributeList(propString, "scope"); + return new MavenCoordinate(groupId, artifactId, version, classifier, type, properties); } public MavenCoordinate(@Nonnull String groupId, @Nonnull String artifactId, @Nonnull String version) { @@ -102,11 +113,18 @@ public MavenCoordinate(@Nonnull String groupId, @Nonnull String artifactId, @Non public MavenCoordinate(@Nonnull String groupId, @Nonnull String artifactId, @Nonnull String version, @Nullable String classifier, @Nullable String type) { + this(groupId, artifactId, version, classifier, type, null); + } + + public MavenCoordinate(@Nonnull String groupId, @Nonnull String artifactId, @Nonnull String version, + @Nullable String classifier, @Nullable String type, @Nullable Map> attributes) { this.groupId = groupId; this.artifactId = artifactId; this.version = version; this.classifier = classifier != null && classifier.isEmpty() ? null : classifier; this.type = type; + this.attributes = attributes == null ? new DependencyAttributes(Collections.emptyMap()) + : new DependencyAttributes(attributes); } public MavenCoordinate withVersion() { @@ -163,6 +181,7 @@ public String toString() { ", version='" + version + '\'' + ", classifier='" + classifier + '\'' + ", type='" + type + '\'' + + ", attributes=" + attributes + '}'; } } diff --git a/src/main/java/dev/jbang/util/AttributeParser.java b/src/main/java/dev/jbang/util/AttributeParser.java new file mode 100644 index 000000000..814966a44 --- /dev/null +++ b/src/main/java/dev/jbang/util/AttributeParser.java @@ -0,0 +1,112 @@ +package dev.jbang.util; + +import java.util.*; + +//handle {} similar to how Asciidoc(tor) interpret attribute lists: https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/ +//Derived from https://github.com/yupiik/tools-maven-plugin/blob/d1e8b97ebcae032684a0fafac426bcc25819089f/asciidoc-java/src/main/java/io/yupiik/asciidoc/parser/Parser.java#L1634 + +public class AttributeParser { + + public static Map> parseAttributeList(String input, String defaultKey) { + Map> result = new LinkedHashMap<>(); + List unnamed = new ArrayList<>(); + + for (String token : tokenize(input)) { + if (token.isEmpty()) + continue; + + if (token.contains("=")) { + String[] kv = token.split("=", 2); + String key = kv[0].trim(); + String value = unquote(kv[1].trim()); + + if (key.equals(defaultKey)) { + unnamed.add(value); + } else { + result.put(key, Collections.singletonList(value)); + } + } else if (token.contains("%")) { + String[] parts = token.split("%"); + int startIdx = 0; + + if (!parts[0].isEmpty()) { + unnamed.add(unquote(parts[0])); + startIdx = 1; + } + + for (int i = startIdx; i < parts.length; i++) { + if (!parts[i].isEmpty()) { + result.put(parts[i], Collections.singletonList("true")); + } + } + } else { + unnamed.add(unquote(token)); + } + } + + if (!unnamed.isEmpty()) { + result.put(defaultKey, unnamed); + } + + return result; + } + + private static List tokenize(String input) { + if (input == null) + return Collections.emptyList(); + List tokens = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean inSingleQuote = false, inDoubleQuote = false; + + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + + if (c == ',' && !inSingleQuote && !inDoubleQuote) { + tokens.add(current.toString().trim()); + current.setLength(0); + } else { + if (c == '"' && !inSingleQuote) { + if (i == 0 || input.charAt(i - 1) != '\\') { + inDoubleQuote = !inDoubleQuote; + } + } else if (c == '\'' && !inDoubleQuote) { + if (i == 0 || input.charAt(i - 1) != '\\') { + inSingleQuote = !inSingleQuote; + } + } + current.append(c); + } + } + + if (current.length() > 0) { + tokens.add(current.toString().trim()); + } + + return tokens; + } + + private static String unquote(String s) { + if (s.length() < 2) + return s; + + char first = s.charAt(0); + char last = s.charAt(s.length() - 1); + + if ((first == '"' || first == '\'') && last != first) { + return s; + } + + if ((first == '"' && last == '"') || (first == '\'' && last == '\'')) { + String inner = s.substring(1, s.length() - 1); + return first == '"' ? inner.replace("\\\"", "\"") : inner.replace("\\'", "'"); + } + + return s; + } + + public static void main(String[] args) { + String input = "basic,run,scope=herewego,%transitive%import"; + Map> parsed = parseAttributeList(input, "scope"); + parsed.forEach((k, v) -> System.out.println("\"" + k + "\" = " + v)); + } +} diff --git a/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java b/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java index 5709b3976..e74e16440 100644 --- a/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java +++ b/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java @@ -2,16 +2,7 @@ import static dev.jbang.dependencies.DependencyUtil.toMavenRepo; import static dev.jbang.util.JavaUtil.defaultJdkManager; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.arrayWithSize; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.endsWith; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasItem; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.not; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.*; import java.io.File; import java.io.IOException; @@ -36,7 +27,7 @@ class DependencyResolverTest extends BaseTest { @Test void testFormatVersion() { - assertEquals("[1.0,)", DependencyUtil.formatVersion("1.0+")); + assertThat(DependencyUtil.formatVersion("1.0+")).isEqualTo("[1.0,)"); } @BeforeEach @@ -47,37 +38,77 @@ void clearCache() { @Test void testdepIdToArtifact() { MavenCoordinate artifact = MavenCoordinate.fromString("com.offbytwo:docopt:0.6.0.20150202:redhat@doc"); - assertEquals("com.offbytwo", artifact.getGroupId()); - assertEquals("docopt", artifact.getArtifactId()); - assertEquals("0.6.0.20150202", artifact.getVersion()); - assertEquals("redhat", artifact.getClassifier()); - assertEquals("doc", artifact.getType()); + assertThat(artifact.getGroupId()).isEqualTo("com.offbytwo"); + assertThat(artifact.getArtifactId()).isEqualTo("docopt"); + assertThat(artifact.getVersion()).isEqualTo("0.6.0.20150202"); + assertThat(artifact.getClassifier()).isEqualTo("redhat"); + assertThat(artifact.getType()).isEqualTo("doc"); artifact = MavenCoordinate.fromString("com.offbytwo:docopt:0.6.0.20150202"); - assertEquals("com.offbytwo", artifact.getGroupId()); - assertEquals("docopt", artifact.getArtifactId()); - assertEquals("0.6.0.20150202", artifact.getVersion()); - assertNull(artifact.getClassifier()); - assertEquals("jar", artifact.getType()); + assertThat(artifact.getGroupId()).isEqualTo("com.offbytwo"); + assertThat(artifact.getArtifactId()).isEqualTo("docopt"); + assertThat(artifact.getVersion()).isEqualTo("0.6.0.20150202"); + assertThat(artifact.getClassifier()).isNull(); + assertThat(artifact.getType()).isEqualTo("jar"); artifact = MavenCoordinate.fromString("com.offbytwo:docopt:0.6+"); - assertEquals("com.offbytwo", artifact.getGroupId()); - assertEquals("docopt", artifact.getArtifactId()); - assertEquals("[0.6,)", artifact.getVersion()); - assertNull(artifact.getClassifier()); - assertEquals("jar", artifact.getType()); + assertThat(artifact.getGroupId()).isEqualTo("com.offbytwo"); + assertThat(artifact.getArtifactId()).isEqualTo("docopt"); + assertThat(artifact.getVersion()).isEqualTo("[0.6,)"); + assertThat(artifact.getClassifier()).isNull(); + assertThat(artifact.getType()).isEqualTo("jar"); - assertThrows(IllegalStateException.class, () -> MavenCoordinate.fromString("bla?f")); + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> MavenCoordinate.fromString("bla?f")); + } + + @Test + void testScopes() { + + MavenCoordinate artifact = MavenCoordinate.fromString("a.b:c:1.2{build}"); + + assertThat(artifact) + .extracting("groupId", "artifactId", "version", "classifier") + .containsExactly("a.b", "c", "1.2", null); + + assertThat(artifact.getAttributes().includeInScope("build")).isTrue(); + + } + + @Test + void testMultiScopes() { + + MavenCoordinate artifact = MavenCoordinate.fromString("a.b:c:1.2{build}"); + + assertThat(artifact) + .extracting("groupId", "artifactId", "version", "classifier") + .containsExactly("a.b", "c", "1.2", null); + + assertThat(artifact.getAttributes().includeInScope("build")).isTrue(); + assertThat(artifact.getAttributes().includeInScope("compile")).isFalse(); + assertThat(artifact.getAttributes().includeInScope("doesnotexist")).isFalse(); + } + + @Test + void testOtherProperties() { + + MavenCoordinate artifact = MavenCoordinate.fromString("a.b:c:1.2"); + + assertThat(artifact) + .extracting("groupId", "artifactId", "version", "classifier") + .containsExactly("a.b", "c", "1.2", null); + + assertThat(artifact.getAttributes().includeInScope("build")).isTrue(); + assertThat(artifact.getAttributes().includeInScope("run")).isTrue(); } @Test void testdecodeEnv() { - assertThrows(IllegalStateException.class, () -> DependencyUtil.decodeEnv("{{wonka}}")); - assertEquals("wonka", DependencyUtil.decodeEnv("wonka")); + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> DependencyUtil.decodeEnv("{{wonka}}")); + assertThat(DependencyUtil.decodeEnv("wonka")).isEqualTo("wonka"); environmentVariables.set("test.value", "wonka"); - assertEquals("wonka", DependencyUtil.decodeEnv("{{test.value}}")); + assertThat(DependencyUtil.decodeEnv("{{test.value}}")).isEqualTo("wonka"); } @Test @@ -90,11 +121,11 @@ void testdepIdWithPlaceHoldersToArtifact() { "com.example:my-native-library:1.0.0:${os.detected.jfxname}", p); MavenCoordinate artifact = MavenCoordinate.fromString(gav); - assertEquals("com.example", artifact.getGroupId()); - assertEquals("my-native-library", artifact.getArtifactId()); - assertEquals("1.0.0", artifact.getVersion()); - assertEquals(p.getProperty("os.detected.jfxname"), artifact.getClassifier()); - assertEquals("jar", artifact.getType()); + assertThat(artifact.getGroupId()).isEqualTo("com.example"); + assertThat(artifact.getArtifactId()).isEqualTo("my-native-library"); + assertThat(artifact.getVersion()).isEqualTo("1.0.0"); + assertThat(artifact.getClassifier()).isEqualTo(p.getProperty("os.detected.jfxname")); + assertThat(artifact.getType()).isEqualTo("jar"); } @Test @@ -106,14 +137,14 @@ void testResolveJavaFXWithAether() { List deps = Collections.singletonList( PropertiesValueResolver.replaceProperties("org.openjfx:javafx-base:18.0.2:${os.detected.jfxname}", p)); List artifacts = ArtifactResolver.Builder.create().build().resolve(deps); - assertEquals(1, artifacts.size()); + assertThat(artifacts.size()).isEqualTo(1); } @Test void testResolveDependenciesWithAether() { List deps = Arrays.asList("com.offbytwo:docopt:0.6.0.20150202", "log4j:log4j:1.2+"); List artifacts = ArtifactResolver.Builder.create().build().resolve(deps); - assertEquals(2, artifacts.size()); + assertThat(artifacts.size()).isEqualTo(2); } @Test @@ -124,14 +155,15 @@ void testResolveDependenciesAltRepo(@TempDir File altrepo) { .localFolder(altrepo.toPath()) .build() .resolve(deps); - assertEquals(2, artifacts.size()); - assertThat(altrepo.listFiles(), arrayWithSize(4)); + assertThat(artifacts.size()).isEqualTo(2); + assertThat(altrepo.listFiles()).hasSize(4); } @Test void testRedHatJBossRepos() { - assertEquals(toMavenRepo("jbossorg").getUrl(), "https://repository.jboss.org/nexus/content/groups/public/"); - assertEquals(toMavenRepo("redhat").getUrl(), "https://maven.repository.redhat.com/ga/"); + assertThat("https://repository.jboss.org/nexus/content/groups/public/") + .isEqualTo(toMavenRepo("jbossorg").getUrl()); + assertThat("https://maven.repository.redhat.com/ga/").isEqualTo(toMavenRepo("redhat").getUrl()); } @Test @@ -143,7 +175,7 @@ void testResolveDependencies() { true, false); // if returns 5 its because optional deps are included which they shouldn't - assertEquals(2, classpath.getClassPaths().size()); + assertThat(classpath.getClassPaths().size()).isEqualTo(2); } @Test @@ -162,7 +194,7 @@ void testResolveDependenciesNoDuplicates() { HashSet othercps = new HashSet<>(cps); - assertThat(cps, containsInAnyOrder(othercps.toArray())); + assertThat(cps).containsAll(othercps); } @Test @@ -177,7 +209,7 @@ void testResolveNativeDependencies() { false, true, false); - assertEquals(46, classpath.getClassPaths().size()); + assertThat(classpath.getClassPaths().size()).isEqualTo(46); } @Test @@ -196,11 +228,11 @@ protected boolean supportsModules(Jdk jdk) { List ma = cp.getAutoDectectedModuleArguments(defaultJdkManager().getOrInstallJdk(null)); - assertThat(ma, hasItem("--module-path")); + assertThat(ma).contains("--module-path"); - assertThat(ma, not(hasItem("docopt"))); + assertThat(ma).doesNotContain("docopt"); - assertThat(cp.getClassPath(), containsString("docopt")); + assertThat(cp.getClassPath()).contains("docopt"); } @Test @@ -211,7 +243,7 @@ void testImportPOM() { false, true, false); - assertEquals(62, classpath.getArtifacts().size()); + assertThat(classpath.getArtifacts().size()).isEqualTo(62); } @Test @@ -235,14 +267,14 @@ void testImportMultipleBoms() { .startsWith("io.vertx:vertx-core")) .findFirst(); - assertEquals("4.2.3", coord.get().getCoordinate().getVersion()); + assertThat(coord.get().getCoordinate().getVersion()).isEqualTo("4.2.3"); coord = classpath.getArtifacts() .stream() .filter(ai -> ai.getCoordinate().toCanonicalForm().startsWith("org.slf4j:slf4j-simple:")) .findFirst(); - assertEquals("1.7.30", coord.get().getCoordinate().getVersion()); + assertThat(coord.get().getCoordinate().getVersion()).isEqualTo("1.7.30"); coord = classpath.getArtifacts() .stream() @@ -251,7 +283,7 @@ void testImportMultipleBoms() { .startsWith("org.apache.camel:camel-vertx")) .findFirst(); - assertEquals(coord.get().getCoordinate().getVersion(), "3.9.0"); + assertThat("3.9.0").isEqualTo(coord.get().getCoordinate().getVersion()); deps = Arrays.asList( "org.apache.camel:camel-bom:3.9.0@pom", @@ -265,7 +297,7 @@ void testImportMultipleBoms() { .filter(ai -> ai.getCoordinate().toCanonicalForm().startsWith("io.vertx:vertx-core")) .findFirst(); - assertEquals("3.9.5", coord.get().getCoordinate().getVersion()); + assertThat(coord.get().getCoordinate().getVersion()).isEqualTo("3.9.5"); } @Test @@ -276,12 +308,11 @@ void testResolveTestJar() { false, true, false); - assertThat(classpath.getArtifacts(), hasSize(7)); + assertThat(classpath.getArtifacts()).hasSize(7); ArtifactInfo ai = classpath.getArtifacts().get(0); - assertThat(ai.getCoordinate().toCanonicalForm(), - equalTo("org.infinispan:infinispan-commons:tests:jar:13.0.5.Final")); - assertThat(ai.getFile().toString(), - endsWith("infinispan-commons-13.0.5.Final-tests.jar")); + assertThat(ai.getCoordinate().toCanonicalForm()) + .isEqualTo("org.infinispan:infinispan-commons:tests:jar:13.0.5.Final"); + assertThat(ai.getFile().toString()).endsWith("infinispan-commons-13.0.5.Final-tests.jar"); } } diff --git a/src/test/java/dev/jbang/util/AttributeParserTest.java b/src/test/java/dev/jbang/util/AttributeParserTest.java new file mode 100644 index 000000000..567c38ade --- /dev/null +++ b/src/test/java/dev/jbang/util/AttributeParserTest.java @@ -0,0 +1,86 @@ +package dev.jbang.util; + +import static dev.jbang.util.AttributeParser.parseAttributeList; +import static java.util.List.of; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +public class AttributeParserTest { + + Map> parse(String str) { + return parse(str, ""); + } + + Map> parse(String str, String def) { + return parseAttributeList(str, def); + } + + @Test + public void testEmpty() { + assertThat(parse("")).isEmpty(); + } + + @Test + public void testParseBasic() { + assertThat(parse("basic")) + .isNotEmpty() + .contains(entry("", of("basic"))); + } + + @Test + public void testParseBoolean() { + assertThat(parse("%transitive")) + .isNotEmpty() + .contains(entry("transitive", of("true"))); + } + + @Test + public void testList() { + assertThat(parse("basic,run")) + .isNotEmpty() + .contains(entry("", of("basic", "run"))); + } + + @Test + public void testMultipleBooleans() { + assertThat(parse("%transitive%import")) + .isNotEmpty() + .contains(entry("transitive", of("true")), entry("import", of("true"))); + } + + @Test + public void testNamedAttributes() { + assertThat(parse("my=basic,run")) + .isNotEmpty() + .hasSize(2) + .contains(entry("my", of("basic")), entry("", of("run"))); + } + + @Test + public void testCombinedMultipleBooleans() { + assertThat(parse("basic,run,scope=herewego,%transitive%import", "scope")) + .isNotEmpty() + .hasSize(3) + .contains( + entry("transitive", of("true")), + entry("import", of("true")), + entry("scope", of("basic", "run", "herewego"))); + } + + @Test + public void testQuotes() { + assertThat(parse("'basic,run' ,scope=\"here 'we' \\\"go\\\" \",%transitive%import", "scope")) + .isNotEmpty() + .hasSize(3) + .contains( + entry("transitive", of("true")), + entry("import", of("true")), + entry("scope", of("basic,run", "here 'we' \"go\" "))); + } + +} From bfd38a199b48e4ad354b6c3648209d3e702243ba Mon Sep 17 00:00:00 2001 From: Max Rydahl Andersen Date: Sat, 2 Aug 2025 01:36:22 +0200 Subject: [PATCH 2/5] chore: implement working equals. Add Tests showing problems --- .../dependencies/DependencyAttributes.java | 32 +++++++++++++++ .../jbang/dependencies/MavenCoordinate.java | 17 +++++++- .../java/dev/jbang/util/AttributeParser.java | 40 +++++++++++++++++-- .../dependencies/DependencyResolverTest.java | 32 +++++++++++++++ 4 files changed, 116 insertions(+), 5 deletions(-) diff --git a/src/main/java/dev/jbang/dependencies/DependencyAttributes.java b/src/main/java/dev/jbang/dependencies/DependencyAttributes.java index 92cabfb19..96fd8d12e 100644 --- a/src/main/java/dev/jbang/dependencies/DependencyAttributes.java +++ b/src/main/java/dev/jbang/dependencies/DependencyAttributes.java @@ -1,6 +1,9 @@ package dev.jbang.dependencies; +import dev.jbang.util.AttributeParser; + import java.util.*; +import java.util.stream.Collectors; public class DependencyAttributes { @@ -26,4 +29,33 @@ public boolean includeInScope(String scope) { return scopes.contains(scope); } + /** + * is there something configured? even if default ? + * @return + */ + public boolean isDefault() { + return !attributes.isEmpty(); + } + + public String toStringFormat() { + return AttributeParser.toStringRep(attributes, "scope"); + } + + @Override + public String toString() { + return toStringFormat(); + } + + @Override + public boolean equals(Object o) { + //TODO: this wont make g:a:v and g:a:v{build,run} equal ...even if they are... + if (o == null || getClass() != o.getClass()) return false; + DependencyAttributes that = (DependencyAttributes) o; + return Objects.equals(attributes, that.attributes); + } + + @Override + public int hashCode() { + return Objects.hashCode(attributes); + } } diff --git a/src/main/java/dev/jbang/dependencies/MavenCoordinate.java b/src/main/java/dev/jbang/dependencies/MavenCoordinate.java index 26a9037d5..892ea0783 100644 --- a/src/main/java/dev/jbang/dependencies/MavenCoordinate.java +++ b/src/main/java/dev/jbang/dependencies/MavenCoordinate.java @@ -36,7 +36,7 @@ public class MavenCoordinate { public static final String DEFAULT_VERSION = "999-SNAPSHOT"; private static final Pattern gavPattern = Pattern.compile( - "^(?[^:]*):(?[^:]*)(:(?[^:@{]*))?(:(?[^@]*))?(@(?.*))?(\\{(?.*)})?$"); + "^(?[^:]*):(?[^:]*)(:(?[^:@{]*))?(:(?[^@{]*))?(@(?.[^{]*))?(\\{(?.*)})?$"); private static final Pattern canonicalPattern = Pattern.compile( "^(?[^:]*):(?[^:]*)((:(?.*)(:(?[^@]*))?)?:(?[^:@]*))?$"); @@ -149,6 +149,9 @@ public String toMavenString() { if (type != null && !type.isEmpty()) { out += "@" + type; } + if(attributes != null && !attributes.isDefault()) { + out += "{" + attributes.toStringFormat() + "}"; + } return out; } @@ -184,4 +187,16 @@ public String toString() { ", attributes=" + attributes + '}'; } + + @Override + public String toString() { + return "MavenCoordinate{" + + "groupId='" + groupId + '\'' + + ", artifactId='" + artifactId + '\'' + + ", version='" + version + '\'' + + ", classifier='" + classifier + '\'' + + ", type='" + type + '\'' + + ", attributes=" + attributes + + '}'; + } } diff --git a/src/main/java/dev/jbang/util/AttributeParser.java b/src/main/java/dev/jbang/util/AttributeParser.java index 814966a44..8f73f512d 100644 --- a/src/main/java/dev/jbang/util/AttributeParser.java +++ b/src/main/java/dev/jbang/util/AttributeParser.java @@ -104,9 +104,41 @@ private static String unquote(String s) { return s; } - public static void main(String[] args) { - String input = "basic,run,scope=herewego,%transitive%import"; - Map> parsed = parseAttributeList(input, "scope"); - parsed.forEach((k, v) -> System.out.println("\"" + k + "\" = " + v)); + private static String quoteValue(String value) { + boolean needsQuote = value.contains(",") || value.contains(" ") || value.contains("\"") || value.contains("'"); + if (!needsQuote) return value; + + boolean useDouble = !value.contains("\"") || value.contains("'"); + String escaped = value.replace(useDouble ? "\"" : "'", useDouble ? "\\\"" : "\\'"); + return useDouble ? "\"" + escaped + "\"" : "'" + escaped + "'"; + } + + public static String toStringRep(Map> attributes, String defaultKey) { + List parts = new ArrayList<>(); + + // Positional values first + List positional = attributes.get(defaultKey); + if (positional != null) { + for (String value : positional) { + parts.add(quoteValue(value)); + } + } + + // Other keys + for (Map.Entry> entry : attributes.entrySet()) { + String key = entry.getKey(); + if (key.equals(defaultKey)) continue; + + List values = entry.getValue(); + for (String value : values) { + if ("true".equals(value)) { + parts.add("%" + key); + } else { + parts.add(key + "=" + quoteValue(value)); + } + } + } + + return String.join(",", parts); } } diff --git a/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java b/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java index e74e16440..9dc2e4faa 100644 --- a/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java +++ b/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java @@ -61,6 +61,38 @@ void testdepIdToArtifact() { assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> MavenCoordinate.fromString("bla?f")); } + @Test() + void testEqualsEmptyAttributes() { + MavenCoordinate a1 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc"); + MavenCoordinate a2 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc"); + assertThat(a1).as("mavencoordinate bad comparison of empty attribtues").isEqualTo(a2); + } + + @Test + void testEqualsGAVBehavior() { + MavenCoordinate a1 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc"); + MavenCoordinate a2 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc{build,run}"); + assertThat(a1).isEqualTo(a2); //TODO: these are equal in behavior - but not in "format" + } + + @Test + void testEqualsGAVFlippedBehavior() { + MavenCoordinate a1 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc{run,build"); + MavenCoordinate a2 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc{build,run}"); + assertThat(a1).isEqualTo(a2); //TODO: these are equal in behavior - but not in "format" + } + + @Test + void testVariants() { + MavenCoordinate artifact = MavenCoordinate.fromString("com.offbytwo:docopt:0.6.0.20150202:redhat@doc{build,boot,run}"); + assertThat(artifact.getGroupId()).isEqualTo("com.offbytwo"); + assertThat(artifact.getArtifactId()).isEqualTo("docopt"); + assertThat(artifact.getVersion()).isEqualTo("0.6.0.20150202"); + assertThat(artifact.getClassifier()).isEqualTo("redhat"); + assertThat(artifact.getType()).isEqualTo("doc"); + assertThat(artifact.getAttributes().includeInScope("boot")).isTrue(); + + } @Test void testScopes() { From aa86bdaf243f8f98f332c8f56542733ffc12b1d4 Mon Sep 17 00:00:00 2001 From: Max Rydahl Andersen Date: Sat, 2 Aug 2025 10:34:06 +0200 Subject: [PATCH 3/5] chore: refactor to introduce DependencyRequest rather than overloading MavenCoordinate --- .../dependencies/DependencyAttributes.java | 11 ++-- .../jbang/dependencies/DependencyRequest.java | 51 +++++++++++++++++++ .../jbang/dependencies/MavenCoordinate.java | 35 +------------ .../java/dev/jbang/util/AttributeParser.java | 6 ++- .../dependencies/DependencyResolverTest.java | 46 ++++++++++++----- 5 files changed, 95 insertions(+), 54 deletions(-) create mode 100644 src/main/java/dev/jbang/dependencies/DependencyRequest.java diff --git a/src/main/java/dev/jbang/dependencies/DependencyAttributes.java b/src/main/java/dev/jbang/dependencies/DependencyAttributes.java index 96fd8d12e..4937011f6 100644 --- a/src/main/java/dev/jbang/dependencies/DependencyAttributes.java +++ b/src/main/java/dev/jbang/dependencies/DependencyAttributes.java @@ -1,9 +1,8 @@ package dev.jbang.dependencies; -import dev.jbang.util.AttributeParser; - import java.util.*; -import java.util.stream.Collectors; + +import dev.jbang.util.AttributeParser; public class DependencyAttributes { @@ -31,6 +30,7 @@ public boolean includeInScope(String scope) { /** * is there something configured? even if default ? + * * @return */ public boolean isDefault() { @@ -48,8 +48,9 @@ public String toString() { @Override public boolean equals(Object o) { - //TODO: this wont make g:a:v and g:a:v{build,run} equal ...even if they are... - if (o == null || getClass() != o.getClass()) return false; + // TODO: this wont make g:a:v and g:a:v{build,run} equal ...even if they are... + if (o == null || getClass() != o.getClass()) + return false; DependencyAttributes that = (DependencyAttributes) o; return Objects.equals(attributes, that.attributes); } diff --git a/src/main/java/dev/jbang/dependencies/DependencyRequest.java b/src/main/java/dev/jbang/dependencies/DependencyRequest.java new file mode 100644 index 000000000..cb0c429ed --- /dev/null +++ b/src/main/java/dev/jbang/dependencies/DependencyRequest.java @@ -0,0 +1,51 @@ +package dev.jbang.dependencies; + +import java.util.Collections; + +import dev.jbang.util.AttributeParser; + +public class DependencyRequest { + + public static final String SCOPE_ATTR = "scope"; + + final MavenCoordinate mc; + final DependencyAttributes attributes; + + public DependencyRequest(MavenCoordinate mc, DependencyAttributes attributes) { + assert mc != null : "Maven Coordinates shuold never be null"; + assert attributes != null : "Attributes should never be null"; + this.mc = mc; + this.attributes = attributes; + } + + public DependencyAttributes getAttributes() { + return attributes; + } + + public MavenCoordinate getArtifact() { + return mc; + } + + public static DependencyRequest fromString(String input) { + + int firstCurly = input.indexOf("{"); + MavenCoordinate mc; + try { + mc = MavenCoordinate.fromString(input.substring(0, firstCurly)); + } catch (IllegalStateException ie) { + throw new IllegalStateException(String.format("Invalid dependency request: '%s'", + "Expected format is groupId:artifactId:version[:classifier][@type][{attrlist}]")); + } + DependencyAttributes da; + if (firstCurly >= 0) { + // TODO: will let "a:b:c:{adsfaf} something else" parse..shuold we fail? + int lastCurly = input.lastIndexOf("}"); + + da = new DependencyAttributes( + AttributeParser.parseAttributeList(input.substring(firstCurly + 1, lastCurly), SCOPE_ATTR)); + } else { + da = new DependencyAttributes(Collections.emptyMap()); + } + return new DependencyRequest(mc, da); + } +} diff --git a/src/main/java/dev/jbang/dependencies/MavenCoordinate.java b/src/main/java/dev/jbang/dependencies/MavenCoordinate.java index 892ea0783..0e8ef8a99 100644 --- a/src/main/java/dev/jbang/dependencies/MavenCoordinate.java +++ b/src/main/java/dev/jbang/dependencies/MavenCoordinate.java @@ -1,8 +1,5 @@ package dev.jbang.dependencies; -import java.util.Collections; -import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -10,8 +7,6 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -import dev.jbang.util.AttributeParser; - /** * * MavenCoordinate should not implement euqals as it is not meaningfull to use @@ -30,7 +25,6 @@ public class MavenCoordinate { private final String version; private final String classifier; private final String type; - private final DependencyAttributes attributes; public static final String DUMMY_GROUP = "group"; public static final String DEFAULT_VERSION = "999-SNAPSHOT"; @@ -62,10 +56,6 @@ public String getType() { return type; } - public DependencyAttributes getAttributes() { - return attributes; - } - public static MavenCoordinate fromString(String depId) { return parse(depId, gavPattern); } @@ -103,8 +93,7 @@ private static MavenCoordinate parse(String depId, Pattern pattern) { String classifier = gav.group("classifier"); String type = Optional.ofNullable(gav.group("type")).orElse("jar"); String propString = gav.group("properties"); - Map> properties = AttributeParser.parseAttributeList(propString, "scope"); - return new MavenCoordinate(groupId, artifactId, version, classifier, type, properties); + return new MavenCoordinate(groupId, artifactId, version, classifier, type); } public MavenCoordinate(@Nonnull String groupId, @Nonnull String artifactId, @Nonnull String version) { @@ -113,18 +102,11 @@ public MavenCoordinate(@Nonnull String groupId, @Nonnull String artifactId, @Non public MavenCoordinate(@Nonnull String groupId, @Nonnull String artifactId, @Nonnull String version, @Nullable String classifier, @Nullable String type) { - this(groupId, artifactId, version, classifier, type, null); - } - - public MavenCoordinate(@Nonnull String groupId, @Nonnull String artifactId, @Nonnull String version, - @Nullable String classifier, @Nullable String type, @Nullable Map> attributes) { this.groupId = groupId; this.artifactId = artifactId; this.version = version; this.classifier = classifier != null && classifier.isEmpty() ? null : classifier; this.type = type; - this.attributes = attributes == null ? new DependencyAttributes(Collections.emptyMap()) - : new DependencyAttributes(attributes); } public MavenCoordinate withVersion() { @@ -149,9 +131,6 @@ public String toMavenString() { if (type != null && !type.isEmpty()) { out += "@" + type; } - if(attributes != null && !attributes.isDefault()) { - out += "{" + attributes.toStringFormat() + "}"; - } return out; } @@ -184,19 +163,7 @@ public String toString() { ", version='" + version + '\'' + ", classifier='" + classifier + '\'' + ", type='" + type + '\'' + - ", attributes=" + attributes + '}'; } - @Override - public String toString() { - return "MavenCoordinate{" + - "groupId='" + groupId + '\'' + - ", artifactId='" + artifactId + '\'' + - ", version='" + version + '\'' + - ", classifier='" + classifier + '\'' + - ", type='" + type + '\'' + - ", attributes=" + attributes + - '}'; - } } diff --git a/src/main/java/dev/jbang/util/AttributeParser.java b/src/main/java/dev/jbang/util/AttributeParser.java index 8f73f512d..1f86202c6 100644 --- a/src/main/java/dev/jbang/util/AttributeParser.java +++ b/src/main/java/dev/jbang/util/AttributeParser.java @@ -106,7 +106,8 @@ private static String unquote(String s) { private static String quoteValue(String value) { boolean needsQuote = value.contains(",") || value.contains(" ") || value.contains("\"") || value.contains("'"); - if (!needsQuote) return value; + if (!needsQuote) + return value; boolean useDouble = !value.contains("\"") || value.contains("'"); String escaped = value.replace(useDouble ? "\"" : "'", useDouble ? "\\\"" : "\\'"); @@ -127,7 +128,8 @@ public static String toStringRep(Map> attributes, String de // Other keys for (Map.Entry> entry : attributes.entrySet()) { String key = entry.getKey(); - if (key.equals(defaultKey)) continue; + if (key.equals(defaultKey)) + continue; List values = entry.getValue(); for (String value : values) { diff --git a/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java b/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java index 9dc2e4faa..7209cf446 100644 --- a/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java +++ b/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java @@ -72,65 +72,85 @@ void testEqualsEmptyAttributes() { void testEqualsGAVBehavior() { MavenCoordinate a1 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc"); MavenCoordinate a2 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc{build,run}"); - assertThat(a1).isEqualTo(a2); //TODO: these are equal in behavior - but not in "format" + assertThat(a1).isEqualTo(a2); // TODO: these are equal in behavior - but not in "format" } @Test void testEqualsGAVFlippedBehavior() { MavenCoordinate a1 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc{run,build"); MavenCoordinate a2 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc{build,run}"); - assertThat(a1).isEqualTo(a2); //TODO: these are equal in behavior - but not in "format" + assertThat(a1).isEqualTo(a2); // TODO: these are equal in behavior - but not in "format" } @Test void testVariants() { - MavenCoordinate artifact = MavenCoordinate.fromString("com.offbytwo:docopt:0.6.0.20150202:redhat@doc{build,boot,run}"); + DependencyRequest request = DependencyRequest + .fromString("com.offbytwo:docopt:0.6.0.20150202:redhat@doc{build,boot,run}"); + MavenCoordinate artifact = request.getArtifact(); assertThat(artifact.getGroupId()).isEqualTo("com.offbytwo"); assertThat(artifact.getArtifactId()).isEqualTo("docopt"); assertThat(artifact.getVersion()).isEqualTo("0.6.0.20150202"); assertThat(artifact.getClassifier()).isEqualTo("redhat"); assertThat(artifact.getType()).isEqualTo("doc"); - assertThat(artifact.getAttributes().includeInScope("boot")).isTrue(); + assertThat(request.getAttributes().includeInScope("boot")).isTrue(); } + @Test void testScopes() { - MavenCoordinate artifact = MavenCoordinate.fromString("a.b:c:1.2{build}"); + DependencyRequest request = DependencyRequest.fromString("a.b:c:1.2{build}"); + MavenCoordinate artifact = request.getArtifact(); assertThat(artifact) .extracting("groupId", "artifactId", "version", "classifier") .containsExactly("a.b", "c", "1.2", null); - assertThat(artifact.getAttributes().includeInScope("build")).isTrue(); + assertThat(request.getAttributes().includeInScope("build")).isTrue(); } @Test void testMultiScopes() { - MavenCoordinate artifact = MavenCoordinate.fromString("a.b:c:1.2{build}"); + DependencyRequest request = DependencyRequest.fromString("a.b:c:1.2{build}"); + MavenCoordinate artifact = request.getArtifact(); assertThat(artifact) .extracting("groupId", "artifactId", "version", "classifier") .containsExactly("a.b", "c", "1.2", null); - assertThat(artifact.getAttributes().includeInScope("build")).isTrue(); - assertThat(artifact.getAttributes().includeInScope("compile")).isFalse(); - assertThat(artifact.getAttributes().includeInScope("doesnotexist")).isFalse(); + assertThat(request.getAttributes().includeInScope("build")).isTrue(); + assertThat(request.getAttributes().includeInScope("compile")).isFalse(); + assertThat(request.getAttributes().includeInScope("doesnotexist")).isFalse(); + } + + @Test + void testBadDependencyRequests() { + DependencyRequest dr = DependencyRequest.fromString("a.b:c:1.2{"); + assertThat(dr).isNull(); + ; } @Test void testOtherProperties() { - MavenCoordinate artifact = MavenCoordinate.fromString("a.b:c:1.2"); + DependencyRequest request = DependencyRequest.fromString("a.b:c:1.2{build}"); + MavenCoordinate artifact = request.getArtifact(); assertThat(artifact) .extracting("groupId", "artifactId", "version", "classifier") .containsExactly("a.b", "c", "1.2", null); - assertThat(artifact.getAttributes().includeInScope("build")).isTrue(); - assertThat(artifact.getAttributes().includeInScope("run")).isTrue(); + DependencyRequest request2 = DependencyRequest.fromString("a.b:c:1.2"); + MavenCoordinate artifact2 = request2.getArtifact(); + + assertThat(artifact2) + .extracting("groupId", "artifactId", "version", "classifier") + .containsExactly("a.b", "c", "1.2", null); + + assertThat(request2.getAttributes().includeInScope("build")).isTrue(); + assertThat(request2.getAttributes().includeInScope("run")).isTrue(); } @Test From bdd493651d9130faa42f980ef2b63d832eb0ef4a Mon Sep 17 00:00:00 2001 From: Max Rydahl Andersen Date: Fri, 15 Aug 2025 08:51:06 +0200 Subject: [PATCH 4/5] chore: merge dependencyrequest back into mavencoordinate --- .../jbang/dependencies/ArtifactResolver.java | 3 +- .../dependencies/DependencyAttributes.java | 4 ++ .../jbang/dependencies/DependencyRequest.java | 51 ------------------- .../jbang/dependencies/MavenCoordinate.java | 28 ++++++++-- .../dependencies/DependencyResolverTest.java | 40 ++++++--------- 5 files changed, 45 insertions(+), 81 deletions(-) delete mode 100644 src/main/java/dev/jbang/dependencies/DependencyRequest.java diff --git a/src/main/java/dev/jbang/dependencies/ArtifactResolver.java b/src/main/java/dev/jbang/dependencies/ArtifactResolver.java index 640a184df..4db3ba1cc 100644 --- a/src/main/java/dev/jbang/dependencies/ArtifactResolver.java +++ b/src/main/java/dev/jbang/dependencies/ArtifactResolver.java @@ -406,8 +406,9 @@ private Artifact toArtifact(MavenCoordinate coord) { } private static ArtifactInfo toArtifactInfo(Artifact artifact) { + // TODO: get attributes from artifact descriptor?? MavenCoordinate coord = new MavenCoordinate(artifact.getGroupId(), artifact.getArtifactId(), - artifact.getVersion(), artifact.getClassifier(), artifact.getExtension()); + artifact.getVersion(), artifact.getClassifier(), artifact.getExtension(), DependencyAttributes.DEFAULT); return new ArtifactInfo(coord, artifact.getFile().toPath()); } diff --git a/src/main/java/dev/jbang/dependencies/DependencyAttributes.java b/src/main/java/dev/jbang/dependencies/DependencyAttributes.java index 4937011f6..4dd1a769f 100644 --- a/src/main/java/dev/jbang/dependencies/DependencyAttributes.java +++ b/src/main/java/dev/jbang/dependencies/DependencyAttributes.java @@ -6,6 +6,10 @@ public class DependencyAttributes { + /** default attributes */ + public static final DependencyAttributes DEFAULT = new DependencyAttributes( + AttributeParser.parseAttributeList("scope=build,run", "scope")); + private final Map> attributes; // Java 8 Set.of() is sad :) diff --git a/src/main/java/dev/jbang/dependencies/DependencyRequest.java b/src/main/java/dev/jbang/dependencies/DependencyRequest.java deleted file mode 100644 index cb0c429ed..000000000 --- a/src/main/java/dev/jbang/dependencies/DependencyRequest.java +++ /dev/null @@ -1,51 +0,0 @@ -package dev.jbang.dependencies; - -import java.util.Collections; - -import dev.jbang.util.AttributeParser; - -public class DependencyRequest { - - public static final String SCOPE_ATTR = "scope"; - - final MavenCoordinate mc; - final DependencyAttributes attributes; - - public DependencyRequest(MavenCoordinate mc, DependencyAttributes attributes) { - assert mc != null : "Maven Coordinates shuold never be null"; - assert attributes != null : "Attributes should never be null"; - this.mc = mc; - this.attributes = attributes; - } - - public DependencyAttributes getAttributes() { - return attributes; - } - - public MavenCoordinate getArtifact() { - return mc; - } - - public static DependencyRequest fromString(String input) { - - int firstCurly = input.indexOf("{"); - MavenCoordinate mc; - try { - mc = MavenCoordinate.fromString(input.substring(0, firstCurly)); - } catch (IllegalStateException ie) { - throw new IllegalStateException(String.format("Invalid dependency request: '%s'", - "Expected format is groupId:artifactId:version[:classifier][@type][{attrlist}]")); - } - DependencyAttributes da; - if (firstCurly >= 0) { - // TODO: will let "a:b:c:{adsfaf} something else" parse..shuold we fail? - int lastCurly = input.lastIndexOf("}"); - - da = new DependencyAttributes( - AttributeParser.parseAttributeList(input.substring(firstCurly + 1, lastCurly), SCOPE_ATTR)); - } else { - da = new DependencyAttributes(Collections.emptyMap()); - } - return new DependencyRequest(mc, da); - } -} diff --git a/src/main/java/dev/jbang/dependencies/MavenCoordinate.java b/src/main/java/dev/jbang/dependencies/MavenCoordinate.java index 0e8ef8a99..dfd0e0853 100644 --- a/src/main/java/dev/jbang/dependencies/MavenCoordinate.java +++ b/src/main/java/dev/jbang/dependencies/MavenCoordinate.java @@ -7,6 +7,8 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import dev.jbang.util.AttributeParser; + /** * * MavenCoordinate should not implement euqals as it is not meaningfull to use @@ -25,15 +27,18 @@ public class MavenCoordinate { private final String version; private final String classifier; private final String type; + private final DependencyAttributes attributes; public static final String DUMMY_GROUP = "group"; public static final String DEFAULT_VERSION = "999-SNAPSHOT"; + public static final String SCOPE_ATTR = "scope"; private static final Pattern gavPattern = Pattern.compile( "^(?[^:]*):(?[^:]*)(:(?[^:@{]*))?(:(?[^@{]*))?(@(?.[^{]*))?(\\{(?.*)})?$"); private static final Pattern canonicalPattern = Pattern.compile( "^(?[^:]*):(?[^:]*)((:(?.*)(:(?[^@]*))?)?:(?[^:@]*))?$"); + private String managementKey; public String getGroupId() { @@ -73,6 +78,10 @@ public String getManagementKey() { return managementKey; } + /** + * Converts a canonical string to a MavenCoordinate. + */ + @Deprecated public static MavenCoordinate fromCanonicalString(String depId) { return parse(depId, canonicalPattern); } @@ -83,7 +92,7 @@ private static MavenCoordinate parse(String depId, Pattern pattern) { if (!gav.matches()) { throw new IllegalStateException(String.format( - "[ERROR] Invalid dependency locator: '%s'. Expected format is groupId:artifactId:version[:classifier][@type]", + "[ERROR] Invalid dependency locator: '%s'. Expected format is groupId:artifactId:version[:classifier][@type][{attrlist}]", depId)); } @@ -93,25 +102,29 @@ private static MavenCoordinate parse(String depId, Pattern pattern) { String classifier = gav.group("classifier"); String type = Optional.ofNullable(gav.group("type")).orElse("jar"); String propString = gav.group("properties"); - return new MavenCoordinate(groupId, artifactId, version, classifier, type); + + DependencyAttributes da = new DependencyAttributes( + AttributeParser.parseAttributeList(propString, "scope")); + return new MavenCoordinate(groupId, artifactId, version, classifier, type, da); } public MavenCoordinate(@Nonnull String groupId, @Nonnull String artifactId, @Nonnull String version) { - this(groupId, artifactId, version, null, null); + this(groupId, artifactId, version, null, null, DependencyAttributes.DEFAULT); } public MavenCoordinate(@Nonnull String groupId, @Nonnull String artifactId, @Nonnull String version, - @Nullable String classifier, @Nullable String type) { + @Nullable String classifier, @Nullable String type, DependencyAttributes attributes) { this.groupId = groupId; this.artifactId = artifactId; this.version = version; this.classifier = classifier != null && classifier.isEmpty() ? null : classifier; this.type = type; + this.attributes = attributes; } public MavenCoordinate withVersion() { return version != null ? this - : new MavenCoordinate(groupId, artifactId, DEFAULT_VERSION, classifier, type); + : new MavenCoordinate(groupId, artifactId, DEFAULT_VERSION, classifier, type, attributes); } /** @@ -163,7 +176,12 @@ public String toString() { ", version='" + version + '\'' + ", classifier='" + classifier + '\'' + ", type='" + type + '\'' + + ", attributes='" + attributes + '\'' + '}'; } + public DependencyAttributes getAttributes() { + return attributes; + } + } diff --git a/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java b/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java index 7209cf446..62beaf099 100644 --- a/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java +++ b/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java @@ -77,80 +77,72 @@ void testEqualsGAVBehavior() { @Test void testEqualsGAVFlippedBehavior() { - MavenCoordinate a1 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc{run,build"); + MavenCoordinate a1 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc{run,build}"); MavenCoordinate a2 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc{build,run}"); - assertThat(a1).isEqualTo(a2); // TODO: these are equal in behavior - but not in "format" + assertThat(a1).isNotEqualTo(a2); // these are equal in behavior - but not in "format" } @Test void testVariants() { - DependencyRequest request = DependencyRequest + MavenCoordinate artifact = MavenCoordinate .fromString("com.offbytwo:docopt:0.6.0.20150202:redhat@doc{build,boot,run}"); - MavenCoordinate artifact = request.getArtifact(); assertThat(artifact.getGroupId()).isEqualTo("com.offbytwo"); assertThat(artifact.getArtifactId()).isEqualTo("docopt"); assertThat(artifact.getVersion()).isEqualTo("0.6.0.20150202"); assertThat(artifact.getClassifier()).isEqualTo("redhat"); assertThat(artifact.getType()).isEqualTo("doc"); - assertThat(request.getAttributes().includeInScope("boot")).isTrue(); - + assertThat(artifact.getAttributes().includeInScope("boot")).isTrue(); } @Test void testScopes() { - DependencyRequest request = DependencyRequest.fromString("a.b:c:1.2{build}"); - MavenCoordinate artifact = request.getArtifact(); + MavenCoordinate artifact = MavenCoordinate.fromString("a.b:c:1.2{build}"); assertThat(artifact) .extracting("groupId", "artifactId", "version", "classifier") .containsExactly("a.b", "c", "1.2", null); - assertThat(request.getAttributes().includeInScope("build")).isTrue(); + assertThat(artifact.getAttributes().includeInScope("build")).isTrue(); } @Test void testMultiScopes() { - - DependencyRequest request = DependencyRequest.fromString("a.b:c:1.2{build}"); - MavenCoordinate artifact = request.getArtifact(); + MavenCoordinate artifact = MavenCoordinate.fromString("a.b:c:1.2{build}"); assertThat(artifact) .extracting("groupId", "artifactId", "version", "classifier") .containsExactly("a.b", "c", "1.2", null); - assertThat(request.getAttributes().includeInScope("build")).isTrue(); - assertThat(request.getAttributes().includeInScope("compile")).isFalse(); - assertThat(request.getAttributes().includeInScope("doesnotexist")).isFalse(); + assertThat(artifact.getAttributes().includeInScope("build")).isTrue(); + assertThat(artifact.getAttributes().includeInScope("compile")).isFalse(); + assertThat(artifact.getAttributes().includeInScope("doesnotexist")).isFalse(); } @Test void testBadDependencyRequests() { - DependencyRequest dr = DependencyRequest.fromString("a.b:c:1.2{"); - assertThat(dr).isNull(); - ; + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> MavenCoordinate.fromString("a.b:c:1.2{")); } @Test void testOtherProperties() { - DependencyRequest request = DependencyRequest.fromString("a.b:c:1.2{build}"); - MavenCoordinate artifact = request.getArtifact(); + MavenCoordinate artifact = MavenCoordinate.fromString("a.b:c:1.2{build}"); assertThat(artifact) .extracting("groupId", "artifactId", "version", "classifier") .containsExactly("a.b", "c", "1.2", null); - DependencyRequest request2 = DependencyRequest.fromString("a.b:c:1.2"); - MavenCoordinate artifact2 = request2.getArtifact(); + MavenCoordinate artifact2 = MavenCoordinate.fromString("a.b:c:1.2"); assertThat(artifact2) .extracting("groupId", "artifactId", "version", "classifier") .containsExactly("a.b", "c", "1.2", null); - assertThat(request2.getAttributes().includeInScope("build")).isTrue(); - assertThat(request2.getAttributes().includeInScope("run")).isTrue(); + assertThat(artifact2.getAttributes().includeInScope("build")).isTrue(); + assertThat(artifact2.getAttributes().includeInScope("run")).isTrue(); } @Test From 28d2eb3481d54ea66e01758cf5603c27bed76fd1 Mon Sep 17 00:00:00 2001 From: Max Rydahl Andersen Date: Fri, 15 Aug 2025 09:52:56 +0200 Subject: [PATCH 5/5] chore: fix equals test --- .../java/dev/jbang/dependencies/DependencyResolverTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java b/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java index 62beaf099..e70689dee 100644 --- a/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java +++ b/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java @@ -65,14 +65,14 @@ void testdepIdToArtifact() { void testEqualsEmptyAttributes() { MavenCoordinate a1 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc"); MavenCoordinate a2 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc"); - assertThat(a1).as("mavencoordinate bad comparison of empty attribtues").isEqualTo(a2); + assertThat(a1).as("mavencoordinate bad comparison of empty attribtues").isNotEqualTo(a2); } @Test void testEqualsGAVBehavior() { MavenCoordinate a1 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc"); MavenCoordinate a2 = MavenCoordinate.fromString("a.b:c:0.6:qf@doc{build,run}"); - assertThat(a1).isEqualTo(a2); // TODO: these are equal in behavior - but not in "format" + assertThat(a1).isNotEqualTo(a2); // these are equal in behavior - but not in "format" } @Test