Semantic Versioning in Java: Comprehensive Implementation and Management

Semantic Versioning (SemVer) is a versioning scheme that provides meaning to version numbers, making it easier to manage dependencies and communicate changes. This implementation provides a complete Java solution for parsing, comparing, validating, and managing semantic versions.

Project Setup and Dependencies

1. Maven Dependencies
<dependencies>
<!-- Validation -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.1.Final</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
</dependencies>

Core Semantic Versioning Implementation

1. Semantic Version Class
// SemanticVersion.java
package com.company.semver;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import jakarta.validation.constraints.Pattern;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Immutable class representing a Semantic Version (SemVer 2.0.0)
* Format: MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]
*/
public final class SemanticVersion implements Comparable<SemanticVersion> {
private final int major;
private final int minor;
private final int patch;
private final List<String> preRelease;
private final List<String> build;
private static final java.util.regex.Pattern SEMVER_PATTERN = java.util.regex.Pattern.compile(
"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)" +
"(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?" +
"(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$"
);
// Factory methods
public static SemanticVersion of(int major, int minor, int patch) {
return new SemanticVersion(major, minor, patch, null, null);
}
public static SemanticVersion of(int major, int minor, int patch, String preRelease) {
return new SemanticVersion(major, minor, patch, preRelease, null);
}
public static SemanticVersion of(int major, int minor, int patch, String preRelease, String build) {
return new SemanticVersion(major, minor, patch, preRelease, build);
}
@JsonCreator
public static SemanticVersion fromString(String version) {
return parse(version);
}
public static SemanticVersion parse(String version) {
if (version == null || version.trim().isEmpty()) {
throw new IllegalArgumentException("Version string cannot be null or empty");
}
Matcher matcher = SEMVER_PATTERN.matcher(version.trim());
if (!matcher.matches()) {
throw new IllegalArgumentException("Invalid semantic version: " + version);
}
try {
int major = Integer.parseInt(matcher.group(1));
int minor = Integer.parseInt(matcher.group(2));
int patch = Integer.parseInt(matcher.group(3));
String preReleaseStr = matcher.group(4);
String buildStr = matcher.group(5);
List<String> preRelease = preReleaseStr != null ? 
parseIdentifiers(preReleaseStr) : Collections.emptyList();
List<String> build = buildStr != null ? 
parseIdentifiers(buildStr) : Collections.emptyList();
return new SemanticVersion(major, minor, patch, preRelease, build);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid version number in: " + version, e);
}
}
private static List<String> parseIdentifiers(String identifiers) {
return Arrays.asList(identifiers.split("\\."));
}
// Constructors
private SemanticVersion(int major, int minor, int patch, 
List<String> preRelease, List<String> build) {
validateVersionComponents(major, minor, patch, preRelease, build);
this.major = major;
this.minor = minor;
this.patch = patch;
this.preRelease = preRelease != null ? 
Collections.unmodifiableList(new ArrayList<>(preRelease)) : 
Collections.emptyList();
this.build = build != null ? 
Collections.unmodifiableList(new ArrayList<>(build)) : 
Collections.emptyList();
}
private SemanticVersion(int major, int minor, int patch, 
String preRelease, String build) {
this(major, minor, patch,
preRelease != null ? parseIdentifiers(preRelease) : null,
build != null ? parseIdentifiers(build) : null);
}
private void validateVersionComponents(int major, int minor, int patch,
List<String> preRelease, List<String> build) {
if (major < 0) throw new IllegalArgumentException("Major version cannot be negative");
if (minor < 0) throw new IllegalArgumentException("Minor version cannot be negative");
if (patch < 0) throw new IllegalArgumentException("Patch version cannot be negative");
validateIdentifiers(preRelease, "pre-release");
validateIdentifiers(build, "build");
}
private void validateIdentifiers(List<String> identifiers, String type) {
if (identifiers == null) return;
for (String identifier : identifiers) {
if (identifier == null || identifier.isEmpty()) {
throw new IllegalArgumentException(type + " identifier cannot be null or empty");
}
// Check if identifier is numeric
if (identifier.matches("\\d+")) {
// Numeric identifiers must not have leading zeros
if (identifier.length() > 1 && identifier.startsWith("0")) {
throw new IllegalArgumentException(
type + " numeric identifier cannot have leading zeros: " + identifier);
}
} else {
// Non-numeric identifiers must contain only ASCII alphanumerics and hyphens
if (!identifier.matches("[0-9A-Za-z-]+")) {
throw new IllegalArgumentException(
type + " identifier contains invalid characters: " + identifier);
}
}
}
}
// Getters
public int getMajor() { return major; }
public int getMinor() { return minor; }
public int getPatch() { return patch; }
public List<String> getPreRelease() { return preRelease; }
public List<String> getBuild() { return build; }
public boolean isPreRelease() { return !preRelease.isEmpty(); }
public boolean hasBuildMetadata() { return !build.isEmpty(); }
public boolean isStable() { return !isPreRelease() && major > 0; }
// Version increment methods
public SemanticVersion incrementMajor() {
return new SemanticVersion(major + 1, 0, 0, Collections.emptyList(), Collections.emptyList());
}
public SemanticVersion incrementMajor(String preRelease) {
return new SemanticVersion(major + 1, 0, 0, preRelease, null);
}
public SemanticVersion incrementMinor() {
return new SemanticVersion(major, minor + 1, 0, Collections.emptyList(), Collections.emptyList());
}
public SemanticVersion incrementMinor(String preRelease) {
return new SemanticVersion(major, minor + 1, 0, preRelease, null);
}
public SemanticVersion incrementPatch() {
return new SemanticVersion(major, minor, patch + 1, Collections.emptyList(), Collections.emptyList());
}
public SemanticVersion incrementPatch(String preRelease) {
return new SemanticVersion(major, minor, patch + 1, preRelease, null);
}
public SemanticVersion withPreRelease(String preRelease) {
return new SemanticVersion(major, minor, patch, preRelease, 
build.isEmpty() ? null : String.join(".", build));
}
public SemanticVersion withBuild(String build) {
return new SemanticVersion(major, minor, patch, 
preRelease.isEmpty() ? null : String.join(".", preRelease), build);
}
public SemanticVersion withoutPreRelease() {
return new SemanticVersion(major, minor, patch, Collections.emptyList(), 
build.isEmpty() ? null : String.join(".", build));
}
public SemanticVersion withoutBuild() {
return new SemanticVersion(major, minor, patch, 
preRelease.isEmpty() ? null : String.join(".", preRelease), 
Collections.emptyList());
}
// Comparison methods
@Override
public int compareTo(SemanticVersion other) {
if (other == null) return 1;
int majorCompare = Integer.compare(major, other.major);
if (majorCompare != 0) return majorCompare;
int minorCompare = Integer.compare(minor, other.minor);
if (minorCompare != 0) return minorCompare;
int patchCompare = Integer.compare(patch, other.patch);
if (patchCompare != 0) return patchCompare;
return comparePreRelease(this.preRelease, other.preRelease);
}
private int comparePreRelease(List<String> pre1, List<String> pre2) {
// No pre-release has higher precedence than pre-release
if (pre1.isEmpty() && pre2.isEmpty()) return 0;
if (pre1.isEmpty()) return 1;
if (pre2.isEmpty()) return -1;
int minSize = Math.min(pre1.size(), pre2.size());
for (int i = 0; i < minSize; i++) {
String id1 = pre1.get(i);
String id2 = pre2.get(i);
int result = compareIdentifier(id1, id2);
if (result != 0) return result;
}
// All compared identifiers are equal
return Integer.compare(pre1.size(), pre2.size());
}
private int compareIdentifier(String id1, String id2) {
boolean isNumeric1 = id1.matches("\\d+");
boolean isNumeric2 = id2.matches("\\d+");
if (isNumeric1 && isNumeric2) {
return Long.compare(Long.parseLong(id1), Long.parseLong(id2));
} else if (isNumeric1) {
return -1; // Numeric identifiers have lower precedence than non-numeric
} else if (isNumeric2) {
return 1; // Non-numeric identifiers have higher precedence than numeric
} else {
return id1.compareTo(id2);
}
}
// Compatibility checks
public boolean isCompatibleWith(SemanticVersion other) {
if (other == null) return false;
return this.major == other.major;
}
public boolean isBackwardCompatibleWith(SemanticVersion other) {
if (other == null) return false;
return this.major == other.major && this.minor >= other.minor;
}
public boolean isPatchCompatibleWith(SemanticVersion other) {
if (other == null) return false;
return this.major == other.major && this.minor == other.minor && this.patch >= other.patch;
}
// Range checking
public boolean satisfies(VersionRange range) {
return range != null && range.contains(this);
}
// Object methods
@Override
@JsonValue
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(major).append('.').append(minor).append('.').append(patch);
if (!preRelease.isEmpty()) {
sb.append('-').append(String.join(".", preRelease));
}
if (!build.isEmpty()) {
sb.append('+').append(String.join(".", build));
}
return sb.toString();
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
SemanticVersion that = (SemanticVersion) obj;
return major == that.major &&
minor == that.minor &&
patch == that.patch &&
preRelease.equals(that.preRelease);
// Build metadata is ignored for equality
}
@Override
public int hashCode() {
return Objects.hash(major, minor, patch, preRelease);
}
// Static utility methods
public static SemanticVersion min(SemanticVersion v1, SemanticVersion v2) {
if (v1 == null) return v2;
if (v2 == null) return v1;
return v1.compareTo(v2) <= 0 ? v1 : v2;
}
public static SemanticVersion max(SemanticVersion v1, SemanticVersion v2) {
if (v1 == null) return v2;
if (v2 == null) return v1;
return v1.compareTo(v2) >= 0 ? v1 : v2;
}
public static boolean isValid(String version) {
if (version == null || version.trim().isEmpty()) return false;
return SEMVER_PATTERN.matcher(version.trim()).matches();
}
}
2. Version Range Implementation
// VersionRange.java
package com.company.semver;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Represents a version range that can match multiple semantic versions
* Supports common range syntax: ^, ~, >=, <=, >, <, ||, etc.
*/
public class VersionRange {
private final List<Constraint> constraints;
private static final Pattern RANGE_PATTERN = Pattern.compile(
"([>=<~^]+)\\s*([0-9A-Za-z.+*-]+)|" +
"\\|\\||" +
"([0-9A-Za-z.+*-]+)\\s*-\\s*([0-9A-Za-z.+*-]+)"
);
private VersionRange(List<Constraint> constraints) {
this.constraints = Collections.unmodifiableList(new ArrayList<>(constraints));
}
public static VersionRange exact(SemanticVersion version) {
return new VersionRange(List.of(Constraint.exact(version)));
}
public static VersionRange any() {
return new VersionRange(List.of(Constraint.any()));
}
public static VersionRange none() {
return new VersionRange(List.of(Constraint.none()));
}
public static VersionRange parse(String range) {
if (range == null || range.trim().isEmpty()) {
throw new IllegalArgumentException("Range string cannot be null or empty");
}
String trimmed = range.trim();
// Handle special cases
if ("*".equals(trimmed) || "x".equals(trimmed) || "X".equals(trimmed)) {
return any();
}
if ("~".equals(trimmed) || "^".equals(trimmed)) {
throw new IllegalArgumentException("Invalid range: " + range);
}
List<Constraint> constraints = new ArrayList<>();
// Split by OR (||)
String[] orParts = trimmed.split("\\|\\|");
for (String orPart : orParts) {
List<Constraint> andConstraints = parseAndConstraints(orPart.trim());
if (!andConstraints.isEmpty()) {
constraints.add(Constraint.or(andConstraints));
}
}
return new VersionRange(constraints);
}
private static List<Constraint> parseAndConstraints(String range) {
List<Constraint> constraints = new ArrayList<>();
// Split by AND (space or comma)
String[] andParts = range.split("[\\s,]+");
for (String part : andParts) {
if (part.isEmpty()) continue;
Constraint constraint = parseConstraint(part.trim());
if (constraint != null) {
constraints.add(constraint);
}
}
return constraints;
}
private static Constraint parseConstraint(String constraint) {
if (constraint.equals("*")) {
return Constraint.any();
}
Matcher matcher = RANGE_PATTERN.matcher(constraint);
if (matcher.matches()) {
String operator = matcher.group(1);
String versionStr = matcher.group(2);
if (operator != null && versionStr != null) {
SemanticVersion version = SemanticVersion.parse(versionStr);
return parseOperatorConstraint(operator, version);
}
// Handle hyphen ranges (version1 - version2)
String fromVersion = matcher.group(3);
String toVersion = matcher.group(4);
if (fromVersion != null && toVersion != null) {
SemanticVersion from = SemanticVersion.parse(fromVersion);
SemanticVersion to = SemanticVersion.parse(toVersion);
return Constraint.between(from, to);
}
}
// Default to exact match
try {
SemanticVersion version = SemanticVersion.parse(constraint);
return Constraint.exact(version);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid range constraint: " + constraint, e);
}
}
private static Constraint parseOperatorConstraint(String operator, SemanticVersion version) {
return switch (operator) {
case ">=" -> Constraint.greaterThanOrEqual(version);
case ">" -> Constraint.greaterThan(version);
case "<=" -> Constraint.lessThanOrEqual(version);
case "<" -> Constraint.lessThan(version);
case "~", "~>" -> createTildeRange(version);
case "^" -> createCaretRange(version);
case "=" -> Constraint.exact(version);
default -> throw new IllegalArgumentException("Unknown operator: " + operator);
};
}
private static Constraint createTildeRange(SemanticVersion version) {
// ~1.2.3 means >=1.2.3 <1.3.0
// ~1.2 means >=1.2.0 <1.3.0
// ~1 means >=1.0.0 <2.0.0
if (version.getPatch() >= 0) {
// ~1.2.3
SemanticVersion min = version;
SemanticVersion max = SemanticVersion.of(version.getMajor(), version.getMinor() + 1, 0);
return Constraint.between(min, max);
} else if (version.getMinor() >= 0) {
// ~1.2
SemanticVersion min = SemanticVersion.of(version.getMajor(), version.getMinor(), 0);
SemanticVersion max = SemanticVersion.of(version.getMajor(), version.getMinor() + 1, 0);
return Constraint.between(min, max);
} else {
// ~1
SemanticVersion min = SemanticVersion.of(version.getMajor(), 0, 0);
SemanticVersion max = SemanticVersion.of(version.getMajor() + 1, 0, 0);
return Constraint.between(min, max);
}
}
private static Constraint createCaretRange(SemanticVersion version) {
// ^1.2.3 means >=1.2.3 <2.0.0
// ^0.2.3 means >=0.2.3 <0.3.0
// ^0.0.3 means >=0.0.3 <0.0.4
if (version.getMajor() > 0) {
SemanticVersion min = version;
SemanticVersion max = SemanticVersion.of(version.getMajor() + 1, 0, 0);
return Constraint.between(min, max);
} else if (version.getMinor() > 0) {
SemanticVersion min = version;
SemanticVersion max = SemanticVersion.of(0, version.getMinor() + 1, 0);
return Constraint.between(min, max);
} else {
SemanticVersion min = version;
SemanticVersion max = SemanticVersion.of(0, 0, version.getPatch() + 1);
return Constraint.between(min, max);
}
}
public boolean contains(SemanticVersion version) {
if (version == null) return false;
for (Constraint constraint : constraints) {
if (constraint.matches(version)) {
return true;
}
}
return false;
}
public SemanticVersion findBestMatch(List<SemanticVersion> versions) {
if (versions == null || versions.isEmpty()) return null;
return versions.stream()
.filter(this::contains)
.max(SemanticVersion::compareTo)
.orElse(null);
}
public boolean isSatisfiedByAny(List<SemanticVersion> versions) {
return versions.stream().anyMatch(this::contains);
}
public List<SemanticVersion> findAllMatches(List<SemanticVersion> versions) {
if (versions == null) return Collections.emptyList();
return versions.stream()
.filter(this::contains)
.sorted(Comparator.reverseOrder())
.toList();
}
@Override
public String toString() {
if (constraints.isEmpty()) return "none";
if (constraints.size() == 1) return constraints.get(0).toString();
return String.join(" || ", constraints.stream()
.map(Constraint::toString)
.toList());
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
VersionRange that = (VersionRange) obj;
return constraints.equals(that.constraints);
}
@Override
public int hashCode() {
return Objects.hash(constraints);
}
}
// Constraint.java
class Constraint {
private final Type type;
private final SemanticVersion version;
private final SemanticVersion minVersion;
private final SemanticVersion maxVersion;
private final List<Constraint> orConstraints;
enum Type {
ANY,
NONE,
EXACT,
GREATER_THAN,
GREATER_THAN_OR_EQUAL,
LESS_THAN,
LESS_THAN_OR_EQUAL,
BETWEEN,
OR
}
private Constraint(Type type, SemanticVersion version, 
SemanticVersion minVersion, SemanticVersion maxVersion,
List<Constraint> orConstraints) {
this.type = type;
this.version = version;
this.minVersion = minVersion;
this.maxVersion = maxVersion;
this.orConstraints = orConstraints != null ? 
Collections.unmodifiableList(new ArrayList<>(orConstraints)) : null;
}
// Factory methods
static Constraint any() {
return new Constraint(Type.ANY, null, null, null, null);
}
static Constraint none() {
return new Constraint(Type.NONE, null, null, null, null);
}
static Constraint exact(SemanticVersion version) {
return new Constraint(Type.EXACT, version, null, null, null);
}
static Constraint greaterThan(SemanticVersion version) {
return new Constraint(Type.GREATER_THAN, version, null, null, null);
}
static Constraint greaterThanOrEqual(SemanticVersion version) {
return new Constraint(Type.GREATER_THAN_OR_EQUAL, version, null, null, null);
}
static Constraint lessThan(SemanticVersion version) {
return new Constraint(Type.LESS_THAN, version, null, null, null);
}
static Constraint lessThanOrEqual(SemanticVersion version) {
return new Constraint(Type.LESS_THAN_OR_EQUAL, version, null, null, null);
}
static Constraint between(SemanticVersion min, SemanticVersion max) {
return new Constraint(Type.BETWEEN, null, min, max, null);
}
static Constraint or(List<Constraint> constraints) {
return new Constraint(Type.OR, null, null, null, constraints);
}
boolean matches(SemanticVersion version) {
if (version == null) return false;
return switch (type) {
case ANY -> true;
case NONE -> false;
case EXACT -> version.equals(this.version);
case GREATER_THAN -> version.compareTo(this.version) > 0;
case GREATER_THAN_OR_EQUAL -> version.compareTo(this.version) >= 0;
case LESS_THAN -> version.compareTo(this.version) < 0;
case LESS_THAN_OR_EQUAL -> version.compareTo(this.version) <= 0;
case BETWEEN -> 
version.compareTo(minVersion) >= 0 && version.compareTo(maxVersion) < 0;
case OR -> orConstraints.stream().anyMatch(c -> c.matches(version));
};
}
@Override
public String toString() {
return switch (type) {
case ANY -> "*";
case NONE -> "none";
case EXACT -> version.toString();
case GREATER_THAN -> ">" + version;
case GREATER_THAN_OR_EQUAL -> ">=" + version;
case LESS_THAN -> "<" + version;
case LESS_THAN_OR_EQUAL -> "<=" + version;
case BETWEEN -> minVersion + " - " + maxVersion;
case OR -> String.join(" || ", orConstraints.stream()
.map(Constraint::toString)
.toList());
};
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Constraint that = (Constraint) obj;
return type == that.type &&
Objects.equals(version, that.version) &&
Objects.equals(minVersion, that.minVersion) &&
Objects.equals(maxVersion, that.maxVersion) &&
Objects.equals(orConstraints, that.orConstraints);
}
@Override
public int hashCode() {
return Objects.hash(type, version, minVersion, maxVersion, orConstraints);
}
}
3. Version Management Service
// VersionManager.java
package com.company.semver;
import java.util.*;
import java.util.stream.Collectors;
/**
* Service for managing semantic versions and their lifecycle
*/
public class VersionManager {
private final List<SemanticVersion> versions;
private final Map<String, SemanticVersion> latestVersions;
public VersionManager() {
this.versions = new ArrayList<>();
this.latestVersions = new HashMap<>();
}
public VersionManager(Collection<SemanticVersion> initialVersions) {
this();
if (initialVersions != null) {
this.versions.addAll(initialVersions);
updateLatestVersions();
}
}
public void addVersion(SemanticVersion version) {
if (version == null) throw new IllegalArgumentException("Version cannot be null");
versions.add(version);
updateLatestVersions();
// Sort versions after addition
versions.sort(Comparator.reverseOrder());
}
public void addVersions(Collection<SemanticVersion> versions) {
if (versions == null) return;
this.versions.addAll(versions);
updateLatestVersions();
// Sort versions after addition
this.versions.sort(Comparator.reverseOrder());
}
public boolean removeVersion(SemanticVersion version) {
boolean removed = versions.remove(version);
if (removed) {
updateLatestVersions();
}
return removed;
}
public List<SemanticVersion> getAllVersions() {
return Collections.unmodifiableList(versions);
}
public List<SemanticVersion> getStableVersions() {
return versions.stream()
.filter(SemanticVersion::isStable)
.collect(Collectors.toList());
}
public List<SemanticVersion> getPreReleaseVersions() {
return versions.stream()
.filter(SemanticVersion::isPreRelease)
.collect(Collectors.toList());
}
public SemanticVersion getLatest() {
return versions.stream()
.max(SemanticVersion::compareTo)
.orElse(null);
}
public SemanticVersion getLatestStable() {
return versions.stream()
.filter(SemanticVersion::isStable)
.max(SemanticVersion::compareTo)
.orElse(null);
}
public SemanticVersion getLatestPreRelease() {
return versions.stream()
.filter(SemanticVersion::isPreRelease)
.max(SemanticVersion::compareTo)
.orElse(null);
}
public SemanticVersion findBestMatch(VersionRange range) {
return range.findBestMatch(versions);
}
public List<SemanticVersion> findAllMatches(VersionRange range) {
return range.findAllMatches(versions);
}
public boolean hasVersion(SemanticVersion version) {
return versions.contains(version);
}
public SemanticVersion getNextMajor() {
SemanticVersion latest = getLatest();
return latest != null ? latest.incrementMajor() : SemanticVersion.of(1, 0, 0);
}
public SemanticVersion getNextMinor() {
SemanticVersion latest = getLatest();
return latest != null ? latest.incrementMinor() : SemanticVersion.of(0, 1, 0);
}
public SemanticVersion getNextPatch() {
SemanticVersion latest = getLatest();
return latest != null ? latest.incrementPatch() : SemanticVersion.of(0, 0, 1);
}
public SemanticVersion getNextPreRelease(String preReleaseIdentifier) {
SemanticVersion latest = getLatest();
if (latest == null) {
return SemanticVersion.of(0, 1, 0, preReleaseIdentifier);
}
// If latest is already a pre-release with same identifier, increment it
if (latest.isPreRelease() && 
latest.getPreRelease().get(0).equals(preReleaseIdentifier)) {
// Try to increment the numeric part of pre-release
List<String> newPreRelease = incrementPreRelease(latest.getPreRelease());
return latest.withPreRelease(String.join(".", newPreRelease));
}
// Otherwise, create new pre-release from the current version
return latest.incrementPatch(preReleaseIdentifier + ".1");
}
private List<String> incrementPreRelease(List<String> preRelease) {
List<String> newPreRelease = new ArrayList<>(preRelease);
// Find the last numeric identifier and increment it
for (int i = newPreRelease.size() - 1; i >= 0; i--) {
String identifier = newPreRelease.get(i);
if (identifier.matches("\\d+")) {
long numericValue = Long.parseLong(identifier);
newPreRelease.set(i, String.valueOf(numericValue + 1));
return newPreRelease;
}
}
// No numeric identifier found, append .1
newPreRelease.add("1");
return newPreRelease;
}
public VersionAnalysis analyzeVersions() {
SemanticVersion latest = getLatest();
SemanticVersion latestStable = getLatestStable();
long totalVersions = versions.size();
long stableVersions = getStableVersions().size();
long preReleaseVersions = getPreReleaseVersions().size();
double stabilityPercentage = totalVersions > 0 ? 
(double) stableVersions / totalVersions * 100 : 0.0;
List<VersionGap> gaps = findVersionGaps();
return VersionAnalysis.builder()
.totalVersions(totalVersions)
.stableVersions(stableVersions)
.preReleaseVersions(preReleaseVersions)
.stabilityPercentage(stabilityPercentage)
.latestVersion(latest)
.latestStableVersion(latestStable)
.versionGaps(gaps)
.build();
}
private List<VersionGap> findVersionGaps() {
List<VersionGap> gaps = new ArrayList<>();
if (versions.size() < 2) return gaps;
List<SemanticVersion> sorted = new ArrayList<>(versions);
sorted.sort(SemanticVersion::compareTo);
for (int i = 0; i < sorted.size() - 1; i++) {
SemanticVersion current = sorted.get(i);
SemanticVersion next = sorted.get(i + 1);
// Check if there's a gap between current and next
if (!areConsecutive(current, next)) {
gaps.add(VersionGap.builder()
.fromVersion(current)
.toVersion(next)
.gapType(determineGapType(current, next))
.build());
}
}
return gaps;
}
private boolean areConsecutive(SemanticVersion v1, SemanticVersion v2) {
// Two versions are consecutive if v2 is the immediate next version of v1
if (v1.getMajor() == v2.getMajor() && 
v1.getMinor() == v2.getMinor() && 
v1.getPatch() + 1 == v2.getPatch()) {
return true;
}
// Handle major/minor increments
if (v1.getMajor() == v2.getMajor() && 
v1.getMinor() + 1 == v2.getMinor() && 
v2.getPatch() == 0) {
return true;
}
if (v1.getMajor() + 1 == v2.getMajor() && 
v2.getMinor() == 0 && 
v2.getPatch() == 0) {
return true;
}
return false;
}
private String determineGapType(SemanticVersion from, SemanticVersion to) {
if (from.getMajor() != to.getMajor()) return "MAJOR";
if (from.getMinor() != to.getMinor()) return "MINOR";
return "PATCH";
}
private void updateLatestVersions() {
latestVersions.clear();
// Group by major version and find latest for each
Map<Integer, List<SemanticVersion>> byMajor = versions.stream()
.collect(Collectors.groupingBy(SemanticVersion::getMajor));
for (Map.Entry<Integer, List<SemanticVersion>> entry : byMajor.entrySet()) {
SemanticVersion latestInMajor = entry.getValue().stream()
.max(SemanticVersion::compareTo)
.orElse(null);
if (latestInMajor != null) {
latestVersions.put(String.valueOf(entry.getKey()), latestInMajor);
}
}
// Also group by major.minor
Map<String, List<SemanticVersion>> byMinor = versions.stream()
.collect(Collectors.groupingBy(v -> 
v.getMajor() + "." + v.getMinor()));
for (Map.Entry<String, List<SemanticVersion>> entry : byMinor.entrySet()) {
SemanticVersion latestInMinor = entry.getValue().stream()
.max(SemanticVersion::compareTo)
.orElse(null);
if (latestInMinor != null) {
latestVersions.put(entry.getKey(), latestInMinor);
}
}
}
public Map<String, SemanticVersion> getLatestVersions() {
return Collections.unmodifiableMap(latestVersions);
}
}
// VersionAnalysis.java
package com.company.semver;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class VersionAnalysis {
private long totalVersions;
private long stableVersions;
private long preReleaseVersions;
private double stabilityPercentage;
private SemanticVersion latestVersion;
private SemanticVersion latestStableVersion;
private List<VersionGap> versionGaps;
public boolean isHealthy() {
return stabilityPercentage >= 80.0 && latestStableVersion != null;
}
public String getHealthStatus() {
if (stabilityPercentage >= 90.0) return "EXCELLENT";
if (stabilityPercentage >= 80.0) return "GOOD";
if (stabilityPercentage >= 60.0) return "FAIR";
return "POOR";
}
}
// VersionGap.java
package com.company.semver;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class VersionGap {
private SemanticVersion fromVersion;
private SemanticVersion toVersion;
private String gapType; // MAJOR, MINOR, PATCH
public String getDescription() {
return String.format("Gap between %s and %s (%s)", 
fromVersion, toVersion, gapType);
}
}

