Skip to content
Open
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
98 changes: 91 additions & 7 deletions src/org/rascalmpl/uri/file/MavenRepositoryURIResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.UUID;
import java.util.stream.Stream;

import org.apache.commons.compress.utils.FileNameUtils;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.rascalmpl.uri.FileAttributes;
import org.rascalmpl.uri.ISourceLocationInput;
import org.rascalmpl.uri.URIResolverRegistry;
Expand All @@ -18,6 +21,9 @@
import org.rascalmpl.uri.jar.JarURIResolver;
import org.rascalmpl.util.maven.MavenSettings;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

import io.usethesource.vallang.ISourceLocation;

/**
Expand Down Expand Up @@ -77,11 +83,37 @@
*/
public class MavenRepositoryURIResolver implements ISourceLocationInput, IClassloaderLocationResolver {
private static final String GROUP_ARTIFACT_VERSION_SEPARATOR = "--";
private static final String URI_PATH_SEPARATOR = "/";
private final ISourceLocation root = inferMavenRepositoryLocation();
private final boolean rootIsCaseSensitive;
private final URIResolverRegistry reg;

private static final boolean isCaseInsensitive(URIResolverRegistry reg, ISourceLocation path) {
// there is no API to detect case insensitive file systems
// so we have to detect it
var filename = UUID.randomUUID().toString().toLowerCase() + ".case-probe";
var testFile = URIUtil.getChildLocation(path, filename);
try {
try (var file = reg.getOutputStream(testFile, false)) {
file.write(0x42);
}
catch (IOException ex) {
return false;
}
return reg.exists(URIUtil.getChildLocation(path, filename.toUpperCase()));
} finally {
try {
reg.remove(testFile, false);
}
catch (IOException e) {
// ignore exception on remove
}
}
}

public MavenRepositoryURIResolver(URIResolverRegistry reg) throws IOException, URISyntaxException {
this.reg = reg;
this.rootIsCaseSensitive = !isCaseInsensitive(reg, root);
}

private static ISourceLocation inferMavenRepositoryLocation() throws URISyntaxException {
Expand All @@ -108,26 +140,78 @@ private ISourceLocation resolveJar(ISourceLocation input) throws IOException {
String version = parts[2];

String jarPath
= group.replaceAll("\\.", "/")
+ "/"
= group.replace(".", URI_PATH_SEPARATOR)
+ URI_PATH_SEPARATOR
+ name
+ "/"
+ URI_PATH_SEPARATOR
+ version
+ "/"
+ URI_PATH_SEPARATOR
+ name
+ "-"
+ version
+ ".jar"
;

// find the right jar file in the .m2 folder
return URIUtil.getChildLocation(root, jarPath);
return calculateChildPath(jarPath);
}
else {
throw new IOException("Pattern mvn://groupId--artifactId--version did not match on " + input);
}
}

private final Cache<String, ISourceLocation> caseCorrectedPaths = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofHours(1))
.build();

private ISourceLocation calculateChildPath(String jarPath) {
var result = URIUtil.getChildLocation(root, jarPath);
if (rootIsCaseSensitive && !reg.exists(result)) {
// since we use the authority of an URI, and it's normalized to lower case
// we can calculate the wrong-cased files path, so lets try and recover
var corrected = caseCorrectedPaths.get(jarPath, this::findDifferentCasedMatch);
if (corrected != null) {
return corrected;
}
}
return result;
}

private @Nullable ISourceLocation findDifferentCasedMatch(String jarPath) {
try {
return pathMatcher(root, jarPath);
}
catch (IOException e) {
return null;
}
}

/**
* recursively probe file system to find candidate matches for the full file name in a different casing
*/
private @Nullable ISourceLocation pathMatcher(ISourceLocation root, String subPart) throws IOException {

@jurgenvinju jurgenvinju Aug 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since listAllMainArtifacts contains the same logic and file discovery process, could we reuse that and normalize the String[] until we find a match. Or even factor out its Stream<String> and normalize and find the first match?

var pathSeparator = subPart.indexOf(URI_PATH_SEPARATOR);
var atFileLevel = pathSeparator <= 0;
var segment = atFileLevel ? subPart : subPart.substring(0, pathSeparator);
for (var candidate : reg.listEntries(root)) {
if (candidate.equalsIgnoreCase(segment)) {
var result = URIUtil.getChildLocation(root, candidate);
if (!atFileLevel) {
try {
result = pathMatcher(result, subPart.substring(pathSeparator + URI_PATH_SEPARATOR.length()));
}
catch (IOException e) {
result = null;
}
}
if (result != null) {
return result;
}
}
}
return null;
}

private ISourceLocation resolveInsideJar(ISourceLocation input) throws IOException {
var jarLocation = resolveJar(input);
return URIUtil.getChildLocation(JarURIResolver.jarify(jarLocation), input.getPath());
Expand Down Expand Up @@ -238,7 +322,7 @@ private String[] listAllMainArtifacts() throws IOException {
var groupId = URIUtil.relativize(root, URIUtil.getParentLocation(grandParent))
.getPath()
.substring(1)
.replace("/", ".");
.replace(URI_PATH_SEPARATOR, ".");

if ((artifact + "-" + version + ".jar").equals(URIUtil.getLocationName(fl))) {
return make(groupId, artifact, version, "").getAuthority();
Expand Down Expand Up @@ -291,7 +375,7 @@ public static ISourceLocation mavenize(ISourceLocation loc) {
relative = URIUtil.getParentLocation(relative);
String artifactId = URIUtil.getLocationName(relative);
relative = URIUtil.getParentLocation(relative);
String groupId = relative.getPath().substring(1).replace("/", ".");
String groupId = relative.getPath().substring(1).replace(URI_PATH_SEPARATOR, ".");

return make(groupId, artifactId, version, "");
}
Expand Down
Loading