Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,9 @@ testing {
suites {
test {
useJUnitJupiter()
dependencies {
implementation "org.assertj:assertj-core:3.24.2"
}
}

integrationTest(JvmTestSuite) {
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/dev/jbang/dependencies/ArtifactResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down
66 changes: 66 additions & 0 deletions src/main/java/dev/jbang/dependencies/DependencyAttributes.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package dev.jbang.dependencies;

import java.util.*;

import dev.jbang.util.AttributeParser;

public class DependencyAttributes {

/** default attributes */
public static final DependencyAttributes DEFAULT = new DependencyAttributes(
AttributeParser.parseAttributeList("scope=build,run", "scope"));

private final Map<String, List<String>> attributes;

// Java 8 Set.of() is sad :)
static Set<String> defaultScopes = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("build", "run")));
Set<String> scopes;

public DependencyAttributes(Map<String, List<String>> attributes) {
this.attributes = attributes;
}

// lazy computed; dont store hashset unless truly needed
public boolean includeInScope(String scope) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the word scope - mainly because these aren't actually maven scopes but rather something that gets translated into a scope.

includeInConfig(String config) maybe ? (gradle use configuration as name for different scenarios)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and it would be defaultConfigs if follow same route.

if (scopes == null) {
if (attributes.containsKey("scope")) {
scopes = new HashSet<>(attributes.get("scope"));
} else {
scopes = defaultScopes;
}
}
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) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so first push resulted in funky TestModules errors that was caused by equals method missing on Attributes - which is fair..but reveals a conondrum - how do we define equals for these? i've implemented a few tests (which currently fails) showing situations that we probably want to make equals...which we can by unifing attributes...but should we?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just did an update where i split it out in a DependencyRequest even though I do thing the other properties makes sense to have on the "thing" that encodes the dependency - but it seems like we have places where we treat MavenCoordinate as absolutes which can be used as keys....that is NOT the case even with these new info because MavenCoordinates can have version ranges defined.

// 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);
}
}
33 changes: 26 additions & 7 deletions src/main/java/dev/jbang/dependencies/MavenCoordinate.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
"^(?<groupid>[^:]*):(?<artifactid>[^:]*)(:(?<version>[^:@]*))?(:(?<classifier>[^@]*))?(@(?<type>.*))?$");
"^(?<groupid>[^:]*):(?<artifactid>[^:]*)(:(?<version>[^:@{]*))?(:(?<classifier>[^@{]*))?(@(?<type>.[^{]*))?(\\{(?<properties>.*)})?$");

private static final Pattern canonicalPattern = Pattern.compile(
"^(?<groupid>[^:]*):(?<artifactid>[^:]*)((:(?<type>.*)(:(?<classifier>[^@]*))?)?:(?<version>[^:@]*))?$");

private String managementKey;

public String getGroupId() {
Expand Down Expand Up @@ -73,17 +78,21 @@ public String getManagementKey() {
return managementKey;
}

/**
* Converts a canonical string to a MavenCoordinate.
*/
@Deprecated
public static MavenCoordinate fromCanonicalString(String depId) {
return parse(depId, canonicalPattern);
}

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(
"[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));
}

Expand All @@ -92,26 +101,30 @@ 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");
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);
}

/**
Expand Down Expand Up @@ -163,6 +176,12 @@ public String toString() {
", version='" + version + '\'' +
", classifier='" + classifier + '\'' +
", type='" + type + '\'' +
", attributes='" + attributes + '\'' +
'}';
}

public DependencyAttributes getAttributes() {
return attributes;
}

}
146 changes: 146 additions & 0 deletions src/main/java/dev/jbang/util/AttributeParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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<String, List<String>> parseAttributeList(String input, String defaultKey) {
Map<String, List<String>> result = new LinkedHashMap<>();
List<String> 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<String> tokenize(String input) {
if (input == null)
return Collections.emptyList();
List<String> 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;
}

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<String, List<String>> attributes, String defaultKey) {
List<String> parts = new ArrayList<>();

// Positional values first
List<String> positional = attributes.get(defaultKey);
if (positional != null) {
for (String value : positional) {
parts.add(quoteValue(value));
}
}

// Other keys
for (Map.Entry<String, List<String>> entry : attributes.entrySet()) {
String key = entry.getKey();
if (key.equals(defaultKey))
continue;

List<String> values = entry.getValue();
for (String value : values) {
if ("true".equals(value)) {
parts.add("%" + key);
} else {
parts.add(key + "=" + quoteValue(value));
}
}
}

return String.join(",", parts);
}
}
Loading
Loading