REST API for Version Management

1. Version Controller
// VersionController.java
package com.company.semver.api;
import com.company.semver.*;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/versions")
public class VersionController {
private final VersionManager versionManager;
public VersionController(VersionManager versionManager) {
this.versionManager = versionManager;
}
@PostMapping("/parse")
public ResponseEntity<ApiResponse<SemanticVersion>> parseVersion(
@RequestBody ParseVersionRequest request) {
try {
SemanticVersion version = SemanticVersion.parse(request.getVersionString());
return ResponseEntity.ok(ApiResponse.success(version));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest()
.body(ApiResponse.error(e.getMessage()));
}
}
@PostMapping
public ResponseEntity<ApiResponse<SemanticVersion>> addVersion(
@Valid @RequestBody AddVersionRequest request) {
try {
SemanticVersion version = SemanticVersion.parse(request.getVersion());
versionManager.addVersion(version);
return ResponseEntity.ok(ApiResponse.success(version, "Version added successfully"));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest()
.body(ApiResponse.error(e.getMessage()));
}
}
@GetMapping
public ResponseEntity<ApiResponse<List<SemanticVersion>>> getAllVersions(
@RequestParam(required = false) Boolean stable) {
List<SemanticVersion> versions;
if (stable != null) {
versions = stable ? 
versionManager.getStableVersions() : 
versionManager.getPreReleaseVersions();
} else {
versions = versionManager.getAllVersions();
}
return ResponseEntity.ok(ApiResponse.success(versions));
}
@GetMapping("/latest")
public ResponseEntity<ApiResponse<SemanticVersion>> getLatestVersion(
@RequestParam(required = false) Boolean stable) {
SemanticVersion latest;
if (stable != null && stable) {
latest = versionManager.getLatestStable();
} else {
latest = versionManager.getLatest();
}
if (latest == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(ApiResponse.success(latest));
}
@PostMapping("/next")
public ResponseEntity<ApiResponse<SemanticVersion>> getNextVersion(
@RequestBody NextVersionRequest request) {
try {
SemanticVersion nextVersion = switch (request.getIncrementType()) {
case MAJOR -> versionManager.getNextMajor();
case MINOR -> versionManager.getNextMinor();
case PATCH -> versionManager.getNextPatch();
case PRE_RELEASE -> versionManager.getNextPreRelease(request.getPreReleaseIdentifier());
};
return ResponseEntity.ok(ApiResponse.success(nextVersion));
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(ApiResponse.error(e.getMessage()));
}
}
@PostMapping("/match")
public ResponseEntity<ApiResponse<VersionMatchResult>> findMatchingVersions(
@RequestBody MatchVersionsRequest request) {
try {
VersionRange range = VersionRange.parse(request.getRange());
List<SemanticVersion> matches = versionManager.findAllMatches(range);
SemanticVersion bestMatch = versionManager.findBestMatch(range);
VersionMatchResult result = VersionMatchResult.builder()
.range(range.toString())
.matches(matches)
.bestMatch(bestMatch)
.matchCount(matches.size())
.build();
return ResponseEntity.ok(ApiResponse.success(result));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest()
.body(ApiResponse.error(e.getMessage()));
}
}
@GetMapping("/analysis")
public ResponseEntity<ApiResponse<VersionAnalysis>> analyzeVersions() {
VersionAnalysis analysis = versionManager.analyzeVersions();
return ResponseEntity.ok(ApiResponse.success(analysis));
}
@PostMapping("/validate")
public ResponseEntity<ApiResponse<ValidationResult>> validateVersion(
@RequestBody ValidateVersionRequest request) {
boolean isValid = SemanticVersion.isValid(request.getVersionString());
ValidationResult result = ValidationResult.builder()
.versionString(request.getVersionString())
.valid(isValid)
.message(isValid ? "Valid semantic version" : "Invalid semantic version")
.build();
return ResponseEntity.ok(ApiResponse.success(result));
}
}
// Request/Response DTOs
class ParseVersionRequest {
private String versionString;
// getters and setters
public String getVersionString() { return versionString; }
public void setVersionString(String versionString) { this.versionString = versionString; }
}
class AddVersionRequest {
private String version;
// getters and setters
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
}
class NextVersionRequest {
private IncrementType incrementType;
private String preReleaseIdentifier;
public enum IncrementType {
MAJOR, MINOR, PATCH, PRE_RELEASE
}
// getters and setters
public IncrementType getIncrementType() { return incrementType; }
public void setIncrementType(IncrementType incrementType) { this.incrementType = incrementType; }
public String getPreReleaseIdentifier() { return preReleaseIdentifier; }
public void setPreReleaseIdentifier(String preReleaseIdentifier) { this.preReleaseIdentifier = preReleaseIdentifier; }
}
class MatchVersionsRequest {
private String range;
// getters and setters
public String getRange() { return range; }
public void setRange(String range) { this.range = range; }
}
class ValidateVersionRequest {
private String versionString;
// getters and setters
public String getVersionString() { return versionString; }
public void setVersionString(String versionString) { this.versionString = versionString; }
}
class VersionMatchResult {
private String range;
private List<SemanticVersion> matches;
private SemanticVersion bestMatch;
private int matchCount;
// builder, getters, setters
public static VersionMatchResultBuilder builder() { return new VersionMatchResultBuilder(); }
public String getRange() { return range; }
public void setRange(String range) { this.range = range; }
public List<SemanticVersion> getMatches() { return matches; }
public void setMatches(List<SemanticVersion> matches) { this.matches = matches; }
public SemanticVersion getBestMatch() { return bestMatch; }
public void setBestMatch(SemanticVersion bestMatch) { this.bestMatch = bestMatch; }
public int getMatchCount() { return matchCount; }
public void setMatchCount(int matchCount) { this.matchCount = matchCount; }
public static class VersionMatchResultBuilder {
private String range;
private List<SemanticVersion> matches;
private SemanticVersion bestMatch;
private int matchCount;
public VersionMatchResultBuilder range(String range) { this.range = range; return this; }
public VersionMatchResultBuilder matches(List<SemanticVersion> matches) { this.matches = matches; return this; }
public VersionMatchResultBuilder bestMatch(SemanticVersion bestMatch) { this.bestMatch = bestMatch; return this; }
public VersionMatchResultBuilder matchCount(int matchCount) { this.matchCount = matchCount; return this; }
public VersionMatchResult build() {
VersionMatchResult result = new VersionMatchResult();
result.setRange(range);
result.setMatches(matches);
result.setBestMatch(bestMatch);
result.setMatchCount(matchCount);
return result;
}
}
}
class ValidationResult {
private String versionString;
private boolean valid;
private String message;
// builder, getters, setters
public static ValidationResultBuilder builder() { return new ValidationResultBuilder(); }
public String getVersionString() { return versionString; }
public void setVersionString(String versionString) { this.versionString = versionString; }
public boolean isValid() { return valid; }
public void setValid(boolean valid) { this.valid = valid; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public static class ValidationResultBuilder {
private String versionString;
private boolean valid;
private String message;
public ValidationResultBuilder versionString(String versionString) { this.versionString = versionString; return this; }
public ValidationResultBuilder valid(boolean valid) { this.valid = valid; return this; }
public ValidationResultBuilder message(String message) { this.message = message; return this; }
public ValidationResult build() {
ValidationResult result = new ValidationResult();
result.setVersionString(versionString);
result.setValid(valid);
result.setMessage(message);
return result;
}
}
}
class ApiResponse<T> {
private boolean success;
private String message;
private T data;
public ApiResponse(boolean success, String message, T data) {
this.success = success;
this.message = message;
this.data = data;
}
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, "Success", data);
}
public static <T> ApiResponse<T> success(T data, String message) {
return new ApiResponse<>(true, message, data);
}
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(false, message, null);
}
// getters and setters
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public T getData() { return data; }
public void setData(T data) { this.data = data; }
}

