All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mooltiverse.oss.nyx.version.SemanticVersion Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2020 Mooltiverse
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.mooltiverse.oss.nyx.version;

import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * The implementation of a Semantic Versioning 2.0.0 compliant version.
 * 
* Instances of this class are immutable so whenever you alter some values you actually receive a * new instance holding the new value, while the old one remains unchanged. *
* To get new instances you can also use one of the {@link #valueOf(String)} methods. */ public class SemanticVersion extends Version implements Comparable { /** * Serial version UID to comply with {@link java.io.Serializable} */ private static final long serialVersionUID = 1L; /** * The default value to start from when bumping an identifier that has no numeric value yet. {@value} */ private static final int DEFAULT_BUMP_VALUE = 1; /** * The default initial version that can be used when non version is yet available. */ public static final String DEFAULT_INITIAL_VERSION = "0.1.0"; /** * The character that marks the separation between the core and the pre-release part. Note that this character is * used as separator only at the first occurrence while other occurrences are considered legal characters in the * pre-release and the build identifiers. */ public static final char PRERELEASE_DELIMITER = '-'; /** * The character that marks the separation between the core or the pre-release part and the build part. */ public static final char BUILD_DELIMITER = '+'; /** * A relaxed version of the {@link #SEMANTIC_VERSION_PATTERN} that works also when a prefix appears at the beginning * of the version string or some zeroes appear in front of numbers. * Value is: {@value} * * @see #SEMANTIC_VERSION_PATTERN */ public static final String SEMANTIC_VERSION_PATTERN_RELAXED = "([0-9]\\d*)\\.([0-9]\\d*)\\.([0-9]\\d*)(?:-((?:[0-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:[0-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"; /** * The regexp pattern taken directly from Semantic Versioning 2.0.0 * used to parse semantic versions. Value is: {@value} */ public static final String SEMANTIC_VERSION_PATTERN = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"; /** * Store the immutable string representation to avoid repetitive formatting. */ private transient String renderedString = null; /** * The identifier of the core version part. It can't be {@code null}. */ private final SemanticVersionCoreIdentifier coreIdentifier; /** * The identifier of the pre-release part of the version. It may be {@code null}. */ private final SemanticVersionPreReleaseIdentifier prereleaseIdentifier; /** * The identifier of the build part of the version. It may be {@code null}. */ private final SemanticVersionBuildIdentifier buildIdentifier; /** * Builds a new version object with the given values. * * @param major the {@code major} number * @param minor the {@code minor} number * @param patch the {@code patch} number * * @throws IllegalArgumentException if one of {@code major}, {@code minor}, {@code patch} is * negative */ public SemanticVersion(int major, int minor, int patch) { this(major, minor, patch, null, null); } /** * Builds a new version object with the given values. * * @param major the {@code major} number * @param minor the {@code minor} number * @param patch the {@code patch} number * @param prereleaseIdentifiers the {@code prereleaseIdentifiers} the array of {@link Integer} or {@link String} * objects to use as identifiers in the prerelease block. {@code null} items are ignored. Integers and strings * representing integers must not have leading zeroes or represent negative numbers. If the array is {@code null} * then the instance will have no prerelease block * @param buildIdentifiers the {@code buildIdentifiers} the array of {@link String} to use as identifiers in * the build block. {@code null} items are ignored. If the array is {@code null} then the instance will * have no build block * * @throws IllegalArgumentException if one of {@code major}, {@code minor}, {@code patch} is * negative or one object in the {@code prereleaseIdentifiers} represents a negative integer (either when * passed as an {@link Integer} or {@link String}) or have leading zeroes. This exception is also raised when objects * in the {@code prereleaseIdentifiers} are not of type {@link Integer} or {@link String} or when string * identifiers in the {@code prereleaseIdentifiers} or {@code buildIdentifiers} contain illegal characters */ public SemanticVersion(int major, int minor, int patch, Object[] prereleaseIdentifiers, String[] buildIdentifiers) { this(SemanticVersionCoreIdentifier.valueOf(major, minor, patch), Parser.hasValues(prereleaseIdentifiers) ? SemanticVersionPreReleaseIdentifier.valueOf(true, prereleaseIdentifiers) : null, Parser.hasValues(buildIdentifiers) ? SemanticVersionBuildIdentifier.valueOf(true, buildIdentifiers) : null); } /** * Builds the version with the given identifier values. * * @param coreIdentifier the identifier of the core version part. It can't be {@code null}. * @param prereleaseIdentifier the identifier of the pre-release part of the version. It may be {@code null}. * @param buildIdentifier the identifier of the build part of the version. It may be {@code null}. * * @throws NullPointerException if the core identifier is {@code null} */ private SemanticVersion(SemanticVersionCoreIdentifier coreIdentifier, SemanticVersionPreReleaseIdentifier prereleaseIdentifier, SemanticVersionBuildIdentifier buildIdentifier) { super(); Objects.requireNonNull(coreIdentifier, "Can't build a valid semantic version without the core version numbers"); this.coreIdentifier = coreIdentifier; this.prereleaseIdentifier = prereleaseIdentifier; this.buildIdentifier = buildIdentifier; } /** * Returns a hash code value for the object. * * @return a hash code value for this object. */ @Override public int hashCode() { return 19 * coreIdentifier.hashCode() * (prereleaseIdentifier == null ? 1 : 23 * prereleaseIdentifier.hashCode()) * (buildIdentifier == null ? 1 : 29 * buildIdentifier.hashCode()); } /** * Indicates whether some other object is "equal to" this one. This object equals to the given one if they are the * same object or they are of the same type and hold exactly the same version. * * @param obj the reference object with which to compare. * * @return {@code true} if this object is the same as the {@code obj} argument; {@code false} otherwise. * * @see Object#equals(Object) */ @Override public boolean equals(Object obj) { if (obj == null) return false; if (this == obj) return true; if (!this.getClass().isInstance(obj)) return false; SemanticVersion otherVersion = SemanticVersion.class.cast(obj); if (!coreIdentifier.equals(otherVersion.coreIdentifier)) return false; if (prereleaseIdentifier == null) { if (otherVersion.prereleaseIdentifier != null) return false; } else if (!prereleaseIdentifier.equals(otherVersion.prereleaseIdentifier)) return false; if (buildIdentifier == null) { if (otherVersion.buildIdentifier != null) return false; } else if (!buildIdentifier.equals(otherVersion.buildIdentifier)) return false; return true; } /** * Returns a comparator for identifier names. The returned comparator can be used to sort identifier names * by their relevance, according to Semantic Versioning 2.0.0, * provided that:
* - core identifiers are always more relevant that any other identifier
* - core identifiers are always sorted as {@code major}, {@code minor}, {@code patch}
* - other identifiers are considered pre-release identifiers and are sorted by their natural order
*
* The comparator does not admit {@code null} values. * * @return a comparator for identifier names. * * @see Collections#sort(List, Comparator) */ static Comparator getIdentifierComparator() { return new Comparator() { @Override public int compare(String i1, String i2) { if (i1.equals(i2)) return 0; else if ("major".equals(i1)) return -1; else if ("major".equals(i2)) return 1; else if ("minor".equals(i1)) return -1; else if ("minor".equals(i2)) return +1; else if ("patch".equals(i1)) return -1; else if ("patch".equals(i2)) return 1; else return i1.compareTo(i2); } }; } /** * Compares this version with the specified version for order. Returns a negative integer, zero, or a positive * integer as this object is less than, equal to, or greater than the specified version. *

* Semantic Versioning 2.0.0 states that:
* - rule #9: Pre-release versions have a lower precedence than the associated normal version.
* - rule #10: Build metadata MUST be ignored when determining version precedence. Thus two versions that differ * only in the build metadata, have the same precedence.
* - rule #11: Precedence refers to how versions are compared to each other when ordered. Precedence MUST be * calculated by separating the version into major, minor, patch and pre-release identifiers in that order * (Build metadata does not figure into precedence). Precedence is determined by the first difference when * comparing each of these identifiers from left to right as follows: Major, minor, and patch versions are always * compared numerically. Example: 1.0.0 < 2.0.0 < 2.1.0 < 2.1.1.
* When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version. * Example: 1.0.0-alpha < 1.0.0.
* Precedence for two pre-release versions with the same major, minor, and patch version MUST be determined by * comparing each dot separated identifier from left to right until a difference is found as follows:
* - identifiers consisting of only digits are compared numerically and identifiers with letters or hyphens are * compared lexically in ASCII sort order.
* - numeric identifiers always have lower precedence than non-numeric identifiers
* - a larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding * identifiers are equal. *

* Note that when the specification says "lower precedence" it translates to <, which means the "less" part is * supposed to appear first in order. Translating this to the {@link Comparable#compareTo(Object)} method, it means * that the object with "lower precedence" returns a negative number. *

* However, to cope with the {@link Comparable} contract, which imposes a total ordering, we need to amend the above * rules and make them a little stricter just because two versions with different values can't be considered equal * (also see {@link #equals(Object)}). With more detail: *
* - rule #10 is amended so that two versions that only differ in their build metadata will not return 0 (as if they * the same) but their build metadata of two versions that are equal in their core and prerelease parts affects * the order by their literal comparison (remember that numeric identifiers are not treated as such in build * metadata and, instead, they are just treated as strings). In other words we are not ignoring build metadata as * required by rule #10 but we consider it with the least priority only when the core and prerelease parts are the * same. Two be consistent with rule #9, when comparing two versions with the same core and prerelease parts, when * one has build metadata and the other doesn't, the one with the build metadata has lower precedence on the one * without the build metadata. Example: 1.2.3+build.1 < 1.2.3 and 1.2.3-alpha.0+build.1 < 1.2.3-alpha.0. * * @param v the version to be compared. * * @return a negative integer, zero, or a positive integer as this version is less than, equal to, or greater than * the specified version. * * @see Comparable#compareTo(Object) */ @Override public int compareTo(SemanticVersion v) { if (v == null) return 1; // Rule #9 if (getMajor() != v.getMajor()) return getMajor() - v.getMajor(); if (getMinor() != v.getMinor()) return getMinor() - v.getMinor(); if (getPatch() != v.getPatch()) return getPatch() - v.getPatch(); // Rule #11 if ((prereleaseIdentifier != null) || (v.prereleaseIdentifier != null)) { // When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version. // Example: 1.0.0-alpha < 1.0.0. // So if only one has the prerelease block, that means it has lower precedence (comes first) if ((prereleaseIdentifier != null) && (v.prereleaseIdentifier == null)) return -1; else if ((prereleaseIdentifier == null) && (v.prereleaseIdentifier != null)) return 1; } if (prereleaseIdentifier != null) { // Precedence for two pre-release versions with the same major, minor, and patch version MUST be determined // by comparing each dot separated identifier from left to right until a difference is found as follows: // identifiers consisting of only digits are compared numerically and identifiers with letters or hyphens // are compared lexically in ASCII sort order. Iterator thisIterator = prereleaseIdentifier.getValue().iterator(); Iterator otherIterator = v.prereleaseIdentifier.getValue().iterator(); while (thisIterator.hasNext()) { if (otherIterator.hasNext()) { Object thisItem = thisIterator.next().getValue(); Object otherItem = otherIterator.next().getValue(); // Identifiers consisting of only digits are compared numerically and identifiers with letters or // hyphens are compared lexically in ASCII sort order. Numeric identifiers always have lower // precedence than non-numeric identifiers. if (Integer.class.isInstance(thisItem)) { // This item is a number if (Integer.class.isInstance(otherItem)) { // Also the other item is a number so let's compare them as such int res = Integer.class.cast(thisItem).compareTo(Integer.class.cast(otherItem)); if (res != 0) return res; } else { // This item is a number while the other is a string, so this has lower precedence (comes first) return -1; } } else { // This item is a string if (Integer.class.isInstance(otherItem)) { // the other item is a number while this is a string so the other has lower precedence (comes first) than this return 1; } else { // Also the other item is a string so let's see how they compare as strings int res = thisItem.toString().compareTo(otherItem.toString()); if (res != 0) return res; } } } else { // This set has more elements than the other so it has more precedence (comes after in order) // // A larger set of pre-release fields has a higher precedence than a smaller set, if all of the // preceding identifiers are equal. // Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0. return 1; } } if (otherIterator.hasNext()) { // This set has less elements than the other so it has less precedence (comes first in order) // // A larger set of pre-release fields has a higher precedence than a smaller set, if all of the // preceding identifiers are equal. // Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0. return -1; } } // Rule #10 (amended to achieve total order) if ((buildIdentifier != null) || (v.buildIdentifier != null)) { // Although the build part should be ignored from SemVer specs, the compareTo method needs to differentiate // versions that have the same core and prerelease but different build parts. // We chose to be consistent with rule #10 (about prerelease) so the version that has a build part has // When major, minor, and patch and prerelease are equal, a build version has lower precedence than a normal // (or normal-prerelease) version. // Example: 1.0.0-alpha+build < 1.0.0-alpha, and 1.0.0+build < 1.0.0. if ((buildIdentifier != null) && (v.buildIdentifier == null)) return -1; else if ((buildIdentifier == null) && (v.buildIdentifier != null)) return 1; } // If no difference is found, let's consider the number of items in the build part. This is not requested by // SemVer but, again, we use the same behavior as for prerelease. So the set that has less elements has less // precedence (comes first in order). if ((buildIdentifier != null) && (v.buildIdentifier != null)) { if (buildIdentifier.children.size() != v.buildIdentifier.children.size()) return v.buildIdentifier.children.size() - buildIdentifier.children.size(); } // If no difference has been found yet, let's just compare the build parts as strings if ((buildIdentifier != null) && (buildIdentifier.toString().compareTo(v.buildIdentifier.toString()) != 0)) return buildIdentifier.toString().compareTo(v.buildIdentifier.toString()); // As a last resort, compare the string version of both entities. This is not requested (explicitly) by SemVer // but it's still compliant with it and with Comparable as well. return toString().compareTo(v.toString()); } /** * Returns {@link Scheme#SEMVER}. * * @return {@link Scheme#SEMVER} */ public final Scheme getScheme() { return Scheme.SEMVER; } /** * Returns {@code true} if the given string is a legal semantic version which, for example, can be parsed using * {@link #valueOf(String)} without exceptions. *
* This method uses a strict criteria, without trying to sanitize the given string. * * @param s the string version to check. * * @return {@code true} if the given string represents a legal semantic version, {@code false} otherwise. * * @see #valueOf(String) */ public static boolean isLegal(String s) { return isLegal(s, false); } /** * Returns {@code true} if the given string is a legal semantic version which, for example, can be parsed using * {@link #valueOf(String, boolean)} without exceptions. * * @param s the string version to check. * @param lenient when {@code true} prefixes and non critical extra characters are tolerated even if they are not * strictly legal from the semantic version specification perspective. * * @return {@code true} if the given string represents a legal semantic version, {@code false} otherwise. * * @see #valueOf(String, boolean) */ public static boolean isLegal(String s, boolean lenient) { Objects.requireNonNull(s, "Can't parse a null string"); Matcher m = Pattern.compile(lenient ? SEMANTIC_VERSION_PATTERN_RELAXED : SEMANTIC_VERSION_PATTERN).matcher(s); return m.find(); } /** * Returns a string representation of the object. * * @return a string representation of the object. * * @see Object#toString() */ @Override public String toString() { if (renderedString == null) { StringBuilder sb = new StringBuilder(coreIdentifier.toString()); if (prereleaseIdentifier != null) { sb.append(PRERELEASE_DELIMITER); sb.append(prereleaseIdentifier.toString()); } if (buildIdentifier != null) { sb.append(BUILD_DELIMITER); sb.append(buildIdentifier.toString()); } renderedString = sb.toString(); } return renderedString; } /** * Returns a SemanticVersion instance representing the specified String value. No sanitization attempt is done. * * @param s the string to parse * * @return the new SemanticVersion instance representing the given string. * * @throws NullPointerException if the given string is {@code null} * @throws IllegalArgumentException if the given string doesn't represent a legal semantic version * * @see #valueOf(String, boolean) * @see #isLegal(String) * @see #sanitizePrefix(String) */ public static SemanticVersion valueOf(String s) { Objects.requireNonNull(s, "Can't parse a null string"); if (s.isBlank()) throw new IllegalArgumentException("Can't parse an empty string"); Matcher m = Pattern.compile(SEMANTIC_VERSION_PATTERN).matcher(s); if (m.find()) { // group 0 is the entire version string // group 1 is the major number // group 2 is the minor number // group 3 is the patch number // group 4 (optional) is the prerelease // group 5 (optional) is the build SemanticVersionCoreIdentifier coreIdentifier = SemanticVersionCoreIdentifier.valueOf(m.group(1), m.group(2), m.group(3)); SemanticVersionPreReleaseIdentifier preReleaseIdentifier = null; if ((m.group(4) != null) && (!m.group(4).isBlank())) preReleaseIdentifier = SemanticVersionPreReleaseIdentifier.valueOf(true, m.group(4)); SemanticVersionBuildIdentifier buildIdentifier = null; if ((m.group(5) != null) && (!m.group(5).isBlank())) buildIdentifier = SemanticVersionBuildIdentifier.valueOf(true, m.group(5)); return new SemanticVersion(coreIdentifier, preReleaseIdentifier, buildIdentifier); } else throw new IllegalArgumentException(String.format("The string '%s' does not contain a valid semantic number", s)); } /** * This method is a shorthand for {@link #valueOf(String)} and {@link #sanitize(String)}. *
* Returns a SemanticVersion instance representing the specified String value. If {@code sanitize} is * {@code true} this method will try to sanitize the given string before parsing so that if there are * illegal characters like a prefix or leading zeroes in numeric identifiers they are removed. *
* When sanitization is enabled on a string that actually needs sanitization the string representation of the * returned object will not exactly match the input value. * * @param s the string to parse * @param sanitize optionally enables sanitization before parsing * * @return the new SemanticVersion instance representing the given string. * * @throws NullPointerException if the given string is {@code null} * @throws IllegalArgumentException if the given string doesn't represent a legal semantic version * * @see #valueOf(String) * @see #sanitizePrefix(String) */ public static SemanticVersion valueOf(String s, boolean sanitize) { return valueOf(sanitize ? sanitize(s) : s); } /** * Performs all of the sanitizations in the given string by sanitizing, in order, the prefix and leading zeroes * in numeric identifiers. This order is the one that yields to the highest success probability in obtaining a * legal version. * Invoking this method is like invoking the sanitization methods {@link #sanitizeNumbers(String)} * and {@link #sanitizePrefix(String)}. * * @param s the semantic version string to sanitize * * @return the sanitized semantic string version * * @throws NullPointerException if the given string is {@code null} * @throws IllegalArgumentException if the given string doesn't represent a semantic version, even tolerating * the aspects to sanitize * * @see #sanitizePrefix(String) * @see #sanitizeNumbers(String) */ public static String sanitize(String s) { return sanitizeNumbers(sanitizePrefix(s)); } /** * Takes the given string and tries to parse it as a semantic version number, even with illegal characters or prefix. * All numeric identifiers in the core version ({@code major.minor.patch}) and in the prerelease metadata are * sanitized by removing all leading zeroes to make them compliant. Numeric identifiers in the build metadata part * are left intact, even when they have leading zeroes. * If the given string contains a prefix (see {@link #sanitizePrefix(String)}) or illegal characters they are left * untouched and they are returned as they were in the input string. * * @param s a semantic version string which may have illegal leading zeroes to be removed in the numeric identifiers * in the core or the prerelease parts. * * @return the string given as input with the illegal leading zeroes removed. * * @throws NullPointerException if the given string is {@code null} * @throws IllegalArgumentException if the given string doesn't represent a legal semantic version, even tolerating * the prefix * * @see #sanitize(String) */ public static String sanitizeNumbers(String s) { Objects.requireNonNull(s, "Can't parse a null string"); if (s.isBlank()) throw new IllegalArgumentException("Can't parse an empty string"); StringBuilder result = new StringBuilder(); // if there's any prefix, leave it there in the result String prefix = getPrefix(s); if (prefix != null) result.append(prefix); Matcher m = Pattern.compile(SEMANTIC_VERSION_PATTERN_RELAXED).matcher(s); if (m.find()) { // group 0 is the entire version string // group 1 is the major number // group 2 is the minor number // group 3 is the patch number // group 4 (optional) is the prerelease // group 5 (optional) is the build try { // to remove leading zeroes, just transform the numbers to Integers and back to strings Integer integer = Integer.valueOf(m.group(1)); if (integer.intValue()<0) throw new IllegalArgumentException(String.format("Can't sanitize negative number '%d' in '%s'", integer, s)); result.append(integer.toString()); result.append(CompositeIdentifier.DEFAULT_SEPARATOR); integer = Integer.valueOf(m.group(2)); if (integer.intValue()<0) throw new IllegalArgumentException(String.format("Can't sanitize negative number '%d' in '%s'", integer, s)); result.append(integer.toString()); result.append(CompositeIdentifier.DEFAULT_SEPARATOR); integer = Integer.valueOf(m.group(3)); if (integer.intValue()<0) throw new IllegalArgumentException(String.format("Can't sanitize negative number '%d' in '%s'", integer, s)); result.append(integer.toString()); } catch (NumberFormatException nfe) { throw new IllegalArgumentException(String.format("Numeric identifiers in string '%s' can't be converted to valid Integers", s), nfe); } // Go through all identifiers in the prerelease part. If they can convert to an integer just do it and // append their string representation to the output, this automatically removes leading zeroes. // If they can't be converted to numberst just append them as they are. if ((m.group(4) != null) && (!m.group(4).isBlank())) { result.append(PRERELEASE_DELIMITER); List identifiers = Arrays.asList(m.group(4).split("["+CompositeIdentifier.DEFAULT_SEPARATOR+"]")); Iterator idIterator = identifiers.iterator(); while (idIterator.hasNext()) { String identifier = idIterator.next(); try { result.append(Integer.valueOf(identifier).toString()); } catch (NumberFormatException nfe) { // it wasn't an integer, so append it as a string result.append(identifier); } if (idIterator.hasNext()) result.append(CompositeIdentifier.DEFAULT_SEPARATOR); } } // append the build part just as it was if ((m.group(5) != null) && (!m.group(5).isBlank())) { result.append(BUILD_DELIMITER); result.append(m.group(5)); } } else throw new IllegalArgumentException(String.format("The string '%s' does not contain a valid semantic number", s)); return result.toString(); } /** * Takes the given string and tries to parse it as a semantic version with an optional prefix (which may be any * string before the core {@code major.minor.patch} numbers). The returned string is the semantic version passed * as input with the prefix removed. If no prefix is present then the returned string is the same as the one passed * as input. Prefixes are often used (i.e. the 'v' used it Git tags or {@code release-}, {@code rel} etc) * so this method helps in stripping those prefixes to get a compliant semantic version. * * @param s a semantic version string which may have an additional prefix to be removed * * @return the string given as input with the prefix removed, if any, or the same string passed as input if no prefix * was found. * * @throws NullPointerException if the given string is {@code null} * @throws IllegalArgumentException if the given string doesn't represent a legal semantic version, even tolerating * the prefix * * @see #sanitize(String) * @see #getPrefix(String) */ public static String sanitizePrefix(String s) { Objects.requireNonNull(s, "Can't sanitize a null string"); if (s.isBlank()) throw new IllegalArgumentException("Can't sanitize an empty string"); Matcher m = Pattern.compile(SEMANTIC_VERSION_PATTERN_RELAXED).matcher(s); if (m.find()) { // group 0 is the entire version string return m.group(0); } else throw new IllegalArgumentException(String.format("The string '%s' does not contain a valid semantic number", s)); } /** * Takes the given string and tries to parse it as a semantic version with an optional prefix (which may be any * string before the core {@code major.minor.patch} numbers). The returned string is the prefix before the core * version number, if any, or {@code null} otherwise. * * @param s a semantic version string which may have an additional prefix to be isolated * * @return the prefix in the given semantic version string, if any, or {@code null} otherwise. {@code null} * is also returned when the given string is empty. * * @throws NullPointerException if the given string is {@code null} * @throws IllegalArgumentException if the given string is not empty but doesn't represent a legal semantic version, * even tolerating the prefix * * @see #sanitizePrefix(String) */ public static String getPrefix(String s) { Objects.requireNonNull(s, "Can't parse a null string"); if (s.isBlank()) return null; Matcher m = Pattern.compile(SEMANTIC_VERSION_PATTERN_RELAXED).matcher(s); if (m.find()) { // group 0 is the entire version string if (s.equals(m.group(0))) return null; else return s.substring(0, s.length()-m.group(0).length()); } else throw new IllegalArgumentException(String.format("The string '%s' does not contain a valid semantic number", s)); } /** * Returns the major version number * * @return the major version number */ public int getMajor() { return coreIdentifier.getMajor(); } /** * Returns the minor version number * * @return the minor version number */ public int getMinor() { return coreIdentifier.getMinor(); } /** * Returns the patch version number * * @return the patch version number */ public int getPatch() { return coreIdentifier.getPatch(); } /** * Returns the core part ({@code major.minor.patch}) of the version as a string. * * @return the core part of the version as a string. */ public String getCore() { return coreIdentifier.toString(); } /** * Returns an array of the single identifiers of the core part of the version * * @return the identifiers of the core part of the version. */ public Integer[] getCoreIdentifiers() { Integer[] res = new Integer[coreIdentifier.children.size()]; for (int i=0; i * If this version doesn't have a prerelease part, the returned version will have one with the new attribute appended * (and its value as well, if not {@code null}). *
* If this version already has a prerelease part with no identifier matching the given attribute name then the returned * version will have the same prerelease part as the current one with the new attribute appended (and its value as well, * if not {@code null}). *
* If this version already has a prerelease part that contains an identifier matching the given attribute name then * the identifier matching the attribute name is left unchanged and if the given value is not {@code null}, * the next identifier is added or replaced with the given value.
* ATTENTION: if the value is not {@code null} the current identifier after the name (if any) is replaced * if it's a numeric identifier. *
* Examples of invoking {@code setPrereleaseAttribute("build")} with {@code null} value:
* - {@code 1.2.3 = 1.2.3-build}
* - {@code 1.2.3-alpha = 1.2.3-alpha.build}
* - {@code 1.2.3-alpha.beta = 1.2.3-alpha.beta.build}
* - {@code 1.2.3+timestamp = 1.2.3-build+timestamp}
* - {@code 1.2.3-alpha+timestamp.20200101 = 1.2.3-alpha.build+timestamp.20200101}
* - {@code 1.2.3-build = 1.2.3-build} (unchanged)
* - {@code 1.2.3-build.12345 = 1.2.3-build.12345} (unchanged)
* - {@code 1.2.3-build.12345.timestamp.20200101 = 1.2.3-build.12345.timestamp.20200101} (unchanged)
*
* Examples of invoking {@code setPrereleaseAttribute("build")} with {@code 12345} value:
* - {@code 1.2.3 = 1.2.3-build.12345}
* - {@code 1.2.3-alpha = 1.2.3-alpha.build.12345}
* - {@code 1.2.3-alpha.beta = 1.2.3-alpha.beta.build.12345}
* - {@code 1.2.3+timestamp = 1.2.3-build.12345+timestamp}
* - {@code 1.2.3-alpha+timestamp.20200101 = 1.2.3-alpha.build.12345+timestamp.20200101}
* - {@code 1.2.3-build = 1.2.3-build.12345}
* - {@code 1.2.3-build.12345 = 1.2.3-build.12345} (unchanged)
* - {@code 1.2.3-build.12345.timestamp.20200101 = 1.2.3-build.12345.timestamp.20200101} (unchanged)
* * @param name the name to set for the attribute * @param value the value to set for the attribute, or {@code null} just set the attribute name, ignoring the value * * @return the new version instance * * @throws IllegalArgumentException if the given name or value contains illegal characters * @throws NullPointerException if the attribute name is {@code null} */ public SemanticVersion setPrereleaseAttribute(String name, Integer value) { Objects.requireNonNull(name, "Can't set a null attribute name"); // SemanticVersionBuildIdentifier.valueOf does not accept a null for a second parameter (value) so we need to discriminate here if (prereleaseIdentifier == null) return new SemanticVersion(coreIdentifier, value == null ? SemanticVersionPreReleaseIdentifier.valueOf(false, name) : SemanticVersionPreReleaseIdentifier.valueOf(false, name, value), buildIdentifier); else return new SemanticVersion(coreIdentifier, prereleaseIdentifier.setAttribute(name, value), buildIdentifier); } /** * Returns a new version object with the new attribute added or replaced in the prerelease part. * This method is a shorthand for {@link #setPrereleaseAttribute(String, Integer) setPrereleaseAttribute(value, null)} to only * set a simple identifier instead of a pair. * * @param name the name to set for the attribute * * @return the new version instance * * @throws IllegalArgumentException if the given name contains illegal characters * @throws NullPointerException if the attribute name is {@code null} * * @see #setPrereleaseAttribute(String, Integer) */ public SemanticVersion setPrereleaseAttribute(String name) { return setPrereleaseAttribute(name, null); } /** * Returns a new version object with the new attribute added or replaced in the build part. This method tries to be * less intrusive as it only works on the given attribute (and its optional value) while leaving the other attributes * unchanged. *
* If this version doesn't have a build part, the returned version will have one with the new attribute appended * (and its value as well, if not {@code null}). *
* If this version already has a build part with no identifier matching the given attribute name then the returned * version will have the same build part as the current one with the new attribute appended (and its value as well, * if not {@code null}). *
* If this version already has a build part that contains an identifier matching the given attribute name then * the identifier matching the attribute name is left unchanged and if the given value is not {@code null}, * the next identifier is added or replaced with the given value.
* ATTENTION: if the value is not {@code null} the current identifier after the name (if any) is replaced * without further consideration. *
* Examples of invoking {@code setBuildAttribute("build")} with {@code null} value:
* - {@code 1.2.3 = 1.2.3+build}
* - {@code 1.2.3-alpha = 1.2.3-alpha+build}
* - {@code 1.2.3-alpha.beta = 1.2.3-alpha.beta+build}
* - {@code 1.2.3+timestamp = 1.2.3+timestamp.build}
* - {@code 1.2.3-alpha+timestamp.20200101 = 1.2.3-alpha+timestamp.20200101.build}
* - {@code 1.2.3+build = 1.2.3+build} (unchanged)
* - {@code 1.2.3+build.12345 = 1.2.3+build.12345} (unchanged)
* - {@code 1.2.3+build.12345.timestamp.20200101 = 1.2.3+build.12345.timestamp.20200101} (unchanged)
*
* Examples of invoking {@code setBuildAttribute("build")} with {@code 12345} value:
* - {@code 1.2.3 = 1.2.3+build.12345}
* - {@code 1.2.3-alpha = 1.2.3-alpha+build.12345}
* - {@code 1.2.3-alpha.beta = 1.2.3-alpha.beta+build.12345}
* - {@code 1.2.3+timestamp = 1.2.3+timestamp.build.12345}
* - {@code 1.2.3-alpha+timestamp.20200101 = 1.2.3-alpha+timestamp.20200101.build.12345}
* - {@code 1.2.3+build = 1.2.3+build.12345}
* - {@code 1.2.3+build.12345 = 1.2.3+build.12345} (unchanged)
* - {@code 1.2.3+build.12345.timestamp.20200101 = 1.2.3+build.12345.timestamp.20200101} (unchanged)
* * @param name the name to set for the attribute * @param value the value to set for the attribute, or {@code null} just set the attribute name, ignoring the value * * @return the new version instance * * @throws IllegalArgumentException if the given name or value contains illegal characters * @throws NullPointerException if the attribute name is {@code null} */ public SemanticVersion setBuildAttribute(String name, String value) { Objects.requireNonNull(name, "Can't set a null attribute name"); // SemanticVersionBuildIdentifier.valueOf does not accept a null for a second parameter (value) so we need to discriminate here if (buildIdentifier == null) return new SemanticVersion(coreIdentifier, prereleaseIdentifier, value == null ? SemanticVersionBuildIdentifier.valueOf(false, name) : SemanticVersionBuildIdentifier.valueOf(false, name, value)); else return new SemanticVersion(coreIdentifier, prereleaseIdentifier, buildIdentifier.setAttribute(name, value)); } /** * Returns a new version object with the new attribute added or replaced in the build part. * This method is a shorthand for {@link #setBuildAttribute(String, String) setBuildAttribute(value, null)} to only * set a simple identifier instead of a pair. * * @param name the name to set for the attribute * * @return the new version instance * * @throws IllegalArgumentException if the given name contains illegal characters * @throws NullPointerException if the attribute name is {@code null} * * @see #setBuildAttribute(String, String) */ public SemanticVersion setBuildAttribute(String name) { return setBuildAttribute(name, null); } /** * Returns a new instance with the new attribute removed from the prerelease part, if any was present, otherwise the same version is returned. * If the attribute is found and {@code removeValue} is {@code true} then also the attribute value (the attribute after the * one identified by {@code name}) is removed, unless there are no more attributes after {@code name} or the value attribute * is not numeric. * * @param name the name of the attribute to remove from the prerelease part, if present. If {@code null} or empty no action is taken * @param removeValue if {@code true} also the attribute after {@code name} is removed (if any) * * @return the new instance, which might be the same of the current object if no attribute with the given {@code name} * is present */ public SemanticVersion removePrereleaseAttribute(String name, boolean removeValue) { return prereleaseIdentifier == null ? this : new SemanticVersion(coreIdentifier, prereleaseIdentifier.removeAttribute(name, removeValue), buildIdentifier); } /** * Returns a new instance with the new attribute removed from the build part, if any was present, otherwise the same version is returned. * If the attribute is found and {@code removeValue} is {@code true} then also the attribute value (the attribute after the * one identified by {@code name}) is removed, unless there are no more attributes after {@code name}. * * @param name the name of the attribute to remove from the build part, if present. If {@code null} or empty no action is taken * @param removeValue if {@code true} also the attribute after {@code name} is removed (if any) * * @return the new instance, which might be the same of the current object if no attribute with the given {@code name} * is present */ public SemanticVersion removeBuildAttribute(String name, boolean removeValue) { return buildIdentifier == null ? this : new SemanticVersion(coreIdentifier, prereleaseIdentifier, buildIdentifier.removeAttribute(name, removeValue)); } /** * Returns a new instance with the major number of this current instance incremented by one and the minor and patch * numbers reset to zero. Prerelease and build parts are left intact. * * @return a new instance with the major number of this current instance incremented by one and the minor and patch * numbers reset to zero. */ public SemanticVersion bumpMajor() { return new SemanticVersion(coreIdentifier.bumpMajor(), prereleaseIdentifier, buildIdentifier); } /** * Returns a new instance with the major number of this current instance, the minor number incremented by one and * the patch number reset to zero. Prerelease and build parts are left intact. * * @return a new instance with the major number of this current instance, the minor number incremented by one and * the patch number reset to zero. */ public SemanticVersion bumpMinor() { return new SemanticVersion(coreIdentifier.bumpMinor(), prereleaseIdentifier, buildIdentifier); } /** * Returns a new instance with the major and minor numbers of this current instance and the patch number * incremented by one. Prerelease and build parts are left intact. * * @return a new instance with the major and minor numbers of this current instance and the patch number * incremented by one. */ public SemanticVersion bumpPatch() { return new SemanticVersion(coreIdentifier.bumpPatch(), prereleaseIdentifier, buildIdentifier); } /** * Returns a new instance with the number identified by the given value bumped. * * @param id the selector of the identifier to bump * * @return a new instance with the number identified by the given value bumped. * * @throws NullPointerException if {@code null} is passed */ public SemanticVersion bump(CoreIdentifiers id) { Objects.requireNonNull(id); switch (id) { case MAJOR: return bumpMajor(); case MINOR: return bumpMinor(); case PATCH: return bumpPatch(); default: throw new IllegalArgumentException(id.toString()); } } /** * Returns a new instance with the number identified by the given value bumped in the prerelease part. The core and * the build blocks (if present) are left unchanged. *
* If this version doesn't have a prerelease block the returned version will have one, containing two identifiers: * the given string and the following number {@code .0}. *
* If this version already has a prerelease block without any identifier that equals the given id, then the returned * version has all the previous prerelease identifiers preceded by the two new identifiers the given string and * the following number {@code .1}. * If this version already has a prerelease block that contains a string identifier equal to the given id there are * two options: if the selected identifier already has a numeric value that follows, the returned version will have * that numeric identifier incremented by one; if the selected identifier doesn't have a numeric identifier that * follows, a new numeric identifiers is added after the string with the initial value {@code .1}. *
* If the version already has multiple identifiers in the prerelease block that equal to the given value then all of * them will be bumped. In case they have different numeric values (or missing) each occurrence is bumped * independently according to the above rules. *
* Examples of invoking {@code bumpPrerelease("alpha")} on different versions:
* - {@code 1.2.3 = 1.2.3-alpha.0}
* - {@code 1.2.3-alpha = 1.2.3-alpha.0}
* - {@code 1.2.3-alpha.beta = 1.2.3-alpha.0.beta}
* - {@code 1.2.3-gamma = 1.2.3-alpha.0.gamma}
* - {@code 1.2.3-gamma.delta = 1.2.3-alpha.0.gamma.delta}
* - {@code 1.2.3+999 = 1.2.3-alpha.0+999}
* - {@code 1.2.3-alpha+999 = 1.2.3-alpha.0+999}
* - {@code 1.2.3-alpha.beta+999 = 1.2.3-alpha.0.beta+999}
* - {@code 1.2.3-gamma+999 = 1.2.3-alpha.0.gamma+999}
* - {@code 1.2.3-gamma.delta+999 = 1.2.3-alpha.0.gamma.delta+999}
* - {@code 1.2.3-alpha.alpha.1.alpha.2 = 1.2.3-alpha.0.alpha.2.alpha.3}
* * @param id the selector of the identifier to bump * * @return a new instance with the number identified by the given value bumped. * * @throws NullPointerException if {@code null} is passed * @throws IllegalArgumentException if the given string is empty, contains illegal characters or represents a number */ public SemanticVersion bumpPrerelease(String id) { Objects.requireNonNull(id, "Can't bump a null identifier"); if (id.isBlank()) throw new IllegalArgumentException("Can't bump an empty identifier"); try { Integer.valueOf(id); // it's a number and can't be bumped throw new IllegalArgumentException(String.format("The value '%s' is numeric and can't be used as a string identifier in the prerelease", id)); } catch (NumberFormatException nfe) { // ok, not a number. Proceed } return new SemanticVersion(coreIdentifier, prereleaseIdentifier == null ? SemanticVersionPreReleaseIdentifier.valueOf(false, id, Integer.valueOf(DEFAULT_BUMP_VALUE)) : prereleaseIdentifier.bump(id, DEFAULT_BUMP_VALUE), buildIdentifier); } /** * Returns a new instance with the number identified by the given value bumped. If the given value represents a core * identifier ({@link CoreIdentifiers}, namely {@code major}, {@code minor}, {@code patch}) then that * identifier is bumped, otherwise the given id is used to bump a prerelease identifier by invoking * {@link #bumpPrerelease(String)}. *
* In other words this method is a shorthand for {@link #bump(CoreIdentifiers)} and {@link #bumpPrerelease(String)}, * the latter being used only when the given id is not a core identifier. *
* If the version already has multiple identifiers in the prerelease block that equal to the given value then all of * them will be bumped. In case they have different numeric values (or missing) each occurrence is bumped * independently according to the above rules. * * @param id the name of the identifier to bump * * @return a new instance with the number identified by the given value bumped. * * @throws NullPointerException if {@code null} is passed * @throws IllegalArgumentException if the given string is empty, contains illegal characters or represents a number * * @see CoreIdentifiers * @see #bump(CoreIdentifiers) * @see #bumpPrerelease(String) */ @Override public SemanticVersion bump(String id) { Objects.requireNonNull(id, "Can't bump a null identifier"); if (id.isBlank()) throw new IllegalArgumentException("Can't bump an empty identifier"); if (CoreIdentifiers.hasName(id)) return bump(CoreIdentifiers.byName(id)); else return bumpPrerelease(id); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy