SubprojectResolver.java
package network.ike.plugin.ws;
import network.ike.workspace.Manifest;
import network.ike.workspace.PublishedArtifactSet;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
/**
* Resolves a Maven coordinate ({@code groupId:artifactId}) to the
* workspace subproject that <em>produces</em> it.
*
* <p>This is the single place that encodes the rule a POM coordinate
* maps to a workspace subproject <strong>only</strong> on a full
* {@code groupId} <em>and</em> {@code artifactId} match against that
* subproject's published artifact set — never by groupId alone. A
* coordinate that matches no subproject is <em>external</em> and
* resolves to {@link Optional#empty()}; callers must leave external
* coordinates untouched (no manifest {@code parent:}/{@code depends-on}
* edge, no version rewrite).
*
* <p>groupId-alone matching mis-associates whenever two subprojects (or
* a subproject and an external artifact) share a groupId — e.g.
* {@code network.ike.platform} is shared by the external
* {@code ike-parent} and the subproject {@code ike-commonmark-attributes}
* (IKE-Network/ike-issues#565), and {@code dev.ikm.komet} is shared by
* the {@code komet} and {@code komet-bom} subprojects (#566). Routing
* every coordinate→subproject lookup through this resolver closes that
* class of defect; it mirrors the GA-matching rule the parent-version
* machinery already applies (#241).
*
* <p>Built once by scanning every cloned subproject's published
* artifacts (regex-based, via {@link PublishedArtifactSet}). Uncloned
* subprojects (no {@code pom.xml} on disk) contribute nothing — they
* have no scannable POM, so their coordinates resolve as external until
* cloned.
*
* <p>Thread-confined: build one per operation; the backing index is an
* immutable snapshot of disk state at {@link #scan} time.
*/
public final class SubprojectResolver {
/** {@code "groupId:artifactId"} → producing subproject name. */
private final Map<String, String> index;
private SubprojectResolver(Map<String, String> index) {
this.index = index;
}
/**
* Build a resolver by scanning each manifest subproject's published
* artifact set.
*
* <p>When two subprojects pathologically publish the same
* coordinate, the first in manifest iteration order wins (matching
* the first-match behaviour of the call sites this replaces).
*
* @param wsDir workspace root directory
* @param manifest parsed workspace manifest
* @return a resolver over the manifest's cloned subprojects
* @throws IOException if a subproject POM cannot be read
*/
public static SubprojectResolver scan(Path wsDir, Manifest manifest)
throws IOException {
Map<String, String> index = new LinkedHashMap<>();
for (String name : manifest.subprojects().keySet()) {
Path subDir = wsDir.resolve(name);
if (!Files.exists(subDir.resolve("pom.xml"))) {
continue;
}
for (PublishedArtifactSet.Artifact artifact
: PublishedArtifactSet.scan(subDir)) {
index.putIfAbsent(
artifact.groupId() + ":" + artifact.artifactId(),
name);
}
}
return new SubprojectResolver(index);
}
/**
* The workspace subproject that publishes the given coordinate, or
* empty when the coordinate is external (produced by no subproject,
* or either coordinate is null).
*
* @param groupId Maven groupId
* @param artifactId Maven artifactId
* @return the producing subproject name, or empty when external
*/
public Optional<String> subprojectForCoordinate(String groupId,
String artifactId) {
if (groupId == null || artifactId == null) {
return Optional.empty();
}
return Optional.ofNullable(index.get(groupId + ":" + artifactId));
}
}