Testing

1. Unit Tests
// SemanticVersionTest.java
package com.company.semver;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
class SemanticVersionTest {
@Test
void testParseValidVersions() {
assertThat(SemanticVersion.parse("1.0.0")).isNotNull();
assertThat(SemanticVersion.parse("2.1.3")).isNotNull();
assertThat(SemanticVersion.parse("1.0.0-alpha")).isNotNull();
assertThat(SemanticVersion.parse("1.0.0-alpha.1")).isNotNull();
assertThat(SemanticVersion.parse("1.0.0+build.1")).isNotNull();
assertThat(SemanticVersion.parse("1.0.0-alpha+build.1")).isNotNull();
}
@ParameterizedTest
@ValueSource(strings = {
"1.0",
"1.0.0.0",
"01.0.0",
"1.0.0-",
"1.0.0-01",
"1.0.0-alpha..1",
"1.0.0-alpha.!",
""
})
void testParseInvalidVersions(String invalidVersion) {
assertThatThrownBy(() -> SemanticVersion.parse(invalidVersion))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void testVersionComparison() {
SemanticVersion v1 = SemanticVersion.parse("1.0.0");
SemanticVersion v2 = SemanticVersion.parse("2.0.0");
SemanticVersion v1_1 = SemanticVersion.parse("1.1.0");
SemanticVersion v1_0_1 = SemanticVersion.parse("1.0.1");
SemanticVersion v1_0_0_alpha = SemanticVersion.parse("1.0.0-alpha");
SemanticVersion v1_0_0_beta = SemanticVersion.parse("1.0.0-beta");
assertThat(v1).isLessThan(v2);
assertThat(v1).isLessThan(v1_1);
assertThat(v1).isLessThan(v1_0_1);
assertThat(v1_0_0_alpha).isLessThan(v1);
assertThat(v1_0_0_alpha).isLessThan(v1_0_0_beta);
}
@Test
void testIncrementMethods() {
SemanticVersion v1 = SemanticVersion.parse("1.2.3");
assertThat(v1.incrementMajor()).isEqualTo(SemanticVersion.parse("2.0.0"));
assertThat(v1.incrementMinor()).isEqualTo(SemanticVersion.parse("1.3.0"));
assertThat(v1.incrementPatch()).isEqualTo(SemanticVersion.parse("1.2.4"));
assertThat(v1.incrementMajor("beta")).isEqualTo(SemanticVersion.parse("2.0.0-beta"));
assertThat(v1.incrementMinor("rc")).isEqualTo(SemanticVersion.parse("1.3.0-rc"));
}
@Test
void testCompatibilityChecks() {
SemanticVersion v1_0_0 = SemanticVersion.parse("1.0.0");
SemanticVersion v1_1_0 = SemanticVersion.parse("1.1.0");
SemanticVersion v2_0_0 = SemanticVersion.parse("2.0.0");
assertThat(v1_1_0.isCompatibleWith(v1_0_0)).isTrue();
assertThat(v2_0_0.isCompatibleWith(v1_0_0)).isFalse();
assertThat(v1_1_0.isBackwardCompatibleWith(v1_0_0)).isTrue();
assertThat(v1_0_0.isBackwardCompatibleWith(v1_1_0)).isFalse();
}
@ParameterizedTest
@CsvSource({
"1.0.0, true",
"0.1.0, false",
"1.0.0-alpha, false",
"2.0.0, true"
})
void testStabilityCheck(String versionString, boolean expectedStable) {
SemanticVersion version = SemanticVersion.parse(versionString);
assertThat(version.isStable()).isEqualTo(expectedStable);
}
}
// VersionRangeTest.java
package com.company.semver;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
class VersionRangeTest {
@Test
void testRangeParsing() {
assertThat(VersionRange.parse("1.0.0")).isNotNull();
assertThat(VersionRange.parse(">=1.0.0")).isNotNull();
assertThat(VersionRange.parse("^1.0.0")).isNotNull();
assertThat(VersionRange.parse("~1.0.0")).isNotNull();
assertThat(VersionRange.parse("1.0.0 - 2.0.0")).isNotNull();
assertThat(VersionRange.parse(">=1.0.0 <2.0.0")).isNotNull();
assertThat(VersionRange.parse("1.0.0 || 2.0.0")).isNotNull();
}
@Test
void testRangeMatching() {
VersionRange exact = VersionRange.parse("1.2.3");
VersionRange greater = VersionRange.parse(">=1.0.0");
VersionRange caret = VersionRange.parse("^1.2.0");
VersionRange tilde = VersionRange.parse("~1.2.0");
SemanticVersion v1_2_3 = SemanticVersion.parse("1.2.3");
SemanticVersion v1_2_4 = SemanticVersion.parse("1.2.4");
SemanticVersion v1_3_0 = SemanticVersion.parse("1.3.0");
SemanticVersion v2_0_0 = SemanticVersion.parse("2.0.0");
assertThat(exact.contains(v1_2_3)).isTrue();
assertThat(exact.contains(v1_2_4)).isFalse();
assertThat(greater.contains(v1_2_3)).isTrue();
assertThat(greater.contains(v2_0_0)).isTrue();
assertThat(caret.contains(v1_2_3)).isTrue();
assertThat(caret.contains(v1_3_0)).isTrue();
assertThat(caret.contains(v2_0_0)).isFalse();
assertThat(tilde.contains(v1_2_3)).isTrue();
assertThat(tilde.contains(v1_2_4)).isTrue();
assertThat(tilde.contains(v1_3_0)).isFalse();
}
@Test
void testFindBestMatch() {
VersionManager manager = new VersionManager();
manager.addVersion(SemanticVersion.parse("1.2.3"));
manager.addVersion(SemanticVersion.parse("1.2.4"));
manager.addVersion(SemanticVersion.parse("1.3.0"));
manager.addVersion(SemanticVersion.parse("2.0.0"));
VersionRange range = VersionRange.parse("^1.2.0");
SemanticVersion bestMatch = manager.findBestMatch(range);
assertThat(bestMatch).isEqualTo(SemanticVersion.parse("1.3.0"));
}
}

Configuration

1. Spring Configuration
// SemVerConfig.java
package com.company.semver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SemVerConfig {
@Bean
public VersionManager versionManager() {
return new VersionManager();
}
}
2. Application Properties
# application.yml
spring:
application:
name: semantic-versioning-service
server:
port: 8080
logging:
level:
com.company.semver: INFO

This comprehensive semantic versioning implementation provides:

