MavenVersion.java
package network.ike.workspace;
import java.util.Objects;
import java.util.regex.Pattern;
/**
* Validated Maven version value — the {@code <version>} string for
* any Maven coordinate, including workspace-root, subproject, and
* parent versions (ike-issues#295).
*
* <p>Per {@code feedback_no_semver_assumption}, this type does NOT
* enforce semver. IKE versions are most commonly single-segment
* monotonic ({@code 1}, {@code 133}, {@code 133-SNAPSHOT}); some
* downstream artifacts use semver-like ({@code 1.0.0-SNAPSHOT},
* {@code 1.127.2-feature-x-SNAPSHOT}); calendar-based versions
* ({@code 20240315-SNAPSHOT}) are also valid. The validator accepts
* any of these.
*
* <p>Validation rules:
* <ul>
* <li>Non-empty</li>
* <li>Starts with a letter or digit</li>
* <li>Allowed characters: ASCII letters, digits,
* {@code .}, {@code -}, {@code _}, {@code +}</li>
* <li>No whitespace or shell-metacharacter hazards
* ({@code <}, {@code >}, {@code "}, {@code '}, {@code $},
* backtick, etc.) — those would be dangerous to interpolate
* into a POM.</li>
* </ul>
*
* <p>Single typed entry point so every consumer of a version
* argument ({@code ws:scaffold-init -Dversion=…},
* {@code ws:scaffold-publish -DparentVersion=…},
* {@code ws:release -DreleaseVersion=…},
* {@code ws:post-release -DnextVersion=…}) gets identical validation.
*/
public final class MavenVersion {
/**
* Syntactic validator. Permissive enough to accept every Maven
* version style the IKE ecosystem uses (single-segment monotonic,
* semver, calendar, branch-qualified) while excluding XML and
* shell hazards.
*/
private static final Pattern VALID =
Pattern.compile("[A-Za-z0-9][A-Za-z0-9._+-]*");
private final String value;
private MavenVersion(String value) {
this.value = value;
}
/**
* Validate {@code raw} and wrap it as a {@code MavenVersion}.
*
* @param raw the candidate version string (typically from a
* {@code -Dversion=…} or {@code -D<x>.version=…}
* command-line argument, or a {@code workspace.yaml}
* field)
* @return a validated {@code MavenVersion}
* @throws IllegalArgumentException if {@code raw} is null, empty,
* or violates any documented rule
*/
public static MavenVersion of(String raw) {
if (raw == null || raw.isEmpty()) {
throw new IllegalArgumentException(
"Maven version must be non-empty.");
}
if (!VALID.matcher(raw).matches()) {
throw new IllegalArgumentException(
"Maven version '" + raw + "' contains invalid characters. "
+ "Allowed: ASCII letters, digits, '.', '-', '_', '+'; "
+ "must start with a letter or digit; no whitespace "
+ "or shell metacharacters.");
}
return new MavenVersion(raw);
}
/**
* The validated version as a string.
*
* @return the raw value (never null or empty)
*/
public String value() {
return value;
}
/**
* Whether this version ends in {@code -SNAPSHOT} — the standard
* Maven SNAPSHOT marker.
*
* @return true iff the version is a SNAPSHOT
*/
public boolean isSnapshot() {
return value.endsWith("-SNAPSHOT");
}
@Override
public boolean equals(Object o) {
return (o instanceof MavenVersion other) && value.equals(other.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return value;
}
}