MavenCoordinate.java
package network.ike.workspace.cascade;
import network.ike.support.enums.TypedMarker;
import java.util.Optional;
/**
* A Maven {@code groupId:artifactId} coordinate — the canonical
* value type for "which artifact" across the cascade model.
*
* <p>Replaces the long-standing {@code (String groupId, String artifactId)}
* idiom that the cascade code carried since the YAML-manifest era.
* Treating the pair as a record gives the compiler what convention
* could not: argument-order checking
* ({@code resolve(artifactId, groupId)} no longer silently compiles),
* single-parameter signatures wherever a coordinate was passed, and
* a free {@link Object#equals(Object)} / {@link Object#hashCode()}
* so {@code Map<MavenCoordinate, ...>} replaces the ad-hoc
* {@code "groupId:artifactId"} string keys the assembler used to
* build by hand.
*
* <p>Coordinates produce three derived strings that recur in the
* cascade model:
* <ul>
* <li>{@link #ga()} — the {@code "groupId:artifactId"} display form,
* used in log lines and as a parse/format target for human
* input.</li>
* <li>{@link #versionProperty()} — the canonical IKE version-property
* name {@code <groupId>__GA__<artifactId>__VERSION} (typed-marker
* family, IKE-Network/ike-issues#525). Used by the alignment
* path to locate the property that pins this coordinate.</li>
* <li>{@link #versionPropertyLegacy()} — the pre-#525 form
* {@code <groupId>·<artifactId>} (U+00B7 MIDDLE DOT). Kept for
* transition-period read fallback; callers should look up the
* typed-marker form first, then fall back to the legacy form.</li>
* <li>{@link #policyProperty()} / {@link #policyPropertyLegacy()} —
* the release-policy property names in the new and legacy
* conventions respectively.</li>
* <li>{@link #toString()} — equal to {@link #ga()}, for direct use
* in error messages and string concatenation.</li>
* </ul>
*
* @param groupId the Maven {@code groupId}; non-null and non-blank
* @param artifactId the Maven {@code artifactId}; non-null and non-blank
*/
public record MavenCoordinate(String groupId, String artifactId)
implements Comparable<MavenCoordinate> {
/**
* Canonical constructor — validates that both components are
* non-null and non-blank.
*/
public MavenCoordinate {
if (groupId == null || groupId.isBlank()) {
throw new IllegalArgumentException(
"MavenCoordinate requires a groupId");
}
if (artifactId == null || artifactId.isBlank()) {
throw new IllegalArgumentException(
"MavenCoordinate requires an artifactId");
}
}
/**
* Builds a coordinate or throws. Sugar for
* {@link #MavenCoordinate(String, String)}.
*
* @param groupId the Maven {@code groupId}
* @param artifactId the Maven {@code artifactId}
* @return the coordinate
*/
public static MavenCoordinate of(String groupId, String artifactId) {
return new MavenCoordinate(groupId, artifactId);
}
/**
* Builds a coordinate, or returns empty when either component is
* null or blank. The lenient companion to {@link #of}, useful
* when scanning Maven models whose {@code <plugin>} or
* {@code <dependency>} entries may legitimately omit
* {@code <groupId>} (the Maven default plugin group, for
* instance) — the deriver does not want to throw on these, it
* wants to skip them.
*
* @param groupId the Maven {@code groupId}; may be {@code null}
* or blank
* @param artifactId the Maven {@code artifactId}; may be
* {@code null} or blank
* @return the coordinate, or empty when either component is
* missing
*/
public static Optional<MavenCoordinate> tryOf(String groupId,
String artifactId) {
if (groupId == null || groupId.isBlank()
|| artifactId == null || artifactId.isBlank()) {
return Optional.empty();
}
return Optional.of(new MavenCoordinate(groupId, artifactId));
}
/**
* Parses a {@code "groupId:artifactId"} string into a coordinate.
* Splits on the first colon, matching the Maven display
* convention.
*
* @param ga the {@code G:A} string
* @return the coordinate
* @throws IllegalArgumentException if {@code ga} contains no
* colon or has an empty
* component
*/
public static MavenCoordinate parse(String ga) {
if (ga == null) {
throw new IllegalArgumentException("ga string is required");
}
int colon = ga.indexOf(':');
if (colon < 0) {
throw new IllegalArgumentException(
"not a 'groupId:artifactId' coordinate: " + ga);
}
return new MavenCoordinate(
ga.substring(0, colon), ga.substring(colon + 1));
}
/**
* Returns the {@code "groupId:artifactId"} display form.
*
* @return the {@code G:A} string
*/
public String ga() {
return groupId + ":" + artifactId;
}
/**
* Returns the canonical IKE version-property name in the
* typed-marker family form:
* {@code <groupId>__GA__<artifactId>__VERSION}. The alignment
* path uses this to locate the property that pins this
* coordinate (IKE-Network/ike-issues#525). Callers operating
* during the transition period should fall back to
* {@link #versionPropertyLegacy()} when this name resolves to
* no value — both forms may appear in real POMs while the
* foundation cascade rolls forward.
*
* @return the canonical typed-marker property name
*/
public String versionProperty() {
return groupId + TypedMarker.GA.token() + artifactId + TypedMarker.VERSION.token();
}
/**
* Returns the legacy IKE version-property name —
* {@code <groupId>·<artifactId>} (U+00B7 MIDDLE DOT) — used by
* the pre-#525 convention. Kept for transition-period read
* fallback: callers should try {@link #versionProperty()} first,
* then fall back to this. Removed once the foundation cascade
* has fully migrated to the typed-marker family.
*
* @return the legacy property name
*/
public String versionPropertyLegacy() {
return groupId + "·" + artifactId;
}
/**
* Returns the canonical IKE release-policy property name in the
* typed-marker family form:
* {@code <groupId>__GA__<artifactId>__POLICY}. Value is a
* {@link network.ike.support.enums.ReleasePolicy} rung.
*
* @return the canonical typed-marker policy property name
*/
public String policyProperty() {
return groupId + TypedMarker.GA.token() + artifactId + TypedMarker.POLICY.token();
}
/**
* Returns the legacy IKE release-policy property name —
* {@code <groupId>·<artifactId>·policy} — used by the pre-#525
* convention. Kept for transition-period read fallback.
*
* @return the legacy policy property name
*/
public String policyPropertyLegacy() {
return groupId + "·" + artifactId + "·policy";
}
/**
* Returns {@link #ga()}.
*
* @return the {@code G:A} string
*/
@Override
public String toString() {
return ga();
}
/**
* Natural ordering: by {@code groupId} first, then by
* {@code artifactId}. Lets {@code TreeMap} / {@code TreeSet}
* use coordinates directly and produces deterministic cascade
* orderings.
*
* @param other the coordinate to compare against
* @return negative / zero / positive per the standard contract
*/
@Override
public int compareTo(MavenCoordinate other) {
int byGroup = groupId.compareTo(other.groupId);
return byGroup != 0
? byGroup
: artifactId.compareTo(other.artifactId);
}
}