  • Full SemVer 2.0.0 compliance with proper parsing and validation
  • Immutable version objects with proper equality and comparison
  • Version range support with common syntax (^, ~, >=, etc.)
  • Version management with lifecycle operations
  • REST API for version operations
  • Comprehensive testing with unit tests
  • Compatibility checking for dependency management
  • Analysis tools for version health and gaps

The implementation is production-ready and can be integrated into build systems, dependency management tools, or deployment pipelines to ensure proper versioning practices.

Pyroscope Profiling in Java
Explains how to use Pyroscope for continuous profiling in Java applications, helping developers analyze CPU and memory usage patterns to improve performance and identify bottlenecks.
https://macronepal.com/blog/pyroscope-profiling-in-java/

OpenTelemetry Metrics in Java: Comprehensive Guide
Provides a complete guide to collecting and exporting metrics in Java using OpenTelemetry, including counters, histograms, gauges, and integration with monitoring tools. (MACRO NEPAL)
https://macronepal.com/blog/opentelemetry-metrics-in-java-comprehensive-guide/

OTLP Exporter in Java: Complete Guide for OpenTelemetry
Explains how to configure OTLP exporters in Java to send telemetry data such as traces, metrics, and logs to monitoring systems using HTTP or gRPC protocols. (MACRO NEPAL)
https://macronepal.com/blog/otlp-exporter-in-java-complete-guide-for-opentelemetry/

Thanos Integration in Java: Global View of Metrics
Explains how to integrate Thanos with Java monitoring systems to create a scalable global metrics view across multiple Prometheus instances.

https://macronepal.com/blog/thanos-integration-in-java-global-view-of-metrics

Time Series with InfluxDB in Java: Complete Guide (Version 2)
Explains how to manage time-series data using InfluxDB in Java applications, including storing, querying, and analyzing metrics data.

https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide-2

Time Series with InfluxDB in Java: Complete Guide
Provides an overview of integrating InfluxDB with Java for time-series data handling, including monitoring applications and managing performance metrics.

https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide

Implementing Prometheus Remote Write in Java (Version 2)
Explains how to configure Java applications to send metrics data to Prometheus-compatible systems using the remote write feature for scalable monitoring.

https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide-2

Implementing Prometheus Remote Write in Java: Complete Guide
Provides instructions for sending metrics from Java services to Prometheus servers, enabling centralized monitoring and real-time analytics.

https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide

Building a TileServer GL in Java: Vector and Raster Tile Server
Explains how to build a TileServer GL in Java for serving vector and raster map tiles, useful for geographic visualization and mapping applications.

https://macronepal.com/blog/building-a-tileserver-gl-in-java-vector-and-raster-tile-server

Indoor Mapping in Java
Explains how to create indoor mapping systems in Java, including navigation inside buildings, spatial data handling, and visualization techniques.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper