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.