├── .gitignore ├── Jenkinsfile ├── JenkinsfileRelease ├── LICENSE ├── README.md ├── RELEASE_NOTES.md ├── pom.xml └── src ├── it └── java │ └── de │ └── skuzzle │ └── semantic │ ├── IsPreReleasePerformanceIT.java │ ├── ParsingPerformanceIT.java │ ├── SortingPerformanceIT.java │ ├── VersionPerformanceTestBase.java │ └── VersionRegEx.java ├── main └── java │ ├── de │ └── skuzzle │ │ └── semantic │ │ ├── Version.java │ │ └── package-info.java │ └── module-info.java └── test ├── java └── de │ └── skuzzle │ └── semantic │ ├── CustomGsonSerialization.java │ ├── CustomJacksonSerialization.java │ ├── IncrementationTest.java │ ├── Java6CompatibilityTest.java │ ├── ParsingTest.java │ └── VersionTest.java └── resources ├── versions_0.5.bin └── versions_0.6.bin /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | 14 | .classpath 15 | .project 16 | .settings/ 17 | target/ -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | docker { 4 | image 'maven:3.6-jdk-11' 5 | args '-v /home/jenkins/.m2:/var/maven/.m2 -v /home/jenkins/.gnupg:/.gnupg -e MAVEN_CONFIG=/var/maven/.m2 -e MAVEN_OPTS=-Duser.home=/var/maven' 6 | } 7 | } 8 | environment { 9 | COVERALLS_REPO_TOKEN = credentials('coveralls_repo_token_semantic_version') 10 | GPG_SECRET = credentials('gpg_password') 11 | } 12 | stages { 13 | stage('Build') { 14 | steps { 15 | sh 'mvn -B clean verify' 16 | } 17 | } 18 | stage('Coverage') { 19 | steps { 20 | sh 'mvn -B jacoco:report jacoco:report-integration coveralls:report -DrepoToken=$COVERALLS_REPO_TOKEN' 21 | } 22 | } 23 | stage('javadoc') { 24 | steps { 25 | sh 'mvn -B javadoc:javadoc' 26 | } 27 | } 28 | stage('Deploy SNAPSHOT') { 29 | when { 30 | branch 'dev' 31 | } 32 | steps { 33 | sh 'mvn -B -Prelease -DskipTests -Dgpg.passphrase=${GPG_SECRET} deploy' 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /JenkinsfileRelease: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | docker { 4 | image 'maven:3.6-jdk-11' 5 | args '-v /home/jenkins/.m2:/var/maven/.m2 -v /home/jenkins/.gnupg:/.gnupg -e MAVEN_CONFIG=/var/maven/.m2 -e MAVEN_OPTS=-Duser.home=/var/maven' 6 | } 7 | } 8 | environment { 9 | GPG_SECRET = credentials('gpg_password') 10 | GITHUB = credentials('Github-Username-Pw') 11 | GITHUB_RELEASE_TOKEN = credentials('github_registry_release') 12 | GIT_ASKPASS='./.git-askpass' 13 | } 14 | stages { 15 | stage ('Ensure dev branch') { 16 | when { 17 | expression { 18 | return env.BRANCH_NAME != 'dev'; 19 | } 20 | } 21 | steps { 22 | error("Releasing is only possible from dev branch") 23 | } 24 | } 25 | stage ('Set Git Information') { 26 | steps { 27 | sh 'echo \'echo \$GITHUB_PSW\' > ./.git-askpass' 28 | sh 'chmod +x ./.git-askpass' 29 | sh 'git config url."https://api@github.com/".insteadOf "https://github.com/"' 30 | sh 'git config url."https://ssh@github.com/".insteadOf "ssh://git@github.com/"' 31 | sh 'git config url."https://git@github.com/".insteadOf "git@github.com:"' 32 | sh 'git config user.email "build@taddiken.online"' 33 | sh 'git config user.name "Jenkins"' 34 | } 35 | } 36 | stage('Create release branch') { 37 | steps { 38 | sh 'mvn -B -Prelease gitflow:release-start' 39 | } 40 | } 41 | stage('Verify release') { 42 | steps { 43 | sh 'mvn -B -Prelease -Dgpg.passphrase=${GPG_SECRET} verify' 44 | } 45 | } 46 | stage('Perform release') { 47 | steps { 48 | sh "mvn -B gitflow:release-finish -DargLine=\"-Prelease -B -Dgpg.passphrase=${GPG_SECRET} -DskipTests\"" 49 | } 50 | } 51 | stage('Create GitHub release') { 52 | steps { 53 | sh 'git checkout master' 54 | sh "mvn -B github-release:github-release -Dgithub.release-token=${GITHUB_RELEASE_TOKEN}" 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Simon Taddiken 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/skuzzle/semantic-version.svg?branch=master)](https://travis-ci.org/skuzzle/semantic-version) 2 | [![Coverage Status](https://coveralls.io/repos/github/skuzzle/semantic-version/badge.svg?branch=master)](https://coveralls.io/github/skuzzle/semantic-version?branch=master) 3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/de.skuzzle/semantic-version/badge.svg)](https://maven-badges.herokuapp.com/maven-central/de.skuzzle/semantic-version) 4 | [![JavaDoc](http://javadoc-badge.appspot.com/de.skuzzle/semantic-version.svg?label=JavaDoc)](http://javadoc-badge.appspot.com/de.skuzzle/semantic-version) 5 | [![Twitter Follow](https://img.shields.io/twitter/follow/skuzzleOSS.svg?style=social)](https://twitter.com/skuzzleOSS) 6 | 7 | 8 | semantic-version 9 | ================ 10 | 11 | This is a single-class [semantic version 2.0.0](http://semver.org/) 12 | implementation for java 6+. It requires no further dependencies and is thereby 13 | easy to use within your own projects. Key features: 14 | 15 | * Lightweight: consists of only a single file, no dependencies 16 | * Immutable: strict immutability ensures easy handling and thread safety 17 | * Serializable: Objects can be serialized using Java's `ObjectOutputStream`. 18 | * Fast: Many performance improvements make this the fastest semver implementation in java 19 | around (according to parsing and sorting performance) 20 | * Compatible: Supports Java 6 but also provides many methods that are suitable to be used 21 | as method references in Java 8. Latest release also features a Java 9 module-info! 22 | * Stable: Ready for production since release 1.0.0 23 | 24 | ## Maven Dependency 25 | semantic-version is available through the Maven Central Repository. Just add 26 | the following dependency to your pom: 27 | 28 | ```xml 29 | 30 | de.skuzzle 31 | semantic-version 32 | 2.1.0 33 | 34 | ``` 35 | 36 | ## Java 9 37 | 38 | Releases `>=2.0.0` are bundled as a JPMS module. If you are using it in your Java 9 project, 39 | add the following line to your `module-info.java`: 40 | 41 | ``` 42 | module com.your.module { 43 | // ... 44 | requires de.skuzzle.semantic; 45 | } 46 | ``` 47 | 48 | ## Usage 49 | 50 | ### Creation and parsing 51 | ```java 52 | // Version with pre-release and build meta data field 53 | Version v1 = Version.parseVersion("1.0.2-rc1.2+build-20142402"); 54 | Version v2 = Version.create(1, 0 , 2, "rc1.2", "build-20142402"); 55 | 56 | // Simple version 57 | Version v3 = Version.parseVersion("1.0.2"); 58 | Version v4 = Version.create(1, 0, 2); 59 | 60 | // Version with no pre-release field but with build meta data field 61 | Version v5 = Version.parseVersion("1.0.2+build-20142402"); 62 | Version v6 = Version.create(1, 0, 2, "", "build-20142402"); 63 | 64 | ``` 65 | 66 | ### Comparing 67 | Versions can be compared as they implement `Comparable`: 68 | 69 | ```java 70 | if (v1.compareTo(v2) < 0) { ... } 71 | if (v1.isGreaterThan(v2)) { ... } 72 | if (v1.isLowerThan(v2)) { ... } 73 | ``` 74 | In rare cases it might be useful to compare versions with including the build meta data 75 | field. If you need to do so, you can use 76 | 77 | ```java 78 | v1.compareToWithBuildMetaData(v2) 79 | v1.equalsWithBuildMetaData(v2) 80 | ``` 81 | 82 | There also exist static methods and comparators for comparing two versions. 83 | 84 | ### Deriving 85 | You can derive new versions from existing ones by modifying a single field: 86 | 87 | ```java 88 | Version v1 = Version.create(1, 0, 0) 89 | .withMinor(2) 90 | .withPatch(3) 91 | .withPreRelease("alpha-1") 92 | .withBuildMetaData("build-20161022"); 93 | ``` 94 | 95 | ### Incrementing 96 | Versions can also be incremented using any of the `next...` methods: 97 | 98 | ``` 99 | // Gives 2.0.0 100 | Version.create(1, 2, 3).nextMajor(); 101 | 102 | // Gives 1.3.0 103 | Version.create(1, 2, 3).nextMinor(); 104 | 105 | // Gives 1.2.4 106 | Version.create(1, 2, 3).nextPatch(); 107 | ``` 108 | 109 | All `next...` methods will drop the pre-release and build meta data fields but provide an 110 | overload to set a new pre-release: 111 | 112 | ``` 113 | // Gives 2.0.0-SNAPSHOT 114 | Version.create(1, 2, 3).nextMajor("SNAPSHOT"); 115 | ``` 116 | 117 | The identifier parts can be incremented as well: 118 | 119 | ``` 120 | // Gives 1.2.3-1 121 | Version.create(1, 2, 3).nextPreRelease(); 122 | 123 | // Gives 1.2.3+1 124 | Version.create(1, 2, 3).nextBuildMetaData(); 125 | ``` 126 | 127 | Incrementing the identifier behaves as follows: 128 | * In case the identifier is currently empty, it becomes `1` in the result. 129 | * If the identifier's last part is numeric, that last part will be incremented in the result. 130 | * If the last part is not numeric, the identifier is interpreted as `identifier.0` which becomes `identifier.1` after increment. 131 | 132 | Version | After increment 133 | --------| --------------- 134 | `1.2.3`| `1.2.3-1` 135 | `1.2.3+build.meta.data` | `1.2.3-1` 136 | `1.2.3-foo` | `1.2.3-foo.1` 137 | `1.2.3-foo.1` | `1.2.3-foo.2` 138 | 139 | The special method `toStable` which has been introduced in version 2.1.0 will give give the next _stable_ version. 140 | That is, it simply drops the pre-release and build meta data identifiers and leaves all other parts unmodified. 141 | 142 | ### Serialization 143 | Versions can be written to/read from streams by Java's `ObjectOutputStream` and 144 | `ObjectInputStream` classes out of the box: 145 | 146 | ```java 147 | new ObjectOutputStream(yourOutStream).writeObject(Version.parseVersion("1.2.3")); 148 | Version version = (Version) new ObjectInputStream(yourInStream).readObject(); 149 | ``` 150 | 151 | Serializing Versions from and to json is also possible but requires third party libraries 152 | like `jackson` or `gson`. Support for those is not built in (in order to not ship extra 153 | dependencies) but examples can be found within the unit tests 154 | [here (jackson)](https://github.com/skuzzle/semantic-version/blob/master/src/test/java/de/skuzzle/semantic/CustomJacksonSerialization.java) 155 | and [here (gson)](https://github.com/skuzzle/semantic-version/blob/master/src/test/java/de/skuzzle/semantic/CustomGsonSerialization.java). Both examples will serialize the Version as its String representation as 156 | opposed to destructing it into its single fields. 157 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | * #5: System locale might lead to illegal identifiers during lower/upper casing (Thx [@portlek](https://github.com/portlek)) -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | 6 | de.skuzzle 7 | skuzzle-parent 8 | 3.0.1 9 | 10 | 11 | semantic-version 12 | 2.1.1 13 | jar 14 | 15 | semantic-version 16 | Single-class semantic version implementation for java 17 | 18 | 19 | 20 | The MIT License (MIT) 21 | http://opensource.org/licenses/MIT 22 | repo 23 | 24 | 25 | 26 | 27 | semantic-version 28 | semantic-version 29 | 30 | 5.7.2 31 | 2.12.6.1 32 | 2.8.9 33 | 0.9.0 34 | 3.1.0 35 | 36 | false 37 | false 38 | 39 | true 40 | 41 | 42 | 43 | scm:git:https://github.com/skuzzle/${github.name}.git 44 | HEAD 45 | 46 | 47 | 48 | 49 | 50 | org.junit 51 | junit-bom 52 | ${junit.version} 53 | pom 54 | import 55 | 56 | 57 | 58 | 59 | 60 | 61 | com.google.code.gson 62 | gson 63 | ${gson.version} 64 | test 65 | 66 | 67 | com.fasterxml.jackson.core 68 | jackson-databind 69 | ${jackson.version} 70 | test 71 | 72 | 73 | com.fasterxml.jackson.core 74 | jackson-core 75 | ${jackson.version} 76 | test 77 | 78 | 79 | com.github.zafarkhaja 80 | java-semver 81 | ${java-semver.version} 82 | test 83 | 84 | 85 | com.vdurmont 86 | semver4j 87 | ${semver4j.version} 88 | test 89 | 90 | 91 | org.junit.jupiter 92 | junit-jupiter 93 | test 94 | 95 | 96 | org.junit.jupiter 97 | junit-jupiter-api 98 | test 99 | 100 | 101 | org.junit-pioneer 102 | junit-pioneer 103 | 1.4.2 104 | test 105 | 106 | 107 | 108 | 109 | 110 | 111 | org.apache.maven.plugins 112 | maven-javadoc-plugin 113 | 114 | 8 115 | 116 | 117 | 118 | com.amashchenko.maven.plugin 119 | gitflow-maven-plugin 120 | 121 | deploy 122 | true 123 | true 124 | true 125 | 126 | master 127 | dev 128 | 129 | 130 | 131 | 132 | com.ragedunicorn.tools.maven 133 | github-release-maven-plugin 134 | 1.0.2 135 | 136 | skuzzle 137 | ${github.name} 138 | ${github.release-token} 139 | v${project.version} 140 | Semantic Version ${project.version} 141 | master 142 | RELEASE_NOTES.md 143 | 144 | 145 | 146 | org.apache.maven.plugins 147 | maven-compiler-plugin 148 | 149 | 150 | default-compile 151 | 152 | 9 153 | 154 | 155 | 156 | default-testCompile 157 | 158 | testCompile 159 | 160 | 161 | 9 162 | 163 | 164 | 165 | base-compile 166 | 167 | compile 168 | 169 | 170 | 171 | module-info.java 172 | 173 | 174 | 175 | 176 | 177 | 178 | 9 179 | 180 | 6 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /src/it/java/de/skuzzle/semantic/IsPreReleasePerformanceIT.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.semantic; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | public class IsPreReleasePerformanceIT extends VersionPerformanceTestBase { 6 | 7 | private static final String INVALID_PRE_RELEASE = "very.long-prelease.id.1234.01"; 8 | 9 | @Test 10 | public void testIsNoPreReleaseWithRegex() throws Exception { 11 | performTest("Is no pre-release with regex", RUNS, new Runnable() { 12 | 13 | @Override 14 | public void run() { 15 | VersionRegEx.isValidPreRelease(INVALID_PRE_RELEASE); 16 | } 17 | }); 18 | } 19 | 20 | @Test 21 | public void testIsNoPreRelease() throws Exception { 22 | performTest("Is no pre-release without regex", RUNS, new Runnable() { 23 | 24 | @Override 25 | public void run() { 26 | Version.isValidPreRelease(INVALID_PRE_RELEASE); 27 | } 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/it/java/de/skuzzle/semantic/ParsingPerformanceIT.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.semantic; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import com.vdurmont.semver4j.Semver; 6 | 7 | public class ParsingPerformanceIT extends VersionPerformanceTestBase { 8 | 9 | private static final String TEST_STRING = "10.153.132-123a.sdfasd.asd.asdhd.124545f.very.long-prelease.012aid.1234+with.build.md.000112"; 10 | 11 | @Test 12 | public void testNoRegex() throws Exception { 13 | performTest("Without regex", RUNS, () -> Version.parseVersion(TEST_STRING)); 14 | } 15 | 16 | @Test 17 | public void testWithRegex() throws Exception { 18 | performTest("With regex", RUNS, () -> VersionRegEx.parseVersion(TEST_STRING)); 19 | } 20 | 21 | @Test 22 | public void testJSemver() throws Exception { 23 | performTest("jsemver", RUNS, () -> com.github.zafarkhaja.semver.Version.valueOf(TEST_STRING)); 24 | } 25 | 26 | @Test 27 | public void testsemver4j() throws Exception { 28 | performTest("semver4j", RUNS, () -> new Semver(TEST_STRING)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/it/java/de/skuzzle/semantic/SortingPerformanceIT.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.semantic; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.Collections; 6 | import java.util.List; 7 | import java.util.Random; 8 | 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import com.vdurmont.semver4j.Semver; 13 | 14 | public class SortingPerformanceIT extends VersionPerformanceTestBase { 15 | 16 | private static final Random SHUFFLE_RANDOM = new Random(0); 17 | 18 | private List sortMe; 19 | 20 | @BeforeEach 21 | public void setup() { 22 | final List mutable = new ArrayList<>( 23 | List.of( 24 | "1.0.0-alpha.beta", 25 | "1.0.0-alpha", 26 | "1.0.0-alpha.1", 27 | "1.0.0-beta.11", 28 | "1.0.0-beta", 29 | "1.0.0-rc.1", 30 | "1.0.0", 31 | "1.0.0-beta.2", 32 | "2.1.1", 33 | "2.1.0", 34 | "1.0.0-alpha.beta", 35 | "2.0.0", 36 | "1.0.0-alpha", 37 | "1.0.0-rc.1", 38 | "1.0.0-alpha.1", 39 | "1.0.0-beta", 40 | "1.0.0-beta.11", 41 | "1.0.0", 42 | "1.0.0-beta.2", 43 | "2.1.0", 44 | "2.0.0", 45 | "2.1.1")); 46 | Collections.shuffle(mutable, SHUFFLE_RANDOM); 47 | sortMe = Collections.unmodifiableList(mutable); 48 | } 49 | 50 | @Test 51 | public void testOldVersion() throws Exception { 52 | performTest("Regex Impl", RUNS, 53 | () -> sortMe.stream() 54 | .map(VersionRegEx::parseVersion) 55 | .toArray(VersionRegEx[]::new), 56 | Arrays::sort); 57 | } 58 | 59 | @Test 60 | public void testCurrentVersion() throws Exception { 61 | performTest("Current Impl", RUNS, 62 | () -> sortMe.stream() 63 | .map(Version::parseVersion) 64 | .toArray(Version[]::new), 65 | Arrays::sort); 66 | } 67 | 68 | @Test 69 | public void testJSemver() throws Exception { 70 | performTest("JSemver", RUNS, 71 | () -> sortMe.stream() 72 | .map(com.github.zafarkhaja.semver.Version::valueOf) 73 | .toArray(com.github.zafarkhaja.semver.Version[]::new), 74 | Arrays::sort); 75 | } 76 | 77 | @Test 78 | public void testSemver4j() throws Exception { 79 | performTest("JSemver", RUNS, 80 | () -> sortMe.stream() 81 | .map(Semver::new) 82 | .toArray(Semver[]::new), 83 | Arrays::sort); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/it/java/de/skuzzle/semantic/VersionPerformanceTestBase.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.semantic; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.function.Consumer; 7 | import java.util.function.Supplier; 8 | 9 | class VersionPerformanceTestBase { 10 | 11 | private static final int WARM_UP = 11000; 12 | protected static final int RUNS = 100000; 13 | 14 | private void warmUp(Runnable subject) { 15 | for (int i = 0; i < WARM_UP; ++i) { 16 | subject.run(); 17 | } 18 | } 19 | 20 | private void warmUp(Supplier before, Consumer subject) { 21 | for (int i = 0; i < WARM_UP; ++i) { 22 | subject.accept(before.get()); 23 | } 24 | } 25 | 26 | protected void performTest(String description, int iterations, 27 | Supplier beforeEach, 28 | Consumer subject) { 29 | 30 | System.out.println("Test: " + description); 31 | warmUp(beforeEach, subject); 32 | long min = Long.MAX_VALUE; 33 | long max = 0; 34 | long sum = 0; 35 | final List times = new ArrayList<>(iterations); 36 | for (int i = 0; i < iterations; ++i) { 37 | final T t = beforeEach.get(); 38 | 39 | final long start = System.nanoTime(); 40 | subject.accept(t); 41 | final long time = System.nanoTime() - start; 42 | times.add(time); 43 | min = Math.min(min, time); 44 | max = Math.max(max, time); 45 | sum += time; 46 | } 47 | Collections.sort(times); 48 | final long avg = sum / iterations; 49 | final long median = times.get(times.size() / 2); 50 | System.out.println("Min " + min); 51 | System.out.println("Max " + max); 52 | System.out.println("Avg " + avg); 53 | System.out.println("Med " + median); 54 | System.out.println(); 55 | } 56 | 57 | protected void performTest(String description, int iterations, Runnable subject) { 58 | 59 | System.out.println("Test: " + description); 60 | warmUp(subject); 61 | long min = Long.MAX_VALUE; 62 | long max = 0; 63 | long sum = 0; 64 | final List times = new ArrayList<>(iterations); 65 | 66 | for (int i = 0; i < iterations; ++i) { 67 | final long start = System.nanoTime(); 68 | subject.run(); 69 | final long time = System.nanoTime() - start; 70 | times.add(time); 71 | 72 | min = Math.min(min, time); 73 | max = Math.max(max, time); 74 | sum += time; 75 | } 76 | Collections.sort(times); 77 | final long avg = sum / iterations; 78 | final long median = times.get(times.size() / 2); 79 | System.out.println("Min " + min); 80 | System.out.println("Max " + max); 81 | System.out.println("Avg " + avg); 82 | System.out.println("Med " + median); 83 | System.out.println(); 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /src/it/java/de/skuzzle/semantic/VersionRegEx.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 Simon Taddiken 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package de.skuzzle.semantic; 25 | 26 | import java.io.Serializable; 27 | import java.util.Comparator; 28 | import java.util.regex.Matcher; 29 | import java.util.regex.Pattern; 30 | 31 | /** 32 | * This class is an implementation of the full semantic version 2.0.0 33 | * specification. Instances can be obtained using the 34 | * static overloads of the create method or by {@link #parseVersion(String) 35 | * parsing} a String. This class implements {@link Comparable} to compare two versions by 36 | * following the specifications linked to above. The {@link #equals(Object)} method 37 | * conforms to the result of {@link #compareTo(VersionRegEx)}, {@link #hashCode()} is 38 | * implemented appropriately. Neither method considers the {@link #getBuildMetaData() 39 | * build meta data} field for comparison. 40 | * 41 | *

42 | * Instances of this class are fully immutable. 43 | *

44 | * 45 | *

46 | * Note that unless stated otherwise, none of the public methods of this class accept 47 | * null values. Most methods will throw an {@link IllegalArgumentException} 48 | * when encountering a null argument. However, to comply with the 49 | * {@link Comparable} contract, the comparison methods will throw a 50 | * {@link NullPointerException} instead. 51 | *

52 | * 53 | * @author Simon Taddiken 54 | */ 55 | public final class VersionRegEx implements Comparable, Serializable { 56 | 57 | /** Conforms to all Version implementations since 0.6.0 */ 58 | private static final long serialVersionUID = 6034927062401119911L; 59 | 60 | private static final String[] EMPTY_ARRAY = new String[0]; 61 | 62 | /** 63 | * Semantic Version Specification to which this class complies 64 | * 65 | * @since 0.2.0 66 | */ 67 | public static final VersionRegEx COMPLIANCE = VersionRegEx.create(2, 0, 0); 68 | 69 | /** 70 | * This exception indicates that a version- or a part of a version string could not be 71 | * parsed according to the semantic version specification. 72 | * 73 | * @author Simon Taddiken 74 | */ 75 | public static class VersionFormatException extends RuntimeException { 76 | 77 | private static final long serialVersionUID = 1L; 78 | 79 | /** 80 | * Creates a new VersionFormatException with the given message. 81 | * 82 | * @param message The exception message. 83 | */ 84 | public VersionFormatException(String message) { 85 | super(message); 86 | } 87 | } 88 | 89 | /** 90 | * Comparator for natural version ordering. See {@link #compare(VersionRegEx, VersionRegEx)} for 91 | * more information. 92 | * 93 | * @since 0.2.0 94 | */ 95 | public static final Comparator NATURAL_ORDER = new Comparator() { 96 | @Override 97 | public int compare(VersionRegEx o1, VersionRegEx o2) { 98 | return VersionRegEx.compare(o1, o2); 99 | } 100 | }; 101 | 102 | /** 103 | * Comparator for ordering versions with additionally considering the build meta data 104 | * field when comparing versions. 105 | * 106 | *

107 | * Note: this comparator imposes orderings that are inconsistent with equals. 108 | *

109 | * 110 | * @since 0.3.0 111 | */ 112 | public static final Comparator WITH_BUILD_META_DATA_ORDER = new Comparator() { 113 | 114 | @Override 115 | public int compare(VersionRegEx o1, VersionRegEx o2) { 116 | return compareWithBuildMetaData(o1, o2); 117 | } 118 | }; 119 | 120 | private static final Pattern PRE_RELEASE = Pattern.compile("" + 121 | "(?:(?:[0-9]+[a-zA-Z-][\\w-]*)|(?:[a-zA-Z][\\w-]*)|(?:[1-9]\\d*)|0)" + 122 | "(?:\\.(?:(?:[0-9]+[a-zA-Z-][\\w-]*)|(?:[a-zA-Z][\\w-]*)|(?:[1-9]\\d*)|0))*"); 123 | 124 | private static final Pattern BUILD_MD = Pattern.compile("[\\w-]+(\\.[\\w-]+)*"); 125 | private static final Pattern VERSION_PATTERN = Pattern.compile("" 126 | + "(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)" 127 | + "(?:-((?:(?:[0-9]+[a-zA-Z-][\\w-]*)|(?:[a-zA-Z][\\w-]*)|(?:[1-9]\\d*)|0)" 128 | + "(?:\\.(?:(?:[0-9]+[a-zA-Z-][\\w-]*)|(?:[a-zA-Z][\\w-]*)|(?:[1-9]\\d*)|0))*))?" 129 | + "(?:\\+([\\w-]+(\\.[\\w-]+)*))?"); 130 | 131 | // Match result group indices 132 | private static final int MAJOR_GROUP = 1; 133 | private static final int MINOR_GROUP = 2; 134 | private static final int PATCH_GROUP = 3; 135 | private static final int PRE_RELEASE_GROUP = 4; 136 | private static final int BUILD_MD_GROUP = 5; 137 | 138 | private static final int TO_STRING_ESTIMATE = 12; 139 | private static final int HASH_PRIME = 31; 140 | 141 | private final int major; 142 | private final int minor; 143 | private final int patch; 144 | private final String preRelease; 145 | private final String buildMetaData; 146 | 147 | // store hash code once it has been calculated 148 | private volatile int hash; 149 | 150 | private VersionRegEx(int major, int minor, int patch, String preRelease, String buildMd) { 151 | this.major = major; 152 | this.minor = minor; 153 | this.patch = patch; 154 | this.preRelease = preRelease; 155 | this.buildMetaData = buildMd; 156 | } 157 | 158 | /** 159 | * Tries to parse the given String as a semantic version and returns whether the 160 | * String is properly formatted according to the semantic version specification. 161 | * 162 | *

163 | * Note: this method does not throw an exception upon null input, but 164 | * returns false instead. 165 | *

166 | * 167 | * @param version The String to check. 168 | * @return Whether the given String is formatted as a semantic version. 169 | * @since 0.5.0 170 | */ 171 | public static boolean isValidVersion(String version) { 172 | if (version == null || version.isEmpty()) { 173 | return false; 174 | } 175 | return VERSION_PATTERN.matcher(version).matches(); 176 | } 177 | 178 | /** 179 | * Returns whether the given String is a valid pre-release identifier. That is, this 180 | * method returns true if, and only if the {@code preRelease} parameter 181 | * is either the empty string or properly formatted as a pre-release identifier 182 | * according to the semantic version specification. 183 | * 184 | *

185 | * Note: this method does not throw an exception upon null input, but 186 | * returns false instead. 187 | *

188 | * 189 | * @param preRelease The String to check. 190 | * @return Whether the given String is a valid pre-release identifier. 191 | * @since 0.5.0 192 | */ 193 | public static boolean isValidPreRelease(String preRelease) { 194 | return preRelease != null && 195 | (preRelease.isEmpty() || PRE_RELEASE.matcher(preRelease).matches()); 196 | } 197 | 198 | /** 199 | * Returns whether the given String is a valid build meta data identifier. That is, 200 | * this method returns true if, and only if the {@code buildMetaData} 201 | * parameter is either the empty string or properly formatted as a build meta data 202 | * identifier according to the semantic version specification. 203 | * 204 | *

205 | * Note: this method does not throw an exception upon null input, but 206 | * returns false instead. 207 | *

208 | * 209 | * @param buildMetaData The String to check. 210 | * @return Whether the given String is a valid build meta data identifier. 211 | * @since 0.5.0 212 | */ 213 | public static boolean isValidBuildMetaData(String buildMetaData) { 214 | return buildMetaData != null && 215 | (buildMetaData.isEmpty() || BUILD_MD.matcher(buildMetaData).matches()); 216 | } 217 | 218 | /** 219 | * Returns the greater of the two given versions by comparing them using their natural 220 | * ordering. If both versions are equal, then the first argument is returned. 221 | * 222 | * @param v1 The first version. 223 | * @param v2 The second version. 224 | * @return The greater version. 225 | * @throws IllegalArgumentException If either argument is null. 226 | * @since 0.4.0 227 | */ 228 | public static VersionRegEx max(VersionRegEx v1, VersionRegEx v2) { 229 | require(v1 != null, "v1 is null"); 230 | require(v2 != null, "v2 is null"); 231 | return compare(v1, v2, false) < 0 232 | ? v2 233 | : v1; 234 | } 235 | 236 | /** 237 | * Returns the lower of the two given versions by comparing them using their natural 238 | * ordering. If both versions are equal, then the first argument is returned. 239 | * 240 | * @param v1 The first version. 241 | * @param v2 The second version. 242 | * @return The lower version. 243 | * @throws IllegalArgumentException If either argument is null. 244 | * @since 0.4.0 245 | */ 246 | public static VersionRegEx min(VersionRegEx v1, VersionRegEx v2) { 247 | require(v1 != null, "v1 is null"); 248 | require(v2 != null, "v2 is null"); 249 | return compare(v1, v2, false) <= 0 250 | ? v1 251 | : v2; 252 | } 253 | 254 | /** 255 | * Compares two versions, following the semantic version specification. Here 256 | * is a quote from semantic version 2.0.0 257 | * specification: 258 | * 259 | *

260 | * Precedence refers to how versions are compared to each other when ordered. 261 | * Precedence MUST be calculated by separating the version into major, minor, patch 262 | * and pre-release identifiers in that order (Build metadata does not figure into 263 | * precedence). Precedence is determined by the first difference when comparing each 264 | * of these identifiers from left to right as follows: Major, minor, and patch 265 | * versions are always compared numerically. Example: 1.0.0 < 2.0.0 < 2.1.0 < 266 | * 2.1.1. When major, minor, and patch are equal, a pre-release version has lower 267 | * precedence than a normal version. Example: 1.0.0-alpha < 1.0.0. Precedence for 268 | * two pre-release versions with the same major, minor, and patch version MUST be 269 | * determined by comparing each dot separated identifier from left to right until a 270 | * difference is found as follows: identifiers consisting of only digits are compared 271 | * numerically and identifiers with letters or hyphens are compared lexically in ASCII 272 | * sort order. Numeric identifiers always have lower precedence than non-numeric 273 | * identifiers. A larger set of pre-release fields has a higher precedence than a 274 | * smaller set, if all of the preceding identifiers are equal. Example: 1.0.0-alpha 275 | * < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 276 | * 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0. 277 | *

278 | * 279 | *

280 | * This method fulfills the general contract for Java's {@link Comparator Comparators} 281 | * and {@link Comparable Comparables}. 282 | *

283 | * 284 | * @param v1 The first version for comparison. 285 | * @param v2 The second version for comparison. 286 | * @return A value below 0 iff {@code v1 < v2}, a value above 0 iff 287 | * {@code v1 > v2 and 0 iff v1 = v2}. 288 | * @throws NullPointerException If either parameter is null. 289 | * @since 0.2.0 290 | */ 291 | public static int compare(VersionRegEx v1, VersionRegEx v2) { 292 | // throw NPE to comply with Comparable specification 293 | if (v1 == null) { 294 | throw new NullPointerException("v1 is null"); 295 | } else if (v2 == null) { 296 | throw new NullPointerException("v2 is null"); 297 | } 298 | return compare(v1, v2, false); 299 | } 300 | 301 | /** 302 | * Compares two Versions with additionally considering the build meta data field if 303 | * all other parts are equal. Note: This is not part of the semantic version 304 | * specification. 305 | * 306 | *

307 | * Comparison of the build meta data parts happens exactly as for pre release 308 | * identifiers. Considering of build meta data first kicks in if both versions are 309 | * equal when using their natural order. 310 | *

311 | * 312 | *

313 | * This method fulfills the general contract for Java's {@link Comparator Comparators} 314 | * and {@link Comparable Comparables}. 315 | *

316 | * 317 | * @param v1 The first version for comparison. 318 | * @param v2 The second version for comparison. 319 | * @return A value below 0 iff {@code v1 < v2}, a value above 0 iff 320 | * {@code v1 > v2
and 0 iff v1 = v2}. 321 | * @throws NullPointerException If either parameter is null. 322 | * @since 0.3.0 323 | */ 324 | public static int compareWithBuildMetaData(VersionRegEx v1, VersionRegEx v2) { 325 | // throw NPE to comply with Comparable specification 326 | if (v1 == null) { 327 | throw new NullPointerException("v1 is null"); 328 | } else if (v2 == null) { 329 | throw new NullPointerException("v2 is null"); 330 | } 331 | return compare(v1, v2, true); 332 | } 333 | 334 | private static int compare(VersionRegEx v1, VersionRegEx v2, boolean withBuildMetaData) { 335 | int result = 0; 336 | if (v1 != v2) { 337 | final int mc, mm, mp, pr, md; 338 | if ((mc = compareInt(v1.major, v2.major)) != 0) { 339 | result = mc; 340 | } else if ((mm = compareInt(v1.minor, v2.minor)) != 0) { 341 | result = mm; 342 | } else if ((mp = compareInt(v1.patch, v2.patch)) != 0) { 343 | result = mp; 344 | } else if ((pr = comparePreRelease(v1, v2)) != 0) { 345 | result = pr; 346 | } else if (withBuildMetaData && ((md = compareBuildMetaData(v1, v2)) != 0)) { 347 | result = md; 348 | } 349 | } 350 | return result; 351 | } 352 | 353 | private static int compareInt(int a, int b) { 354 | return a - b; 355 | } 356 | 357 | private static int comparePreRelease(VersionRegEx v1, VersionRegEx v2) { 358 | return compareLiterals(v1.getPreRelease(), v2.getPreRelease()); 359 | } 360 | 361 | private static int compareBuildMetaData(VersionRegEx v1, VersionRegEx v2) { 362 | return compareLiterals(v1.getBuildMetaData(), v2.getBuildMetaData()); 363 | } 364 | 365 | private static int compareLiterals(String v1Literal, String v2Literal) { 366 | int result = 0; 367 | if (!v1Literal.isEmpty() && !v2Literal.isEmpty()) { 368 | // compare pre release parts 369 | result = compareIdentifiers(v1Literal.split("\\."), v2Literal.split("\\.")); 370 | } else if (!v1Literal.isEmpty()) { 371 | // other is greater, because it is no pre release 372 | result = -1; 373 | } else if (!v2Literal.isEmpty()) { 374 | // this is greater because other is no pre release 375 | result = 1; 376 | } 377 | return result; 378 | } 379 | 380 | private static int compareIdentifiers(String[] parts1, String[] parts2) { 381 | final int min = Math.min(parts1.length, parts2.length); 382 | for (int i = 0; i < min; ++i) { 383 | final int r = compareIdentifierParts(parts1[i], parts2[i]); 384 | if (r != 0) { 385 | // versions differ in part i 386 | return r; 387 | } 388 | } 389 | 390 | // all id's are equal, so compare amount of id's 391 | return compareInt(parts1.length, parts2.length); 392 | } 393 | 394 | private static int compareIdentifierParts(String p1, String p2) { 395 | final int num1 = isNumeric(p1); 396 | final int num2 = isNumeric(p2); 397 | 398 | final int result; 399 | if (num1 < 0 && num2 < 0) { 400 | // both are not numerical -> compare lexically 401 | result = p1.compareTo(p2); 402 | } else if (num1 >= 0 && num2 >= 0) { 403 | // both are numerical 404 | result = compareInt(num1, num2); 405 | } else if (num1 >= 0) { 406 | // only part1 is numerical -> p2 is greater 407 | result = -1; 408 | } else { 409 | // only part2 is numerical -> p1 is greater 410 | result = 1; 411 | } 412 | return result; 413 | } 414 | 415 | /** 416 | * Determines whether s is a positive number. If it is, the number is returned, 417 | * otherwise the result is -1. 418 | * 419 | * @param s The String to check. 420 | * @return The positive number (incl. 0) if s a number, or -1 if it is not. 421 | */ 422 | private static int isNumeric(String s) { 423 | try { 424 | return Integer.parseInt(s); 425 | } catch (final NumberFormatException e) { 426 | return -1; 427 | } 428 | } 429 | 430 | /** 431 | * Creates a new Version from the provided components. Neither value of 432 | * {@code major, minor} or {@code patch} must be lower than 0 and at least one must be 433 | * greater than zero. {@code preRelease} or {@code buildMetaData} may be the empty 434 | * String. In this case, the created {@code Version} will have no pre release resp. 435 | * build meta data field. If those parameters are not empty, they must conform to the 436 | * semantic version specification. 437 | * 438 | * @param major The major version. 439 | * @param minor The minor version. 440 | * @param patch The patch version. 441 | * @param preRelease The pre release version or the empty string. 442 | * @param buildMetaData The build meta data field or the empty string. 443 | * @return The version instance. 444 | * @throws VersionFormatException If {@code preRelease} or {@code buildMetaData} does 445 | * not conform to the semantic version specification. 446 | */ 447 | public static final VersionRegEx create(int major, int minor, int patch, 448 | String preRelease, 449 | String buildMetaData) { 450 | checkParams(major, minor, patch); 451 | require(preRelease != null, "preRelease is null"); 452 | require(buildMetaData != null, "buildMetaData is null"); 453 | 454 | if (!isValidPreRelease(preRelease)) { 455 | throw new VersionFormatException( 456 | String.format("Illegal pre-release part: %s", preRelease)); 457 | } else if (!isValidBuildMetaData(buildMetaData)) { 458 | throw new VersionFormatException( 459 | String.format("Illegal build-meta-data part: %s", buildMetaData)); 460 | } 461 | return new VersionRegEx(major, minor, patch, preRelease, buildMetaData); 462 | } 463 | 464 | /** 465 | * Creates a new Version from the provided components. The version's build meta data 466 | * field will be empty. Neither value of {@code major, minor} or {@code patch} must be 467 | * lower than 0 and at least one must be greater than zero. {@code preRelease} may be 468 | * the empty String. In this case, the created version will have no pre release field. 469 | * If it is not empty, it must conform to the specifications of the semantic version. 470 | * 471 | * @param major The major version. 472 | * @param minor The minor version. 473 | * @param patch The patch version. 474 | * @param preRelease The pre release version or the empty string. 475 | * @return The version instance. 476 | * @throws VersionFormatException If {@code preRelease} is not empty and does not 477 | * conform to the semantic versioning specification 478 | */ 479 | public static final VersionRegEx create(int major, int minor, int patch, 480 | String preRelease) { 481 | checkParams(major, minor, patch); 482 | require(preRelease != null, "preRelease is null"); 483 | 484 | if (!preRelease.isEmpty() && !PRE_RELEASE.matcher(preRelease).matches()) { 485 | throw new VersionFormatException(preRelease); 486 | } 487 | return new VersionRegEx(major, minor, patch, preRelease, ""); 488 | } 489 | 490 | /** 491 | * Creates a new Version from the three provided components. The version's pre release 492 | * and build meta data fields will be empty. Neither value must be lower than 0 and at 493 | * least one must be greater than zero 494 | * 495 | * @param major The major version. 496 | * @param minor The minor version. 497 | * @param patch The patch version. 498 | * @return The version instance. 499 | */ 500 | public static final VersionRegEx create(int major, int minor, int patch) { 501 | checkParams(major, minor, patch); 502 | return new VersionRegEx(major, minor, patch, "", ""); 503 | } 504 | 505 | private static void checkParams(int major, int minor, int patch) { 506 | require(major >= 0, "major < 0"); 507 | require(minor >= 0, "minor < 0"); 508 | require(patch >= 0, "patch < 0"); 509 | require(major != 0 || minor != 0 || patch != 0, "all parts are 0"); 510 | } 511 | 512 | private static void require(boolean condition, String message) { 513 | if (!condition) { 514 | throw new IllegalArgumentException(message); 515 | } 516 | } 517 | 518 | /** 519 | * Tries to parse the provided String as a semantic version. If the string does not 520 | * conform to the semantic version specification, a {@link VersionFormatException} 521 | * will be thrown. 522 | * 523 | * @param versionString The String to parse. 524 | * @return The parsed version. 525 | * @throws VersionFormatException If the String is no valid version 526 | * @throws IllegalArgumentException If {@code versionString} is null. 527 | */ 528 | public static final VersionRegEx parseVersion(String versionString) { 529 | require(versionString != null, "versionString is null"); 530 | final Matcher m = VERSION_PATTERN.matcher(versionString); 531 | if (!m.matches()) { 532 | throw new VersionFormatException(versionString); 533 | } 534 | 535 | final int major = Integer.parseInt(m.group(MAJOR_GROUP)); 536 | final int minor = Integer.parseInt(m.group(MINOR_GROUP)); 537 | final int patch = Integer.parseInt(m.group(PATCH_GROUP)); 538 | 539 | checkParams(major, minor, patch); 540 | 541 | final String preRelease; 542 | if (m.group(PRE_RELEASE_GROUP) != null) { 543 | preRelease = m.group(PRE_RELEASE_GROUP); 544 | } else { 545 | preRelease = ""; 546 | } 547 | 548 | final String buildMD; 549 | if (m.group(BUILD_MD_GROUP) != null) { 550 | buildMD = m.group(BUILD_MD_GROUP); 551 | } else { 552 | buildMD = ""; 553 | } 554 | 555 | return new VersionRegEx(major, minor, patch, preRelease, buildMD); 556 | } 557 | 558 | /** 559 | * Tries to parse the provided String as a semantic version. If 560 | * {@code allowPreRelease} is false, the String must have neither a 561 | * pre-release nor a build meta data part. Thus the given String must have the format 562 | * {@code X.Y.Z} where at least one part must be greater than zero. 563 | * 564 | *

565 | * If {@code allowPreRelease} is true, the String is parsed according to 566 | * the normal semantic version specification. 567 | *

568 | * 569 | * @param versionString The String to parse. 570 | * @param allowPreRelease Whether pre-release and build meta data field are allowed. 571 | * @return The parsed version. 572 | * @throws VersionFormatException If the String is no valid version 573 | * @since 0.4.0 574 | */ 575 | public static VersionRegEx parseVersion(String versionString, boolean allowPreRelease) { 576 | final VersionRegEx version = parseVersion(versionString); 577 | if (!allowPreRelease && (version.isPreRelease() || version.hasBuildMetaData())) { 578 | throw new VersionFormatException(String.format( 579 | "Version is expected to have no pre-release or build meta data part")); 580 | } 581 | return version; 582 | } 583 | 584 | /** 585 | * Returns the lower of this version and the given version according to its natural 586 | * ordering. If versions are equal, {@code this} is returned. 587 | * 588 | * @param other The version to compare with. 589 | * @return The lower version. 590 | * @throws IllegalArgumentException If {@code other} is null. 591 | * @since 0.5.0 592 | * @see #min(VersionRegEx, VersionRegEx) 593 | */ 594 | public VersionRegEx min(VersionRegEx other) { 595 | return min(this, other); 596 | } 597 | 598 | /** 599 | * Returns the greater of this version and the given version according to its natural 600 | * ordering. If versions are equal, {@code this} is returned. 601 | * 602 | * @param other The version to compare with. 603 | * @return The greater version. 604 | * @throws IllegalArgumentException If {@code other} is null. 605 | * @since 0.5.0 606 | * @see #max(VersionRegEx, VersionRegEx) 607 | */ 608 | public VersionRegEx max(VersionRegEx other) { 609 | return max(this, other); 610 | } 611 | 612 | /** 613 | * Gets this version's major number. 614 | * 615 | * @return The major version. 616 | */ 617 | public int getMajor() { 618 | return this.major; 619 | } 620 | 621 | /** 622 | * Gets this version's minor number. 623 | * 624 | * @return The minor version. 625 | */ 626 | public int getMinor() { 627 | return this.minor; 628 | } 629 | 630 | /** 631 | * Gets this version's path number. 632 | * 633 | * @return The patch number. 634 | */ 635 | public int getPatch() { 636 | return this.patch; 637 | } 638 | 639 | /** 640 | * Gets the pre release parts of this version as array by splitting the pre result 641 | * version string at the dots. 642 | * 643 | * @return Pre release version as array. Array is empty if this version has no pre 644 | * release part. 645 | */ 646 | public String[] getPreReleaseParts() { 647 | return this.preRelease.isEmpty() ? EMPTY_ARRAY : this.preRelease.split("\\."); 648 | } 649 | 650 | /** 651 | * Gets the pre release identifier of this version. If this version has no such 652 | * identifier, an empty string is returned. 653 | * 654 | * @return This version's pre release identifier or an empty String if this version 655 | * has no such identifier. 656 | */ 657 | public String getPreRelease() { 658 | return this.preRelease; 659 | } 660 | 661 | /** 662 | * Gets this version's build meta data. If this version has no build meta data, the 663 | * returned string is empty. 664 | * 665 | * @return The build meta data or an empty String if this version has no build meta 666 | * data. 667 | */ 668 | public String getBuildMetaData() { 669 | return this.buildMetaData; 670 | } 671 | 672 | /** 673 | * Gets this version's build meta data as array by splitting the meta data at dots. If 674 | * this version has no build meta data, the result is an empty array. 675 | * 676 | * @return The build meta data as array. 677 | */ 678 | public String[] getBuildMetaDataParts() { 679 | return this.buildMetaData.isEmpty() ? EMPTY_ARRAY 680 | : this.buildMetaData.split("\\."); 681 | } 682 | 683 | /** 684 | * Determines whether this version is still under initial development. 685 | * 686 | * @return true iff this version's major part is zero. 687 | */ 688 | public boolean isInitialDevelopment() { 689 | return this.major == 0; 690 | } 691 | 692 | /** 693 | * Determines whether this is a pre release version. 694 | * 695 | * @return true iff {@link #getPreRelease()} is not empty. 696 | */ 697 | public boolean isPreRelease() { 698 | return !this.preRelease.isEmpty(); 699 | } 700 | 701 | /** 702 | * Determines whether this version has a build meta data field. 703 | * 704 | * @return true iff {@link #getBuildMetaData()} is not empty. 705 | */ 706 | public boolean hasBuildMetaData() { 707 | return !this.buildMetaData.isEmpty(); 708 | } 709 | 710 | /** 711 | * Creates a String representation of this version by joining its parts together as by 712 | * the semantic version specification. 713 | * 714 | * @return The version as a String. 715 | */ 716 | @Override 717 | public String toString() { 718 | final StringBuilder b = new StringBuilder(this.preRelease.length() 719 | + this.buildMetaData.length() + TO_STRING_ESTIMATE); 720 | b.append(this.major).append(".").append(this.minor).append(".") 721 | .append(this.patch); 722 | if (!this.preRelease.isEmpty()) { 723 | b.append("-").append(this.preRelease); 724 | } 725 | if (!this.buildMetaData.isEmpty()) { 726 | b.append("+").append(this.buildMetaData); 727 | } 728 | return b.toString(); 729 | } 730 | 731 | /** 732 | * The hash code for a version instance is computed from the fields {@link #getMajor() 733 | * major}, {@link #getMinor() minor}, {@link #getPatch() patch} and 734 | * {@link #getPreRelease() pre-release}. 735 | * 736 | * @return A hash code for this object. 737 | */ 738 | @Override 739 | public int hashCode() { 740 | int h = this.hash; 741 | if (h == 0) { 742 | h = HASH_PRIME + this.major; 743 | h = HASH_PRIME * h + this.minor; 744 | h = HASH_PRIME * h + this.patch; 745 | h = HASH_PRIME * h + this.preRelease.hashCode(); 746 | this.hash = h; 747 | } 748 | return this.hash; 749 | } 750 | 751 | /** 752 | * Determines whether this version is equal to the passed object. This is the case if 753 | * the passed object is an instance of Version and this version 754 | * {@link #compareTo(VersionRegEx) compared} to the provided one yields 0. Thus, this 755 | * method ignores the {@link #getBuildMetaData()} field. 756 | * 757 | * @param obj the object to compare with. 758 | * @return true iff {@code obj} is an instance of {@code Version} and 759 | * {@code this.compareTo((Version) obj) == 0} 760 | * @see #compareTo(VersionRegEx) 761 | */ 762 | @Override 763 | public boolean equals(Object obj) { 764 | return testEquality(obj, false); 765 | } 766 | 767 | /** 768 | * Determines whether this version is equal to the passed object (as determined by 769 | * {@link #equals(Object)} and additionally considers the build meta data part of both 770 | * versions for equality. 771 | * 772 | * @param obj The object to compare with. 773 | * @return true iff {@code this.equals(obj)} and 774 | * {@code this.getBuildMetaData().equals(((Version) obj).getBuildMetaData())} 775 | * @since 0.4.0 776 | */ 777 | public boolean equalsWithBuildMetaData(Object obj) { 778 | return testEquality(obj, true); 779 | } 780 | 781 | private boolean testEquality(Object obj, boolean includeBuildMd) { 782 | return obj == this || obj instanceof VersionRegEx 783 | && compare(this, (VersionRegEx) obj, includeBuildMd) == 0; 784 | } 785 | 786 | /** 787 | * Compares this version to the provided one, following the semantic 788 | * versioning specification. See {@link #compare(VersionRegEx, VersionRegEx)} for more 789 | * information. 790 | * 791 | * @param other The version to compare to. 792 | * @return A value lower than 0 if this < other, a value greater than 0 if this 793 | * > other and 0 if this == other. The absolute value of the result has no 794 | * semantical interpretation. 795 | */ 796 | @Override 797 | public int compareTo(VersionRegEx other) { 798 | return compare(this, other); 799 | } 800 | 801 | /** 802 | * Compares this version to the provided one. Unlike the {@link #compareTo(VersionRegEx)} 803 | * method, this one additionally considers the build meta data field of both versions, 804 | * if all other parts are equal. Note: This is not part of the semantic 805 | * version specification. 806 | * 807 | *

808 | * Comparison of the build meta data parts happens exactly as for pre release 809 | * identifiers. Considering of build meta data first kicks in if both versions are 810 | * equal when using their natural order. 811 | *

812 | * 813 | * @param other The version to compare to. 814 | * @return A value lower than 0 if this < other, a value greater than 0 if this 815 | * > other and 0 if this == other. The absolute value of the result has no 816 | * semantical interpretation. 817 | * @since 0.3.0 818 | */ 819 | public int compareToWithBuildMetaData(VersionRegEx other) { 820 | return compareWithBuildMetaData(this, other); 821 | } 822 | } 823 | -------------------------------------------------------------------------------- /src/main/java/de/skuzzle/semantic/Version.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Simon Taddiken 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package de.skuzzle.semantic; 25 | 26 | import java.io.ObjectStreamException; 27 | import java.io.Serializable; 28 | import java.util.ArrayList; 29 | import java.util.Arrays; 30 | import java.util.Comparator; 31 | import java.util.List; 32 | import java.util.Locale; 33 | 34 | /** 35 | * This class is an implementation of the full semantic version 2.0.0 36 | * specification. Instances can be obtained using the 37 | * static overloads of the create method or by {@link #parseVersion(String) 38 | * parsing} a String. This class implements {@link Comparable} to compare two versions by 39 | * following the specifications linked to above. The {@link #equals(Object)} method 40 | * conforms to the result of {@link #compareTo(Version)}, {@link #hashCode()} is 41 | * implemented appropriately. Neither method considers the {@link #getBuildMetaData() 42 | * build meta data} field for comparison. 43 | * 44 | *

45 | * Instances of this class are immutable and thus thread safe. This also means that all 46 | * methods taking an array or other kind of modifiable objects as input, will first make a 47 | * copy before using it as internal state. 48 | * 49 | *

50 | * Note that unless stated otherwise, none of the public methods of this class accept 51 | * null values. Most methods will throw an {@link IllegalArgumentException} 52 | * when encountering a null argument. However, to comply with the 53 | * {@link Comparable} contract, the comparison methods will throw a 54 | * {@link NullPointerException} instead. 55 | * 56 | * @author Simon Taddiken 57 | */ 58 | public final class Version implements Comparable, Serializable { 59 | 60 | /** Conforms to all Version implementations since 0.6.0 */ 61 | private static final long serialVersionUID = 6034927062401119911L; 62 | 63 | private static final String[] EMPTY_ARRAY = new String[0]; 64 | 65 | /** 66 | * The minimum value '0.0.0' for valid versions where all parts are 0 or empty. 67 | * 68 | * @since 2.1.0 69 | */ 70 | public static final Version ZERO = Version.create(0, 0, 0); 71 | 72 | /** 73 | * Semantic Version Specification to which this class complies. 74 | * 75 | * @since 0.2.0 76 | */ 77 | public static final Version COMPLIANCE = Version.create(2, 0, 0); 78 | 79 | /** 80 | * This exception indicates that a version- or a part of a version string could not be 81 | * parsed according to the semantic version specification. 82 | * 83 | * @author Simon Taddiken 84 | */ 85 | public static class VersionFormatException extends RuntimeException { 86 | 87 | private static final long serialVersionUID = 1L; 88 | 89 | /** 90 | * Creates a new VersionFormatException with the given message. 91 | * 92 | * @param message The exception message. 93 | */ 94 | private VersionFormatException(String message) { 95 | super(message); 96 | } 97 | } 98 | 99 | /** 100 | * Comparator for natural version ordering. See {@link #compare(Version, Version)} for 101 | * more information. 102 | *

103 | * Instead of using this field, consider using a method reference like in 104 | * Version::compare. 105 | * 106 | * @since 0.2.0 107 | */ 108 | public static final Comparator NATURAL_ORDER = new Comparator() { 109 | 110 | @Override 111 | public int compare(Version o1, Version o2) { 112 | return o1.compareTo(o2); 113 | } 114 | }; 115 | 116 | /** 117 | * Comparator for ordering versions with additionally considering the build meta data 118 | * field when comparing versions. 119 | *

120 | * Instead of using this field, consider using a method reference like in 121 | * Version::compareWithBuildMetaData. 122 | *

123 | * Note: this comparator imposes orderings that are inconsistent with equals. 124 | * 125 | * @since 0.3.0 126 | */ 127 | public static final Comparator WITH_BUILD_META_DATA_ORDER = new Comparator() { 128 | 129 | @Override 130 | public int compare(Version o1, Version o2) { 131 | return o1.compareToWithBuildMetaData(o2); 132 | } 133 | }; 134 | 135 | private static final int TO_STRING_ESTIMATE = 16; 136 | 137 | // state machine states for parsing a version string 138 | private static final int STATE_MAJOR_INIT = 0; 139 | private static final int STATE_MAJOR_LEADING_ZERO = 1; 140 | private static final int STATE_MAJOR_DEFAULT = 2; 141 | private static final int STATE_MINOR_INIT = 3; 142 | private static final int STATE_MINOR_LEADING_ZERO = 4; 143 | private static final int STATE_MINOR_DEFAULT = 5; 144 | private static final int STATE_PATCH_INIT = 6; 145 | private static final int STATE_PATCH_LEADING_ZERO = 7; 146 | private static final int STATE_PATCH_DEFAULT = 8; 147 | private static final int STATE_PRERELEASE_INIT = 9; 148 | private static final int STATE_BUILDMD_INIT = 10; 149 | 150 | private static final int STATE_PART_INIT = 0; 151 | private static final int STATE_PART_LEADING_ZERO = 1; 152 | private static final int STATE_PART_NUMERIC = 2; 153 | private static final int STATE_PART_DEFAULT = 3; 154 | 155 | private static final int DECIMAL = 10; 156 | 157 | private static final int EOS = -1; 158 | private static final int FAILURE = -2; 159 | 160 | private final int major; 161 | private final int minor; 162 | private final int patch; 163 | private final String[] preReleaseParts; 164 | private final String[] buildMetaDataParts; 165 | 166 | // Since 1.1.0 167 | // these fields are only necessary for deserializing previous versions 168 | // see #readResolve method 169 | @Deprecated 170 | private String preRelease; 171 | @Deprecated 172 | private String buildMetaData; 173 | 174 | // store hash code once it has been calculated 175 | private static final int NOT_YET_CALCULATED = 2; 176 | private static final int HASH_PRIME = 31; 177 | private volatile int hash = NOT_YET_CALCULATED; 178 | 179 | private Version(int major, int minor, int patch, String[] preRelease, 180 | String[] buildMd) { 181 | checkParams(major, minor, patch); 182 | this.major = major; 183 | this.minor = minor; 184 | this.patch = patch; 185 | this.preReleaseParts = preRelease; 186 | this.buildMetaDataParts = buildMd; 187 | } 188 | 189 | private static Version parse(String s, boolean verifyOnly) { 190 | /* 191 | * Since 1.1.0: 192 | * 193 | * This huge and ugly inline parsing replaces the prior regex because it is way 194 | * faster. Besides that it also does provide better error messages in case a 195 | * String could not be parsed correctly. Condition and mutation coverage is 196 | * extremely high to ensure correctness. 197 | */ 198 | 199 | // note: getting the char array once is faster than calling charAt multiple times 200 | final char[] stream = s.toCharArray(); 201 | int major = 0; 202 | int minor = 0; 203 | int patch = 0; 204 | int state = STATE_MAJOR_INIT; 205 | 206 | List preRelease = null; 207 | List buildMd = null; 208 | loop: for (int i = 0; i <= stream.length; ++i) { 209 | final int c = i < stream.length ? stream[i] : EOS; 210 | 211 | switch (state) { 212 | 213 | // Parse major part 214 | case STATE_MAJOR_INIT: 215 | if (c == '0') { 216 | state = STATE_MAJOR_LEADING_ZERO; 217 | } else if (c >= '1' && c <= '9') { 218 | major = major * DECIMAL + Character.digit(c, DECIMAL); 219 | state = STATE_MAJOR_DEFAULT; 220 | } else if (verifyOnly) { 221 | return null; 222 | } else { 223 | throw unexpectedChar(s, c); 224 | } 225 | break; 226 | case STATE_MAJOR_LEADING_ZERO: 227 | if (c == '.') { 228 | // single 0 is allowed 229 | state = STATE_MINOR_INIT; 230 | } else if (c >= '0' && c <= '9') { 231 | if (verifyOnly) { 232 | return null; 233 | } 234 | throw illegalLeadingChar(s, '0', "major"); 235 | } else if (verifyOnly) { 236 | return null; 237 | } else { 238 | throw unexpectedChar(s, c); 239 | } 240 | break; 241 | case STATE_MAJOR_DEFAULT: 242 | if (c >= '0' && c <= '9') { 243 | major = major * DECIMAL + Character.digit(c, DECIMAL); 244 | } else if (c == '.') { 245 | state = STATE_MINOR_INIT; 246 | } else if (verifyOnly) { 247 | return null; 248 | } else { 249 | throw unexpectedChar(s, c); 250 | } 251 | break; 252 | 253 | // parse minor part 254 | case STATE_MINOR_INIT: 255 | if (c == '0') { 256 | state = STATE_MINOR_LEADING_ZERO; 257 | } else if (c >= '1' && c <= '9') { 258 | minor = minor * DECIMAL + Character.digit(c, DECIMAL); 259 | state = STATE_MINOR_DEFAULT; 260 | } else if (verifyOnly) { 261 | return null; 262 | } else { 263 | throw unexpectedChar(s, c); 264 | } 265 | break; 266 | case STATE_MINOR_LEADING_ZERO: 267 | if (c == '.') { 268 | // single 0 is allowed 269 | state = STATE_PATCH_INIT; 270 | } else if (c >= '0' && c <= '9') { 271 | if (verifyOnly) { 272 | return null; 273 | } 274 | throw illegalLeadingChar(s, '0', "minor"); 275 | } else if (verifyOnly) { 276 | return null; 277 | } else { 278 | throw unexpectedChar(s, c); 279 | } 280 | break; 281 | case STATE_MINOR_DEFAULT: 282 | if (c >= '0' && c <= '9') { 283 | minor = minor * DECIMAL + Character.digit(c, DECIMAL); 284 | } else if (c == '.') { 285 | state = STATE_PATCH_INIT; 286 | } else if (verifyOnly) { 287 | return null; 288 | } else { 289 | throw unexpectedChar(s, c); 290 | } 291 | break; 292 | 293 | // parse patch part 294 | case STATE_PATCH_INIT: 295 | if (c == '0') { 296 | state = STATE_PATCH_LEADING_ZERO; 297 | } else if (c >= '1' && c <= '9') { 298 | patch = patch * DECIMAL + Character.digit(c, DECIMAL); 299 | state = STATE_PATCH_DEFAULT; 300 | } else if (verifyOnly) { 301 | return null; 302 | } else { 303 | throw unexpectedChar(s, c); 304 | } 305 | break; 306 | case STATE_PATCH_LEADING_ZERO: 307 | if (c == '-') { 308 | // single 0 is allowed 309 | state = STATE_PRERELEASE_INIT; 310 | } else if (c == '+') { 311 | state = STATE_BUILDMD_INIT; 312 | } else if (c == EOS) { 313 | break loop; 314 | } else if (c >= '0' && c <= '9') { 315 | if (verifyOnly) { 316 | return null; 317 | } 318 | throw illegalLeadingChar(s, '0', "patch"); 319 | } else if (verifyOnly) { 320 | return null; 321 | } else { 322 | throw unexpectedChar(s, c); 323 | } 324 | break; 325 | case STATE_PATCH_DEFAULT: 326 | if (c >= '0' && c <= '9') { 327 | patch = patch * DECIMAL + Character.digit(c, DECIMAL); 328 | } else if (c == '-') { 329 | state = STATE_PRERELEASE_INIT; 330 | } else if (c == '+') { 331 | state = STATE_BUILDMD_INIT; 332 | } else if (c != EOS) { 333 | // eos is allowed here 334 | if (verifyOnly) { 335 | return null; 336 | } 337 | throw unexpectedChar(s, c); 338 | } 339 | break; 340 | case STATE_PRERELEASE_INIT: 341 | 342 | preRelease = verifyOnly ? null : new ArrayList(); 343 | i = parseID(stream, s, i, verifyOnly, false, true, preRelease, 344 | "pre-release"); 345 | if (i == FAILURE) { 346 | // implies verifyOnly == true, otherwise exception would have been 347 | // thrown 348 | return null; 349 | } 350 | final int c1 = i < stream.length ? stream[i] : EOS; 351 | 352 | if (c1 == '+') { 353 | state = STATE_BUILDMD_INIT; 354 | } else { 355 | break loop; 356 | } 357 | break; 358 | 359 | case STATE_BUILDMD_INIT: 360 | buildMd = verifyOnly ? null : new ArrayList(); 361 | i = parseID(stream, s, i, verifyOnly, true, false, buildMd, 362 | "build-meta-data"); 363 | if (i == FAILURE) { 364 | // implies verifyOnly == true, otherwise exception would have been 365 | // thrown 366 | return null; 367 | } 368 | 369 | break loop; 370 | default: 371 | throw new IllegalStateException("Illegal state: " + state); 372 | } 373 | } 374 | final String[] prerelease = preRelease == null ? EMPTY_ARRAY 375 | : preRelease.toArray(new String[preRelease.size()]); 376 | final String[] buildmetadata = buildMd == null ? EMPTY_ARRAY 377 | : buildMd.toArray(new String[buildMd.size()]); 378 | return new Version(major, minor, patch, prerelease, buildmetadata); 379 | } 380 | 381 | private static int parseID(char[] stream, String full, int start, boolean verifyOnly, 382 | boolean allowLeading0, boolean preRelease, List parts, 383 | String partName) { 384 | 385 | assert verifyOnly || parts != null; 386 | 387 | final StringBuilder b = verifyOnly 388 | ? null 389 | : new StringBuilder(stream.length - start); 390 | 391 | int i = start; 392 | while (i <= stream.length) { 393 | 394 | i = parseIDPart(stream, full, i, verifyOnly, allowLeading0, preRelease, true, 395 | b, partName); 396 | if (i == FAILURE) { 397 | // implies verifyOnly == true, otherwise exception would have been thrown 398 | return FAILURE; 399 | } else if (!verifyOnly) { 400 | parts.add(b.toString()); 401 | } 402 | 403 | final int c = i < stream.length ? stream[i] : EOS; 404 | if (c == '.') { 405 | // keep looping 406 | ++i; 407 | } else { 408 | // identifier is done (hit EOS or '+') 409 | return i; 410 | } 411 | } 412 | throw new IllegalStateException(); 413 | } 414 | 415 | private static int parseIDPart(char[] stream, String full, int start, 416 | boolean verifyOnly, 417 | boolean allowLeading0, boolean preRelease, boolean allowDot, 418 | StringBuilder b, String partName) { 419 | 420 | if (b != null) { 421 | b.setLength(0); 422 | } 423 | 424 | int state = STATE_PART_INIT; 425 | for (int i = start; i <= stream.length; ++i) { 426 | final int c = i < stream.length ? stream[i] : EOS; 427 | 428 | switch (state) { 429 | case STATE_PART_INIT: 430 | if (c == '0' && !allowLeading0) { 431 | state = STATE_PART_LEADING_ZERO; 432 | if (b != null) { 433 | b.append('0'); 434 | } 435 | } else if (c == '-' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' 436 | || c >= '0' && c <= '9') { 437 | if (b != null) { 438 | b.appendCodePoint(c); 439 | } 440 | state = STATE_PART_DEFAULT; 441 | } else if (c == '.') { 442 | if (verifyOnly) { 443 | return FAILURE; 444 | } 445 | throw unexpectedChar(full, -1); 446 | } else { 447 | if (verifyOnly) { 448 | return FAILURE; 449 | } 450 | throw unexpectedChar(full, c); 451 | } 452 | break; 453 | case STATE_PART_LEADING_ZERO: 454 | // when in this state we consumed a single '0' 455 | if (c == '-' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z') { 456 | if (b != null) { 457 | b.appendCodePoint(c); 458 | } 459 | state = STATE_PART_DEFAULT; 460 | } else if (c >= '0' && c <= '9') { 461 | if (b != null) { 462 | b.appendCodePoint(c); 463 | } 464 | state = STATE_PART_NUMERIC; 465 | } else if (c == '.' && allowDot || c == EOS || c == '+' && preRelease) { 466 | // if we are parsing a pre release part it can be terminated by a 467 | // '+' in case a build meta data follows 468 | 469 | // here, this part consist of a single '0' 470 | return i; 471 | } else if (verifyOnly) { 472 | return FAILURE; 473 | } else { 474 | throw unexpectedChar(full, c); 475 | } 476 | break; 477 | case STATE_PART_NUMERIC: 478 | // when in this state, the part began with a '0' and we only consumed 479 | // numeric chars so far 480 | if (c == '-' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z') { 481 | if (b != null) { 482 | b.appendCodePoint(c); 483 | } 484 | state = STATE_PART_DEFAULT; 485 | } else if (c >= '0' && c <= '9') { 486 | if (b != null) { 487 | b.appendCodePoint(c); 488 | } 489 | } else if (c == '.' || c == EOS || c == '+' && preRelease) { 490 | // if we are parsing a pre release part it can be terminated by a 491 | // '+' in case a build meta data follows 492 | 493 | if (verifyOnly) { 494 | return FAILURE; 495 | } 496 | throw illegalLeadingChar(full, '0', partName); 497 | } else if (verifyOnly) { 498 | return FAILURE; 499 | } else { 500 | throw unexpectedChar(full, c); 501 | } 502 | break; 503 | case STATE_PART_DEFAULT: 504 | if (c == '-' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' 505 | || c >= '0' && c <= '9') { 506 | if (b != null) { 507 | b.appendCodePoint(c); 508 | } 509 | } else if (c == '.' && allowDot || c == EOS || c == '+' && preRelease) { 510 | // if we are parsing a pre release part it can be terminated by a 511 | // '+' in case a build meta data follows 512 | return i; 513 | } else if (verifyOnly) { 514 | return FAILURE; 515 | } else { 516 | throw unexpectedChar(full, c); 517 | } 518 | break; 519 | } 520 | } 521 | 522 | throw new IllegalStateException(); 523 | } 524 | 525 | private static VersionFormatException illegalLeadingChar(String v, int c, 526 | String part) { 527 | return new VersionFormatException( 528 | String.format("Illegal leading char '%c' in %s part of %s", c, part, v)); 529 | } 530 | 531 | private static VersionFormatException unexpectedChar(String v, int c) { 532 | if (c == EOS) { 533 | return new VersionFormatException(String.format( 534 | "Incomplete version part in %s", v)); 535 | } 536 | return new VersionFormatException( 537 | String.format("Unexpected char in %s: %c", v, c)); 538 | } 539 | 540 | /** 541 | * Creates a new Version from this one, replacing only the major part with the given 542 | * one. All other parts will remain the same as in this Version. 543 | * 544 | * @param newMajor The new major version. 545 | * @return A new Version. 546 | * @throws IllegalArgumentException If all fields in the resulting Version are 0. 547 | * @since 1.1.0 548 | */ 549 | public Version withMajor(int newMajor) { 550 | return new Version(newMajor, this.minor, this.patch, this.preReleaseParts, 551 | this.buildMetaDataParts); 552 | } 553 | 554 | /** 555 | * Creates a new Version from this one, replacing only the minor part with the given 556 | * one. All other parts will remain the same as in this Version. 557 | * 558 | * @param newMinor The new minor version. 559 | * @return A new Version. 560 | * @throws IllegalArgumentException If all fields in the resulting Version are 0. 561 | * @since 1.1.0 562 | */ 563 | public Version withMinor(int newMinor) { 564 | return new Version(this.major, newMinor, this.patch, this.preReleaseParts, 565 | this.buildMetaDataParts); 566 | } 567 | 568 | /** 569 | * Creates a new Version from this one, replacing only the patch part with the given 570 | * one. All other parts will remain the same as in this Version. 571 | * 572 | * @param newPatch The new patch version. 573 | * @return A new Version. 574 | * @throws IllegalArgumentException If all fields in the resulting Version are 0. 575 | * @since 1.1.0 576 | */ 577 | public Version withPatch(int newPatch) { 578 | return new Version(this.major, this.minor, newPatch, this.preReleaseParts, 579 | this.buildMetaDataParts); 580 | } 581 | 582 | /** 583 | * Creates a new Version from this one, replacing only the pre-release part with the 584 | * given String. All other parts will remain the same as in this Version. You can 585 | * remove the pre-release part by passing an empty String. 586 | * 587 | * @param newPreRelease The new pre-release identifier. 588 | * @return A new Version. 589 | * @throws VersionFormatException If the given String is not a valid pre-release 590 | * identifier. 591 | * @throws IllegalArgumentException If preRelease is null. 592 | * @since 1.1.0 593 | */ 594 | public Version withPreRelease(String newPreRelease) { 595 | require(newPreRelease != null, "newPreRelease is null"); 596 | final String[] newPreReleaseParts = parsePreRelease(newPreRelease); 597 | return new Version(this.major, this.minor, this.patch, newPreReleaseParts, 598 | this.buildMetaDataParts); 599 | } 600 | 601 | /** 602 | * Creates a new Version from this one, replacing only the pre-release part with the 603 | * given array. All other parts will remain the same as in this Version. You can 604 | * remove the pre-release part by passing an empty array. 605 | *

606 | * The passed array will be copied to not allow external modification to the new 607 | * Version's inner state. 608 | * 609 | *

610 | * A single part within the array is allowed to contain a dot ('.'). Such parts will 611 | * be treated as if the array contained those parts as single elements. 612 | * 613 | * 614 | *

 615 |      * v.withPreRelease(new String[] { "a.b" })
 616 |      * <=>
 617 |      * v.withPreRelease(new String[] { "a", "b" })
 618 |      * 
619 | * 620 | * @param newPreRelease the new pre release parts. 621 | * @return A new Version. 622 | * @throws VersionFormatException If the any element of the given array is not a valid 623 | * pre release identifier part. 624 | * @throws IllegalArgumentException If newPreRelease is null. 625 | * @since 1.2.0 626 | */ 627 | public Version withPreRelease(String[] newPreRelease) { 628 | require(newPreRelease != null, "newPreRelease is null"); 629 | final String joined = join(newPreRelease); 630 | final String[] newPreReleaseParts = parsePreRelease(joined); 631 | return new Version(this.major, this.minor, this.patch, newPreReleaseParts, 632 | this.buildMetaDataParts); 633 | } 634 | 635 | /** 636 | * Creates a new Version from this one, replacing only the build-meta-data part with 637 | * the given String. All other parts will remain the same as in this Version. You can 638 | * remove the build-meta-data part by passing an empty String. 639 | * 640 | * @param newBuildMetaData The new build meta data identifier. 641 | * @return A new Version. 642 | * @throws VersionFormatException If the given String is not a valid build-meta-data 643 | * identifier. 644 | * @throws IllegalArgumentException If newBuildMetaData is null. 645 | * @since 1.1.0 646 | */ 647 | public Version withBuildMetaData(String newBuildMetaData) { 648 | require(newBuildMetaData != null, "newBuildMetaData is null"); 649 | final String[] newBuildMdParts = parseBuildMd(newBuildMetaData); 650 | return new Version(this.major, this.minor, this.patch, this.preReleaseParts, 651 | newBuildMdParts); 652 | } 653 | 654 | /** 655 | * Creates a new Version from this one, replacing only the build-meta-data part with 656 | * the given array. All other parts will remain the same as in this Version. You can 657 | * remove the build-meta-data part by passing an empty array. 658 | *

659 | * The passed array will be copied to not allow external modification to the new 660 | * Version's inner state. 661 | * 662 | * A single part within the array is allowed to contain a dot ('.'). Such parts will 663 | * be treated as if the array contained those parts as single elements. 664 | * 665 | * 666 | *

 667 |      * v.withBuildMetaData(new String[] { "a.b" })
 668 |      * <=>
 669 |      * v.withBuildMetaData(new String[] { "a", "b" })
 670 |      * 
671 | * 672 | * @param newBuildMetaData the new build meta data parts. 673 | * @return A new Version. 674 | * @throws VersionFormatException If the any element of the given array is not a valid 675 | * build meta data identifier part. 676 | * @throws IllegalArgumentException If newBuildMetaData is null. 677 | * @since 1.2.0 678 | */ 679 | public Version withBuildMetaData(String[] newBuildMetaData) { 680 | require(newBuildMetaData != null, "newBuildMetaData is null"); 681 | final String joined = join(newBuildMetaData); 682 | final String[] newBuildMdParts = parseBuildMd(joined); 683 | return new Version(this.major, this.minor, this.patch, this.preReleaseParts, 684 | newBuildMdParts); 685 | } 686 | 687 | private String[] verifyAndCopyArray(String parts[], boolean allowLeading0) { 688 | final String[] result = new String[parts.length]; 689 | for (int i = 0; i < parts.length; ++i) { 690 | final String part = parts[i]; 691 | require(part != null, "version part is null"); 692 | if (part.isEmpty()) { 693 | throw new VersionFormatException( 694 | "Incomplete version part in " + join(parts)); 695 | } 696 | result[i] = part; 697 | 698 | // note: pass "pre-release" because this string will not be used when parsing 699 | // build-meta-data 700 | parseIDPart(part.toCharArray(), part, 0, false, allowLeading0, false, false, 701 | null, "pre-release"); 702 | } 703 | return result; 704 | } 705 | 706 | /** 707 | * Drops both the pre-release and the build meta data from this version. 708 | * 709 | * @return The nearest stable version. 710 | * @since 2.1.0 711 | */ 712 | public Version toStable() { 713 | return new Version(this.major, this.minor, this.patch, EMPTY_ARRAY, EMPTY_ARRAY); 714 | } 715 | 716 | /** 717 | * Given this Version, returns the next major Version. That is, the major part is 718 | * incremented by 1 and the remaining parts are set to 0. This also drops the 719 | * pre-release and build-meta-data. 720 | * 721 | * @return The incremented version. 722 | * @see #nextMajor(String) 723 | * @see #nextMajor(String[]) 724 | * @since 1.2.0 725 | */ 726 | public Version nextMajor() { 727 | return new Version(this.major + 1, 0, 0, EMPTY_ARRAY, EMPTY_ARRAY); 728 | } 729 | 730 | /** 731 | * Given this Version, returns the next major Version. That is, the major part is 732 | * incremented by 1 and the remaining parts are set to 0. The pre-release part will be 733 | * set to the given identifier and the build-meta-data is dropped. 734 | * 735 | * @param newPrelease The pre-release part for the resulting Version. 736 | * @return The incremented version. 737 | * @throws VersionFormatException If the given String is not a valid pre-release 738 | * identifier. 739 | * @throws IllegalArgumentException If newPreRelease is null. 740 | * @see #nextMajor() 741 | * @see #nextMajor(String[]) 742 | * @since 1.2.0 743 | */ 744 | public Version nextMajor(String newPrelease) { 745 | require(newPrelease != null, "newPreRelease is null"); 746 | final String[] preReleaseParts = parsePreRelease(newPrelease); 747 | return new Version(this.major + 1, 0, 0, preReleaseParts, EMPTY_ARRAY); 748 | } 749 | 750 | /** 751 | * Given this Version, returns the next major Version. That is, the major part is 752 | * incremented by 1 and the remaining parts are set to 0. The pre-release part will be 753 | * set to the given identifier and the build-meta-data is dropped. 754 | * 755 | * @param newPrelease The pre-release part for the resulting Version. 756 | * @return The incremented version. 757 | * @throws VersionFormatException If the any element of the given array is not a valid 758 | * pre release identifier part. 759 | * @throws IllegalArgumentException If newPreRelease is null. 760 | * @see #nextMajor() 761 | * @see #nextMajor(String) 762 | * @since 1.2.0 763 | */ 764 | public Version nextMajor(String[] newPrelease) { 765 | require(newPrelease != null, "newPreRelease is null"); 766 | final String[] newPreReleaseParts = verifyAndCopyArray(newPrelease, false); 767 | return new Version(this.major + 1, 0, 0, newPreReleaseParts, EMPTY_ARRAY); 768 | } 769 | 770 | /** 771 | * Given this version, returns the next minor version. That is, the major part remains 772 | * the same, the minor version is incremented and all other parts are reset/dropped. 773 | * 774 | * @return The incremented version. 775 | * @see #nextMinor(String) 776 | * @see #nextMinor(String[]) 777 | * @since 1.2.0 778 | */ 779 | public Version nextMinor() { 780 | return new Version(this.major, this.minor + 1, 0, EMPTY_ARRAY, EMPTY_ARRAY); 781 | } 782 | 783 | /** 784 | * Given this version, returns the next minor version. That is, the major part remains 785 | * the same and the minor version is incremented. The pre-release part will be set to 786 | * the given identifier and the build-meta-data is dropped. 787 | * 788 | * @param newPrelease The pre-release part for the resulting Version. 789 | * @return The incremented version. 790 | * @throws VersionFormatException If the given String is not a valid pre-release 791 | * identifier. 792 | * @throws IllegalArgumentException If newPreRelease is null. 793 | * @see #nextMinor() 794 | * @see #nextMinor(String[]) 795 | * @since 1.2.0 796 | */ 797 | public Version nextMinor(String newPrelease) { 798 | require(newPrelease != null, "newPreRelease is null"); 799 | final String[] preReleaseParts = parsePreRelease(newPrelease); 800 | return new Version(this.major, this.minor + 1, 0, preReleaseParts, EMPTY_ARRAY); 801 | } 802 | 803 | /** 804 | * Given this version, returns the next minor version. That is, the major part remains 805 | * the same and the minor version is incremented. The pre-release part will be set to 806 | * the given identifier and the build-meta-data is dropped. 807 | * 808 | * @param newPrelease The pre-release part for the resulting Version. 809 | * @return The incremented version. 810 | * @throws VersionFormatException If the any element of the given array is not a valid 811 | * pre release identifier part. 812 | * @throws IllegalArgumentException If newPreRelease is null. 813 | * @see #nextMinor() 814 | * @see #nextMinor(String) 815 | * @since 1.2.0 816 | */ 817 | public Version nextMinor(String[] newPrelease) { 818 | require(newPrelease != null, "newPreRelease is null"); 819 | final String[] newPreReleaseParts = verifyAndCopyArray(newPrelease, false); 820 | return new Version(this.major, this.minor + 1, 0, newPreReleaseParts, 821 | EMPTY_ARRAY); 822 | } 823 | 824 | /** 825 | * Given this version, returns the next patch version. That is, the major and minor 826 | * parts remain the same, the patch version is incremented and all other parts are 827 | * reset/dropped. 828 | * 829 | * @return The incremented version. 830 | * @see #nextPatch(String) 831 | * @see #nextPatch(String[]) 832 | * @since 1.2.0 833 | */ 834 | public Version nextPatch() { 835 | return new Version(this.major, this.minor, this.patch + 1, EMPTY_ARRAY, 836 | EMPTY_ARRAY); 837 | } 838 | 839 | /** 840 | * Given this version, returns the next patch version. That is, the major and minor 841 | * parts remain the same and the patch version is incremented. The pre-release part 842 | * will be set to the given identifier and the build-meta-data is dropped. 843 | * 844 | * @param newPrelease The pre-release part for the resulting Version. 845 | * @return The incremented version. 846 | * @throws VersionFormatException If the given String is not a valid pre-release 847 | * identifier. 848 | * @throws IllegalArgumentException If newPreRelease is null. 849 | * @see #nextPatch() 850 | * @see #nextPatch(String[]) 851 | * @since 1.2.0 852 | */ 853 | public Version nextPatch(String newPrelease) { 854 | require(newPrelease != null, "newPreRelease is null"); 855 | final String[] preReleaseParts = parsePreRelease(newPrelease); 856 | return new Version(this.major, this.minor, this.patch + 1, preReleaseParts, 857 | EMPTY_ARRAY); 858 | } 859 | 860 | /** 861 | * Given this version, returns the next patch version. That is, the major and minor 862 | * parts remain the same and the patch version is incremented. The pre-release part 863 | * will be set to the given identifier and the build-meta-data is dropped. 864 | * 865 | * @param newPrelease The pre-release part for the resulting Version. 866 | * @return The incremented version. 867 | * @throws VersionFormatException If the any element of the given array is not a valid 868 | * pre release identifier part. 869 | * @throws IllegalArgumentException If newPreRelease is null. 870 | * @see #nextPatch() 871 | * @see #nextPatch(String) 872 | * @since 1.2.0 873 | */ 874 | public Version nextPatch(String[] newPrelease) { 875 | require(newPrelease != null, "newPreRelease is null"); 876 | final String[] newPreReleaseParts = verifyAndCopyArray(newPrelease, false); 877 | return new Version(this.major, this.minor, this.patch + 1, newPreReleaseParts, 878 | EMPTY_ARRAY); 879 | } 880 | 881 | /** 882 | * Derives a new Version instance from this one by only incrementing the pre-release 883 | * identifier. The build-meta-data will be dropped, all other fields remain the same. 884 | * 885 | *

886 | * The incrementation of the pre-release identifier behaves as follows: 887 | * 888 | *

    889 | *
  • In case the identifier is currently empty, it becomes "1" in the result.
  • 890 | *
  • If the identifier's last part is numeric, that last part will be incremented in 891 | * the result.
  • 892 | *
  • If the last part is not numeric, the identifier is interpreted as 893 | * {@code identifier.0} which becomes {@code identifier.1} after increment. 894 | *
895 | * Examples: 896 | * 897 | * 898 | * 899 | * 900 | * 901 | * 902 | * 903 | * 904 | * 905 | * 906 | * 907 | * 908 | * 909 | * 910 | * 911 | * 912 | * 913 | * 914 | * 915 | * 916 | * 917 | * 918 | * 919 | *
Pre-release identifier incrementation behavior
VersionAfter increment
1.2.31.2.3-1
1.2.3+build.meta.data1.2.3-1
1.2.3-foo1.2.3-foo.1
1.2.3-foo.11.2.3-foo.2
920 | * 921 | * @return The incremented Version. 922 | * @since 1.2.0 923 | */ 924 | public Version nextPreRelease() { 925 | final String[] newPreReleaseParts = incrementIdentifier(this.preReleaseParts); 926 | return new Version(this.major, this.minor, this.patch, newPreReleaseParts, 927 | EMPTY_ARRAY); 928 | } 929 | 930 | /** 931 | * Derives a new Version instance from this one by only incrementing the 932 | * build-meta-data identifier. All other fields remain the same. 933 | * 934 | *

935 | * The incrementation of the build-meta-data identifier behaves as follows: 936 | * 937 | *

    938 | *
  • In case the identifier is currently empty, it becomes "1" in the result.
  • 939 | *
  • If the identifier's last part is numeric, that last part will be incremented in 940 | * the result. Leading 0's will be removed.
  • 941 | *
  • If the last part is not numeric, the identifier is interpreted as 942 | * {@code identifier.0} which becomes {@code identifier.1} after increment. 943 | *
944 | * Examples: 945 | * 946 | * 947 | * 948 | * 949 | * 950 | * 951 | * 952 | * 953 | * 954 | * 955 | * 956 | * 957 | * 958 | * 959 | * 960 | * 961 | * 962 | * 963 | * 964 | * 965 | * 966 | * 967 | * 968 | *
Build meta data incrementation behavior
VersionAfter increment
1.2.31.2.3+1
1.2.3-pre.release1.2.3-pre.release+1
1.2.3+foo1.2.3+foo.1
1.2.3+foo.11.2.3+foo.2
969 | * 970 | * @return The incremented Version. 971 | * @since 1.2.0 972 | */ 973 | public Version nextBuildMetaData() { 974 | final String[] newBuildMetaData = incrementIdentifier(this.buildMetaDataParts); 975 | return new Version(this.major, this.minor, this.patch, this.preReleaseParts, 976 | newBuildMetaData); 977 | } 978 | 979 | private String[] incrementIdentifier(String[] parts) { 980 | if (parts.length == 0) { 981 | return new String[] { "1" }; 982 | } 983 | final int lastIdx = parts.length - 1; 984 | final String lastPart = parts[lastIdx]; 985 | 986 | int num = isNumeric(lastPart); 987 | int newLength = parts.length; 988 | if (num >= 0) { 989 | num += 1; 990 | } else { 991 | newLength += 1; 992 | num = 1; 993 | } 994 | final String[] result = Arrays.copyOf(parts, newLength); 995 | result[newLength - 1] = String.valueOf(num); 996 | return result; 997 | } 998 | 999 | /** 1000 | * Tries to parse the given String as a semantic version and returns whether the 1001 | * String is properly formatted according to the semantic version specification. 1002 | * 1003 | *

1004 | * Note: this method does not throw an exception upon null input, but 1005 | * returns false instead. 1006 | * 1007 | * 1008 | * @param version The String to check. 1009 | * @return Whether the given String is formatted as a semantic version. 1010 | * @since 0.5.0 1011 | */ 1012 | public static boolean isValidVersion(String version) { 1013 | return version != null && !version.isEmpty() && parse(version, true) != null; 1014 | } 1015 | 1016 | /** 1017 | * Returns whether the given String is a valid pre-release identifier. That is, this 1018 | * method returns true if, and only if the {@code preRelease} parameter 1019 | * is either the empty string or properly formatted as a pre-release identifier 1020 | * according to the semantic version specification. 1021 | * 1022 | *

1023 | * Note: this method does not throw an exception upon null input, but 1024 | * returns false instead. 1025 | * 1026 | * @param preRelease The String to check. 1027 | * @return Whether the given String is a valid pre-release identifier. 1028 | * @since 0.5.0 1029 | */ 1030 | public static boolean isValidPreRelease(String preRelease) { 1031 | if (preRelease == null) { 1032 | return false; 1033 | } else if (preRelease.isEmpty()) { 1034 | return true; 1035 | } 1036 | 1037 | return parseID(preRelease.toCharArray(), preRelease, 0, true, false, false, null, 1038 | "") != FAILURE; 1039 | } 1040 | 1041 | /** 1042 | * Returns whether the given String is a valid build meta data identifier. That is, 1043 | * this method returns true if, and only if the {@code buildMetaData} 1044 | * parameter is either the empty string or properly formatted as a build meta data 1045 | * identifier according to the semantic version specification. 1046 | * 1047 | *

1048 | * Note: this method does not throw an exception upon null input, but 1049 | * returns false instead. 1050 | * 1051 | * 1052 | * @param buildMetaData The String to check. 1053 | * @return Whether the given String is a valid build meta data identifier. 1054 | * @since 0.5.0 1055 | */ 1056 | public static boolean isValidBuildMetaData(String buildMetaData) { 1057 | if (buildMetaData == null) { 1058 | return false; 1059 | } else if (buildMetaData.isEmpty()) { 1060 | return true; 1061 | } 1062 | 1063 | return parseID(buildMetaData.toCharArray(), buildMetaData, 0, true, true, false, 1064 | null, "") != FAILURE; 1065 | } 1066 | 1067 | /** 1068 | * Returns the greater of the two given versions by comparing them using their natural 1069 | * ordering. If both versions are equal, then the first argument is returned. 1070 | * 1071 | * @param v1 The first version. 1072 | * @param v2 The second version. 1073 | * @return The greater version. 1074 | * @throws IllegalArgumentException If either argument is null. 1075 | * @since 0.4.0 1076 | */ 1077 | public static Version max(Version v1, Version v2) { 1078 | require(v1 != null, "v1 is null"); 1079 | require(v2 != null, "v2 is null"); 1080 | return compare(v1, v2, false) < 0 1081 | ? v2 1082 | : v1; 1083 | } 1084 | 1085 | /** 1086 | * Returns the lower of the two given versions by comparing them using their natural 1087 | * ordering. If both versions are equal, then the first argument is returned. 1088 | * 1089 | * @param v1 The first version. 1090 | * @param v2 The second version. 1091 | * @return The lower version. 1092 | * @throws IllegalArgumentException If either argument is null. 1093 | * @since 0.4.0 1094 | */ 1095 | public static Version min(Version v1, Version v2) { 1096 | require(v1 != null, "v1 is null"); 1097 | require(v2 != null, "v2 is null"); 1098 | return compare(v1, v2, false) <= 0 1099 | ? v1 1100 | : v2; 1101 | } 1102 | 1103 | /** 1104 | * Compares two versions, following the semantic version specification. Here 1105 | * is a quote from semantic version 2.0.0 1106 | * specification: 1107 | * 1108 | *

1109 | * Precedence refers to how versions are compared to each other when ordered. 1110 | * Precedence MUST be calculated by separating the version into major, minor, patch 1111 | * and pre-release identifiers in that order (Build metadata does not figure into 1112 | * precedence). Precedence is determined by the first difference when comparing each 1113 | * of these identifiers from left to right as follows: Major, minor, and patch 1114 | * versions are always compared numerically. Example: 1.0.0 < 2.0.0 < 2.1.0 < 1115 | * 2.1.1. When major, minor, and patch are equal, a pre-release version has lower 1116 | * precedence than a normal version. Example: 1.0.0-alpha < 1.0.0. Precedence for 1117 | * two pre-release versions with the same major, minor, and patch version MUST be 1118 | * determined by comparing each dot separated identifier from left to right until a 1119 | * difference is found as follows: identifiers consisting of only digits are compared 1120 | * numerically and identifiers with letters or hyphens are compared lexically in ASCII 1121 | * sort order. Numeric identifiers always have lower precedence than non-numeric 1122 | * identifiers. A larger set of pre-release fields has a higher precedence than a 1123 | * smaller set, if all of the preceding identifiers are equal. Example: 1.0.0-alpha 1124 | * < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1125 | * 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0. 1126 | * 1127 | * 1128 | *

1129 | * This method fulfills the general contract for Java's {@link Comparator Comparators} 1130 | * and {@link Comparable Comparables}. 1131 | * 1132 | * 1133 | * @param v1 The first version for comparison. 1134 | * @param v2 The second version for comparison. 1135 | * @return A value below 0 iff {@code v1 < v2}, a value above 0 iff 1136 | * {@code v1 > v2 and 0 iff v1 = v2}. 1137 | * @throws NullPointerException If either parameter is null. 1138 | * @since 0.2.0 1139 | */ 1140 | public static int compare(Version v1, Version v2) { 1141 | // throw NPE to comply with Comparable specification 1142 | if (v1 == null) { 1143 | throw new NullPointerException("v1 is null"); 1144 | } else if (v2 == null) { 1145 | throw new NullPointerException("v2 is null"); 1146 | } 1147 | return compare(v1, v2, false); 1148 | } 1149 | 1150 | /** 1151 | * Compares two Versions with additionally considering the build meta data field if 1152 | * all other parts are equal. Note: This is not part of the semantic version 1153 | * specification. 1154 | * 1155 | *

1156 | * Comparison of the build meta data parts happens exactly as for pre release 1157 | * identifiers. Considering of build meta data first kicks in if both versions are 1158 | * equal when using their natural order. 1159 | * 1160 | * 1161 | *

1162 | * This method fulfills the general contract for Java's {@link Comparator Comparators} 1163 | * and {@link Comparable Comparables}. 1164 | * 1165 | * 1166 | * @param v1 The first version for comparison. 1167 | * @param v2 The second version for comparison. 1168 | * @return A value below 0 iff {@code v1 < v2}, a value above 0 iff 1169 | * {@code v1 > v2 and 0 iff v1 = v2}. 1170 | * @throws NullPointerException If either parameter is null. 1171 | * @since 0.3.0 1172 | */ 1173 | public static int compareWithBuildMetaData(Version v1, Version v2) { 1174 | // throw NPE to comply with Comparable specification 1175 | if (v1 == null) { 1176 | throw new NullPointerException("v1 is null"); 1177 | } else if (v2 == null) { 1178 | throw new NullPointerException("v2 is null"); 1179 | } 1180 | return compare(v1, v2, true); 1181 | } 1182 | 1183 | private static int compare(Version v1, Version v2, 1184 | boolean withBuildMetaData) { 1185 | int result = 0; 1186 | if (v1 != v2) { 1187 | final int mc, mm, mp, pr, md; 1188 | if ((mc = compareInt(v1.major, v2.major)) != 0) { 1189 | result = mc; 1190 | } else if ((mm = compareInt(v1.minor, v2.minor)) != 0) { 1191 | result = mm; 1192 | } else if ((mp = compareInt(v1.patch, v2.patch)) != 0) { 1193 | result = mp; 1194 | } else if ((pr = comparePreRelease(v1, v2)) != 0) { 1195 | result = pr; 1196 | } else if (withBuildMetaData && ((md = compareBuildMetaData(v1, v2)) != 0)) { 1197 | result = md; 1198 | } 1199 | } 1200 | return result; 1201 | } 1202 | 1203 | private static int compareInt(int a, int b) { 1204 | return a - b; 1205 | } 1206 | 1207 | private static int comparePreRelease(Version v1, Version v2) { 1208 | return compareLiterals(v1.preReleaseParts, v2.preReleaseParts); 1209 | } 1210 | 1211 | private static int compareBuildMetaData(Version v1, Version v2) { 1212 | return compareLiterals(v1.buildMetaDataParts, v2.buildMetaDataParts); 1213 | } 1214 | 1215 | private static int compareLiterals(String[] v1Literal, String[] v2Literal) { 1216 | final int result; 1217 | if (v1Literal.length > 0 && v2Literal.length > 0) { 1218 | // compare pre release parts 1219 | result = compareIdentifiers(v1Literal, v2Literal); 1220 | } else if (v1Literal.length > 0) { 1221 | // other is greater, because it is no pre release 1222 | result = -1; 1223 | } else if (v2Literal.length > 0) { 1224 | // this is greater because other is no pre release 1225 | result = 1; 1226 | } else { 1227 | result = 0; 1228 | } 1229 | return result; 1230 | } 1231 | 1232 | private static int compareIdentifiers(String[] parts1, String[] parts2) { 1233 | final int min = Math.min(parts1.length, parts2.length); 1234 | for (int i = 0; i < min; ++i) { 1235 | final int r = compareIdentifierParts(parts1[i], parts2[i]); 1236 | if (r != 0) { 1237 | // versions differ in part i 1238 | return r; 1239 | } 1240 | } 1241 | 1242 | // all id's are equal, so compare amount of id's 1243 | return compareInt(parts1.length, parts2.length); 1244 | } 1245 | 1246 | private static int compareIdentifierParts(String p1, String p2) { 1247 | final int num1 = isNumeric(p1); 1248 | final int num2 = isNumeric(p2); 1249 | 1250 | final int result; 1251 | if (num1 < 0 && num2 < 0) { 1252 | // both are not numerical -> compare lexically 1253 | result = p1.compareTo(p2); 1254 | } else if (num1 >= 0 && num2 >= 0) { 1255 | // both are numerical 1256 | result = compareInt(num1, num2); 1257 | } else if (num1 >= 0) { 1258 | // only part1 is numerical -> p2 is greater 1259 | result = -1; 1260 | } else { 1261 | // only part2 is numerical -> p1 is greater 1262 | result = 1; 1263 | } 1264 | return result; 1265 | } 1266 | 1267 | /** 1268 | * Determines whether s is a positive number. If it is, the number is returned, 1269 | * otherwise the result is -1. 1270 | * 1271 | * @param s The String to check. 1272 | * @return The positive number (incl. 0) if s a number, or -1 if it is not. 1273 | */ 1274 | private static int isNumeric(String s) { 1275 | final char[] chars = s.toCharArray(); 1276 | int num = 0; 1277 | 1278 | // note: this method does not account for leading zeroes as could occur in build 1279 | // meta data parts. Leading zeroes are thus simply ignored when parsing the 1280 | // number. 1281 | for (int i = 0; i < chars.length; ++i) { 1282 | final char c = chars[i]; 1283 | if (c >= '0' && c <= '9') { 1284 | num = num * DECIMAL + Character.digit(c, DECIMAL); 1285 | } else { 1286 | return -1; 1287 | } 1288 | } 1289 | return num; 1290 | } 1291 | 1292 | private static String[] parsePreRelease(String preRelease) { 1293 | if (preRelease != null && !preRelease.isEmpty()) { 1294 | final List parts = new ArrayList(); 1295 | parseID(preRelease.toCharArray(), preRelease, 0, false, false, false, parts, 1296 | "pre-release"); 1297 | return parts.toArray(new String[parts.size()]); 1298 | } 1299 | return EMPTY_ARRAY; 1300 | } 1301 | 1302 | private static String[] parseBuildMd(String buildMetaData) { 1303 | if (buildMetaData != null && !buildMetaData.isEmpty()) { 1304 | final List parts = new ArrayList(); 1305 | parseID(buildMetaData.toCharArray(), buildMetaData, 0, false, true, false, 1306 | parts, "build-meta-data"); 1307 | return parts.toArray(new String[parts.size()]); 1308 | } 1309 | return EMPTY_ARRAY; 1310 | } 1311 | 1312 | private static final Version createInternal(int major, int minor, int patch, 1313 | String preRelease, String buildMetaData) { 1314 | 1315 | final String[] preReleaseParts = parsePreRelease(preRelease); 1316 | final String[] buildMdParts = parseBuildMd(buildMetaData); 1317 | return new Version(major, minor, patch, preReleaseParts, buildMdParts); 1318 | } 1319 | 1320 | /** 1321 | * Creates a new Version from the provided components. Neither value of 1322 | * {@code major, minor} or {@code patch} must be lower than 0 and at least one must be 1323 | * greater than zero. {@code preRelease} or {@code buildMetaData} may be the empty 1324 | * String. In this case, the created {@code Version} will have no pre release resp. 1325 | * build meta data field. If those parameters are not empty, they must conform to the 1326 | * semantic version specification. 1327 | * 1328 | * @param major The major version. 1329 | * @param minor The minor version. 1330 | * @param patch The patch version. 1331 | * @param preRelease The pre release version or the empty string. 1332 | * @param buildMetaData The build meta data field or the empty string. 1333 | * @return The version instance. 1334 | * @throws VersionFormatException If {@code preRelease} or {@code buildMetaData} does 1335 | * not conform to the semantic version specification. 1336 | */ 1337 | public static final Version create(int major, int minor, int patch, 1338 | String preRelease, 1339 | String buildMetaData) { 1340 | require(preRelease != null, "preRelease is null"); 1341 | require(buildMetaData != null, "buildMetaData is null"); 1342 | return createInternal(major, minor, patch, preRelease, buildMetaData); 1343 | } 1344 | 1345 | /** 1346 | * Creates a new Version from the provided components. The version's build meta data 1347 | * field will be empty. Neither value of {@code major, minor} or {@code patch} must be 1348 | * lower than 0 and at least one must be greater than zero. {@code preRelease} may be 1349 | * the empty String. In this case, the created version will have no pre release field. 1350 | * If it is not empty, it must conform to the specifications of the semantic version. 1351 | * 1352 | * @param major The major version. 1353 | * @param minor The minor version. 1354 | * @param patch The patch version. 1355 | * @param preRelease The pre release version or the empty string. 1356 | * @return The version instance. 1357 | * @throws VersionFormatException If {@code preRelease} is not empty and does not 1358 | * conform to the semantic versioning specification 1359 | */ 1360 | public static final Version create(int major, int minor, int patch, 1361 | String preRelease) { 1362 | return create(major, minor, patch, preRelease, ""); 1363 | } 1364 | 1365 | /** 1366 | * Creates a new Version from the three provided components. The version's pre release 1367 | * and build meta data fields will be empty. Neither value must be lower than 0 and at 1368 | * least one must be greater than zero. 1369 | * 1370 | * @param major The major version. 1371 | * @param minor The minor version. 1372 | * @param patch The patch version. 1373 | * @return The version instance. 1374 | */ 1375 | public static final Version create(int major, int minor, int patch) { 1376 | return new Version(major, minor, patch, EMPTY_ARRAY, EMPTY_ARRAY); 1377 | } 1378 | 1379 | /** 1380 | * Creates a new Version from the two provided components, leaving the patch version 1381 | * 0. The version's pre release and build meta data fields will be empty. Neither 1382 | * value must be lower than 0 and at least one must be greater than zero. 1383 | * 1384 | * @param major The major version. 1385 | * @param minor The minor version. 1386 | * @return The version instance. 1387 | * @since 2.1.0 1388 | */ 1389 | public static final Version create(int major, int minor) { 1390 | return new Version(major, minor, 0, EMPTY_ARRAY, EMPTY_ARRAY); 1391 | } 1392 | 1393 | /** 1394 | * Creates a new Version with the provided major version, leaving the minor and patch 1395 | * version 0. The version's pre release and build meta data fields will be empty. The 1396 | * major value must be lower than or equal to 0. 1397 | * 1398 | * @param major The major version. 1399 | * @return The version instance. 1400 | * @since 2.1.0 1401 | */ 1402 | public static final Version create(int major) { 1403 | return new Version(major, 0, 0, EMPTY_ARRAY, EMPTY_ARRAY); 1404 | } 1405 | 1406 | private static void checkParams(int major, int minor, int patch) { 1407 | require(major >= 0, "major < 0"); 1408 | require(minor >= 0, "minor < 0"); 1409 | require(patch >= 0, "patch < 0"); 1410 | } 1411 | 1412 | private static void require(boolean condition, String message) { 1413 | if (!condition) { 1414 | throw new IllegalArgumentException(message); 1415 | } 1416 | } 1417 | 1418 | /** 1419 | * Tries to parse the provided String as a semantic version. If the string does not 1420 | * conform to the semantic version specification, a {@link VersionFormatException} 1421 | * will be thrown. 1422 | * 1423 | * @param versionString The String to parse. 1424 | * @return The parsed version. 1425 | * @throws VersionFormatException If the String is no valid version 1426 | * @throws IllegalArgumentException If {@code versionString} is null. 1427 | */ 1428 | public static final Version parseVersion(String versionString) { 1429 | require(versionString != null, "versionString is null"); 1430 | return parse(versionString, false); 1431 | } 1432 | 1433 | /** 1434 | * Tries to parse the provided String as a semantic version. If 1435 | * {@code allowPreRelease} is false, the String must have neither a 1436 | * pre-release nor a build meta data part. Thus the given String must have the format 1437 | * {@code X.Y.Z} where at least one part must be greater than zero. 1438 | * 1439 | *

1440 | * If {@code allowPreRelease} is true, the String is parsed according to 1441 | * the normal semantic version specification. 1442 | * 1443 | * 1444 | * @param versionString The String to parse. 1445 | * @param allowPreRelease Whether pre-release and build meta data field are allowed. 1446 | * @return The parsed version. 1447 | * @throws VersionFormatException If the String is no valid version 1448 | * @since 0.4.0 1449 | */ 1450 | public static Version parseVersion(String versionString, 1451 | boolean allowPreRelease) { 1452 | final Version version = parseVersion(versionString); 1453 | if (!allowPreRelease && (version.isPreRelease() || version.hasBuildMetaData())) { 1454 | throw new VersionFormatException(String.format( 1455 | "Version string '%s' is expected to have no pre-release or " 1456 | + "build meta data part", 1457 | versionString)); 1458 | } 1459 | return version; 1460 | } 1461 | 1462 | /** 1463 | * Returns the lower of this version and the given version according to its natural 1464 | * ordering. If versions are equal, {@code this} is returned. 1465 | * 1466 | * @param other The version to compare with. 1467 | * @return The lower version. 1468 | * @throws IllegalArgumentException If {@code other} is null. 1469 | * @since 0.5.0 1470 | * @see #min(Version, Version) 1471 | */ 1472 | public Version min(Version other) { 1473 | return min(this, other); 1474 | } 1475 | 1476 | /** 1477 | * Returns the greater of this version and the given version according to its natural 1478 | * ordering. If versions are equal, {@code this} is returned. 1479 | * 1480 | * @param other The version to compare with. 1481 | * @return The greater version. 1482 | * @throws IllegalArgumentException If {@code other} is null. 1483 | * @since 0.5.0 1484 | * @see #max(Version, Version) 1485 | */ 1486 | public Version max(Version other) { 1487 | return max(this, other); 1488 | } 1489 | 1490 | /** 1491 | * Gets this version's major number. 1492 | * 1493 | * @return The major version. 1494 | */ 1495 | public int getMajor() { 1496 | return this.major; 1497 | } 1498 | 1499 | /** 1500 | * Gets this version's minor number. 1501 | * 1502 | * @return The minor version. 1503 | */ 1504 | public int getMinor() { 1505 | return this.minor; 1506 | } 1507 | 1508 | /** 1509 | * Gets this version's path number. 1510 | * 1511 | * @return The patch number. 1512 | */ 1513 | public int getPatch() { 1514 | return this.patch; 1515 | } 1516 | 1517 | /** 1518 | * Gets the pre release identifier parts of this version as array. Modifying the 1519 | * resulting array will have no influence on the internal state of this object. 1520 | * 1521 | * @return Pre release version as array. Array is empty if this version has no pre 1522 | * release part. 1523 | */ 1524 | public String[] getPreReleaseParts() { 1525 | if (this.preReleaseParts.length == 0) { 1526 | return EMPTY_ARRAY; 1527 | } 1528 | return Arrays.copyOf(this.preReleaseParts, this.preReleaseParts.length); 1529 | } 1530 | 1531 | /** 1532 | * Gets the pre release identifier of this version. If this version has no such 1533 | * identifier, an empty string is returned. 1534 | * 1535 | *

1536 | * Note: This method will always reconstruct a new String by joining the single 1537 | * identifier parts. 1538 | * 1539 | * 1540 | * @return This version's pre release identifier or an empty String if this version 1541 | * has no such identifier. 1542 | */ 1543 | public String getPreRelease() { 1544 | return join(this.preReleaseParts); 1545 | } 1546 | 1547 | /** 1548 | * Gets this version's build meta data. If this version has no build meta data, the 1549 | * returned string is empty. 1550 | * 1551 | *

1552 | * Note: This method will always reconstruct a new String by joining the single 1553 | * identifier parts. 1554 | * 1555 | * 1556 | * @return The build meta data or an empty String if this version has no build meta 1557 | * data. 1558 | */ 1559 | public String getBuildMetaData() { 1560 | return join(this.buildMetaDataParts); 1561 | } 1562 | 1563 | private static String join(String[] parts) { 1564 | if (parts.length == 0) { 1565 | return ""; 1566 | } 1567 | final StringBuilder b = new StringBuilder(); 1568 | for (int i = 0; i < parts.length; i++) { 1569 | final String part = parts[i]; 1570 | b.append(part); 1571 | if (i < parts.length - 1) { 1572 | b.append("."); 1573 | } 1574 | } 1575 | return b.toString(); 1576 | } 1577 | 1578 | /** 1579 | * Gets the build meta data identifier parts of this version as array. Modifying the 1580 | * resulting array will have no influence on the internal state of this object. 1581 | * 1582 | * @return Build meta data as array. Array is empty if this version has no build meta 1583 | * data part. 1584 | */ 1585 | public String[] getBuildMetaDataParts() { 1586 | if (this.buildMetaDataParts.length == 0) { 1587 | return EMPTY_ARRAY; 1588 | } 1589 | return Arrays.copyOf(this.buildMetaDataParts, this.buildMetaDataParts.length); 1590 | } 1591 | 1592 | /** 1593 | * Determines whether this version is still under initial development. 1594 | * 1595 | * @return true iff this version's major part is zero. 1596 | */ 1597 | public boolean isInitialDevelopment() { 1598 | return this.major == 0; 1599 | } 1600 | 1601 | /** 1602 | * Whether this is a 'stable' version. That is, it has no pre-release identifier. 1603 | * 1604 | * @return true iff {@link #getPreRelease()} is empty. 1605 | * @see #isPreRelease() 1606 | * @since 2.1.0 1607 | */ 1608 | public boolean isStable() { 1609 | return this.preReleaseParts.length == 0; 1610 | } 1611 | 1612 | /** 1613 | * Determines whether this is a pre release version. 1614 | * 1615 | * @return true iff {@link #getPreRelease()} is not empty. 1616 | * @see #isStable() 1617 | */ 1618 | public boolean isPreRelease() { 1619 | return this.preReleaseParts.length > 0; 1620 | } 1621 | 1622 | /** 1623 | * Determines whether this version has a build meta data field. 1624 | * 1625 | * @return true iff {@link #getBuildMetaData()} is not empty. 1626 | */ 1627 | public boolean hasBuildMetaData() { 1628 | return this.buildMetaDataParts.length > 0; 1629 | } 1630 | 1631 | /** 1632 | * Creates a String representation of this version by joining its parts together as by 1633 | * the semantic version specification. 1634 | * 1635 | * @return The version as a String. 1636 | */ 1637 | @Override 1638 | public String toString() { 1639 | final StringBuilder b = new StringBuilder(TO_STRING_ESTIMATE); 1640 | b.append(this.major).append(".") 1641 | .append(this.minor).append(".") 1642 | .append(this.patch); 1643 | 1644 | if (isPreRelease()) { 1645 | b.append("-").append(getPreRelease()); 1646 | } 1647 | if (hasBuildMetaData()) { 1648 | b.append("+").append(getBuildMetaData()); 1649 | } 1650 | return b.toString(); 1651 | } 1652 | 1653 | /** 1654 | * The hash code for a version instance is computed from the fields {@link #getMajor() 1655 | * major}, {@link #getMinor() minor}, {@link #getPatch() patch} and 1656 | * {@link #getPreRelease() pre-release}. 1657 | * 1658 | * @return A hash code for this object. 1659 | */ 1660 | @Override 1661 | public int hashCode() { 1662 | final int h = this.hash; 1663 | if (h == NOT_YET_CALCULATED) { 1664 | this.hash = calculateHashCode(); 1665 | } 1666 | return this.hash; 1667 | } 1668 | 1669 | private int calculateHashCode() { 1670 | int h = HASH_PRIME + this.major; 1671 | h = HASH_PRIME * h + this.minor; 1672 | h = HASH_PRIME * h + this.patch; 1673 | h = HASH_PRIME * h + Arrays.hashCode(this.preReleaseParts); 1674 | return h; 1675 | } 1676 | 1677 | /** 1678 | * Determines whether this version is equal to the passed object. This is the case if 1679 | * the passed object is an instance of Version and this version 1680 | * {@link #compareTo(Version) compared} to the provided one yields 0. Thus, this 1681 | * method ignores the {@link #getBuildMetaData()} field. 1682 | * 1683 | * @param obj the object to compare with. 1684 | * @return true iff {@code obj} is an instance of {@code Version} and 1685 | * {@code this.compareTo((Version) obj) == 0} 1686 | * @see #compareTo(Version) 1687 | */ 1688 | @Override 1689 | public boolean equals(Object obj) { 1690 | return testEquality(obj, false); 1691 | } 1692 | 1693 | /** 1694 | * Determines whether this version is equal to the passed object (as determined by 1695 | * {@link #equals(Object)} and additionally considers the build meta data part of both 1696 | * versions for equality. 1697 | * 1698 | * @param obj The object to compare with. 1699 | * @return true iff {@code this.equals(obj)} and 1700 | * {@code this.getBuildMetaData().equals(((Version) obj).getBuildMetaData())} 1701 | * @since 0.4.0 1702 | */ 1703 | public boolean equalsWithBuildMetaData(Object obj) { 1704 | return testEquality(obj, true); 1705 | } 1706 | 1707 | private boolean testEquality(Object obj, boolean includeBuildMd) { 1708 | return obj == this || obj instanceof Version 1709 | && compare(this, (Version) obj, includeBuildMd) == 0; 1710 | } 1711 | 1712 | /** 1713 | * Compares this version to the provided one, following the semantic 1714 | * versioning specification. See {@link #compare(Version, Version)} for more 1715 | * information. 1716 | * 1717 | * @param other The version to compare to. 1718 | * @return A value lower than 0 if this < other, a value greater than 0 if this 1719 | * > other and 0 if this == other. The absolute value of the result has no 1720 | * semantical interpretation. 1721 | */ 1722 | @Override 1723 | public int compareTo(Version other) { 1724 | return compare(this, other); 1725 | } 1726 | 1727 | /** 1728 | * Compares this version to the provided one. Unlike the {@link #compareTo(Version)} 1729 | * method, this one additionally considers the build meta data field of both versions, 1730 | * if all other parts are equal. Note: This is not part of the semantic 1731 | * version specification. 1732 | * 1733 | *

1734 | * Comparison of the build meta data parts happens exactly as for pre release 1735 | * identifiers. Considering of build meta data first kicks in if both versions are 1736 | * equal when using their natural order. 1737 | * 1738 | * 1739 | * @param other The version to compare to. 1740 | * @return A value lower than 0 if this < other, a value greater than 0 if this 1741 | * > other and 0 if this == other. The absolute value of the result has no 1742 | * semantical interpretation. 1743 | * @since 0.3.0 1744 | */ 1745 | public int compareToWithBuildMetaData(Version other) { 1746 | return compareWithBuildMetaData(this, other); 1747 | } 1748 | 1749 | /** 1750 | * Returns a new Version where all identifiers are converted to upper case letters. 1751 | * 1752 | * @return A new version with lower case identifiers. 1753 | * @since 1.1.0 1754 | */ 1755 | public Version toUpperCase() { 1756 | return new Version(this.major, this.minor, this.patch, 1757 | copyCase(this.preReleaseParts, true), 1758 | copyCase(this.buildMetaDataParts, true)); 1759 | } 1760 | 1761 | /** 1762 | * Returns a new Version where all identifiers are converted to lower case letters. 1763 | * 1764 | * @return A new version with lower case identifiers. 1765 | * @since 1.1.0 1766 | */ 1767 | public Version toLowerCase() { 1768 | return new Version(this.major, this.minor, this.patch, 1769 | copyCase(this.preReleaseParts, false), 1770 | copyCase(this.buildMetaDataParts, false)); 1771 | } 1772 | 1773 | private static String[] copyCase(String[] source, boolean toUpper) { 1774 | final String[] result = new String[source.length]; 1775 | for (int i = 0; i < source.length; i++) { 1776 | final String string = source[i]; 1777 | result[i] = toUpper ? string.toUpperCase(Locale.ROOT) : string.toLowerCase(Locale.ROOT); 1778 | } 1779 | return result; 1780 | } 1781 | 1782 | /** 1783 | * Tests whether this version is strictly greater than the given other version in 1784 | * terms of precedence. Does not consider the build meta data part. 1785 | *

1786 | * Convenience method for {@code this.compareTo(other) > 0} except that this method 1787 | * throws an {@link IllegalArgumentException} if other is null. 1788 | * 1789 | * 1790 | * @param other The version to compare to. 1791 | * @return Whether this version is strictly greater. 1792 | * @since 1.1.0 1793 | */ 1794 | public boolean isGreaterThan(Version other) { 1795 | require(other != null, "other must no be null"); 1796 | return compareTo(other) > 0; 1797 | } 1798 | 1799 | /** 1800 | * Tests whether this version is equal to or greater than the given other version in 1801 | * terms of precedence. Does not consider the build meta data part. 1802 | *

1803 | * Convenience method for {@code this.compareTo(other) >= 0} except that this method 1804 | * throws an {@link IllegalArgumentException} if other is null. 1805 | * 1806 | * @param other The version to compare to. 1807 | * @return Whether this version is greater or equal. 1808 | * @since 2.1.0 1809 | */ 1810 | public boolean isGreaterThanOrEqualTo(Version other) { 1811 | require(other != null, "other must no be null"); 1812 | return compareTo(other) >= 0; 1813 | } 1814 | 1815 | /** 1816 | * Tests whether this version is strictly lower than the given other version in terms 1817 | * of precedence. Does not consider the build meta data part. 1818 | *

1819 | * Convenience method for {@code this.compareTo(other) < 0} except that this method 1820 | * throws an {@link IllegalArgumentException} if other is null. 1821 | * 1822 | * @param other The version to compare to. 1823 | * @return Whether this version is strictly lower. 1824 | * @since 1.1.0 1825 | */ 1826 | public boolean isLowerThan(Version other) { 1827 | require(other != null, "other must no be null"); 1828 | return compareTo(other) < 0; 1829 | } 1830 | 1831 | /** 1832 | * Tests whether this version is equal to or lower than the given other version in 1833 | * terms of precedence. Does not consider the build meta data part. 1834 | *

1835 | * Convenience method for {@code this.compareTo(other) <= 0} except that this method 1836 | * throws an {@link IllegalArgumentException} if other is null. 1837 | * 1838 | * @param other The version to compare to. 1839 | * @return Whether this version is lower or equal. 1840 | * @since 2.1.0 1841 | */ 1842 | public boolean isLowerThanOrEqualTo(Version other) { 1843 | require(other != null, "other must no be null"); 1844 | return compareTo(other) <= 0; 1845 | } 1846 | 1847 | /** 1848 | * Handles proper deserialization of objects serialized with a version prior to 1.1.0 1849 | * 1850 | * @return the deserialized object. 1851 | * @throws ObjectStreamException If deserialization fails. 1852 | * @since 1.1.0 1853 | */ 1854 | private Object readResolve() throws ObjectStreamException { 1855 | if (this.preRelease != null || this.buildMetaData != null) { 1856 | return createInternal(this.major, this.minor, this.patch, 1857 | this.preRelease, 1858 | this.buildMetaData); 1859 | } 1860 | return this; 1861 | } 1862 | } 1863 | -------------------------------------------------------------------------------- /src/main/java/de/skuzzle/semantic/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains a single class semantic version (specification 2.0) implementation. 3 | */ 4 | package de.skuzzle.semantic; 5 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Simon Taddiken 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 7 | * software and associated documentation files (the "Software"), to deal in the Software 8 | * without restriction, including without limitation the rights to use, copy, modify, 9 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 10 | * permit persons to whom the Software is furnished to do so, subject to the following 11 | * conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all copies 14 | * or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 17 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 20 | * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 21 | * THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | module de.skuzzle.semantic { 24 | exports de.skuzzle.semantic; 25 | } -------------------------------------------------------------------------------- /src/test/java/de/skuzzle/semantic/CustomGsonSerialization.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.semantic; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.lang.reflect.Type; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | import com.google.gson.Gson; 10 | import com.google.gson.GsonBuilder; 11 | import com.google.gson.JsonDeserializationContext; 12 | import com.google.gson.JsonDeserializer; 13 | import com.google.gson.JsonElement; 14 | import com.google.gson.JsonParseException; 15 | import com.google.gson.JsonPrimitive; 16 | import com.google.gson.JsonSerializationContext; 17 | import com.google.gson.JsonSerializer; 18 | 19 | public class CustomGsonSerialization { 20 | 21 | private static class SemanticVersionSerializer implements JsonSerializer, JsonDeserializer { 22 | 23 | @Override 24 | public Version deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) 25 | throws JsonParseException { 26 | 27 | final String versionString = json.getAsString(); 28 | return Version.parseVersion(versionString); 29 | } 30 | 31 | @Override 32 | public JsonElement serialize(Version src, Type typeOfSrc, JsonSerializationContext context) { 33 | return new JsonPrimitive(src.toString()); 34 | } 35 | } 36 | 37 | private static final class ObjectWithVersionField { 38 | private Version version; 39 | private String differentField; 40 | 41 | public ObjectWithVersionField() { 42 | } 43 | 44 | public ObjectWithVersionField(Version version, String differentField) { 45 | this.version = version; 46 | this.differentField = differentField; 47 | } 48 | 49 | public Version getVersion() { 50 | return this.version; 51 | } 52 | } 53 | 54 | @Test 55 | public void testCustomGsonSerialization() throws Exception { 56 | final Gson gson = new GsonBuilder() 57 | .registerTypeAdapter(Version.class, new SemanticVersionSerializer()) 58 | .create(); 59 | 60 | final ObjectWithVersionField object = new ObjectWithVersionField(Version.create(1, 2, 3, "pre-release"), 61 | "someString"); 62 | final String json = gson.toJson(object); 63 | final ObjectWithVersionField object2 = gson.fromJson(json, ObjectWithVersionField.class); 64 | assertEquals(object.getVersion(), object2.getVersion()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/de/skuzzle/semantic/CustomJacksonSerialization.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.semantic; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.io.IOException; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | import com.fasterxml.jackson.core.JsonGenerator; 10 | import com.fasterxml.jackson.core.JsonParser; 11 | import com.fasterxml.jackson.core.JsonProcessingException; 12 | import com.fasterxml.jackson.databind.DeserializationContext; 13 | import com.fasterxml.jackson.databind.JsonDeserializer; 14 | import com.fasterxml.jackson.databind.JsonSerializer; 15 | import com.fasterxml.jackson.databind.ObjectMapper; 16 | import com.fasterxml.jackson.databind.SerializerProvider; 17 | import com.fasterxml.jackson.databind.module.SimpleModule; 18 | 19 | public class CustomJacksonSerialization { 20 | 21 | private static class SemanticVersionDeserializer extends JsonDeserializer { 22 | 23 | @Override 24 | public Version deserialize(JsonParser p, DeserializationContext ctxt) 25 | throws IOException, JsonProcessingException { 26 | 27 | final String versionString = p.readValueAs(String.class); 28 | return Version.parseVersion(versionString); 29 | } 30 | } 31 | 32 | private static final class SemanticVersionSerializer extends JsonSerializer { 33 | 34 | @Override 35 | public void serialize(Version value, JsonGenerator gen, SerializerProvider serializers) throws IOException { 36 | gen.writeString(value.toString()); 37 | } 38 | 39 | } 40 | 41 | private static final class ObjectWithVersionField { 42 | private Version version; 43 | private String differentField; 44 | 45 | public ObjectWithVersionField() { 46 | } 47 | 48 | public ObjectWithVersionField(Version version, String differentField) { 49 | this.version = version; 50 | this.differentField = differentField; 51 | } 52 | 53 | public Version getVersion() { 54 | return this.version; 55 | } 56 | 57 | public String getDifferentField() { 58 | return this.differentField; 59 | } 60 | 61 | } 62 | 63 | @Test 64 | public void testCustomGsonSerialization() throws Exception { 65 | final SimpleModule module = new SimpleModule(); 66 | module.addDeserializer(Version.class, new SemanticVersionDeserializer()); 67 | module.addSerializer(Version.class, new SemanticVersionSerializer()); 68 | final ObjectMapper objectMapper = new ObjectMapper().registerModule(module); 69 | 70 | final ObjectWithVersionField object = new ObjectWithVersionField(Version.create(1, 2, 3, "pre-release"), 71 | "someString"); 72 | final String json = objectMapper.writeValueAsString(object); 73 | final ObjectWithVersionField object2 = objectMapper.readValue(json, ObjectWithVersionField.class); 74 | assertEquals(object.getVersion(), object2.getVersion()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/test/java/de/skuzzle/semantic/IncrementationTest.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.semantic; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertThrows; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class IncrementationTest { 10 | 11 | @Test 12 | void testToStableIsAlreadyStable() throws Exception { 13 | final Version v = Version.create(1, 2, 3).withBuildMetaData("build"); 14 | final Version stable = v.toStable(); 15 | assertEquals(v, stable); 16 | assertFalse(stable.hasBuildMetaData()); 17 | } 18 | 19 | @Test 20 | void testToStableDropIdentifiers() throws Exception { 21 | final Version v = Version.create(1, 2, 3).withPreRelease("SNAPSHOT").withBuildMetaData("build"); 22 | final Version stable = v.toStable(); 23 | final Version expected = Version.create(1, 2, 3); 24 | assertEquals(expected, stable); 25 | assertFalse(stable.hasBuildMetaData()); 26 | } 27 | 28 | @Test 29 | public void testIncrementMajor() throws Exception { 30 | final Version v = Version.create(1, 2, 3, "pre-release", "build"); 31 | assertEquals(Version.create(2, 0, 0), v.nextMajor()); 32 | } 33 | 34 | @Test 35 | public void testIncrementMajorWithPreReleaseString() throws Exception { 36 | final Version v = Version.create(1, 2, 3, "pre-release", "build"); 37 | assertEquals(Version.create(2, 0, 0, "new.pre.release"), 38 | v.nextMajor("new.pre.release")); 39 | } 40 | 41 | @Test 42 | public void testIncrementMajorWithPreReleaseArray() throws Exception { 43 | final Version v = Version.create(1, 2, 3, "pre-release", "build"); 44 | assertEquals(Version.create(2, 0, 0, "new.pre.release"), 45 | v.nextMajor(new String[] { "new", "pre", "release" })); 46 | } 47 | 48 | @Test 49 | public void testIncrementMajorWithPreReleaseStringNull() throws Exception { 50 | final Version v = Version.create(1, 2, 3, "pre-release", "build"); 51 | assertThrows(IllegalArgumentException.class, () -> v.nextMajor((String) null)); 52 | } 53 | 54 | @Test 55 | public void testIncrementMajorWithPreReleaseArrayNull() throws Exception { 56 | final Version v = Version.create(1, 2, 3, "pre-release", "build"); 57 | assertThrows(IllegalArgumentException.class, () -> v.nextMajor((String[]) null)); 58 | } 59 | 60 | @Test 61 | public void testIncrementMinor() throws Exception { 62 | final Version v = Version.create(1, 2, 3, "pre-release", "build"); 63 | assertEquals(Version.create(1, 3, 0), v.nextMinor()); 64 | } 65 | 66 | @Test 67 | public void testIncrementMinorWithPreReleaseString() throws Exception { 68 | final Version v = Version.create(1, 2, 3, "pre-release", "build"); 69 | assertEquals(Version.create(1, 3, 0, "new.pre.release"), 70 | v.nextMinor("new.pre.release")); 71 | } 72 | 73 | @Test 74 | public void testIncrementMinorWithPreReleaseArray() throws Exception { 75 | final Version v = Version.create(1, 2, 3, "pre-release", "build"); 76 | assertEquals(Version.create(1, 3, 0, "new.pre.release"), 77 | v.nextMinor(new String[] { "new", "pre", "release" })); 78 | } 79 | 80 | @Test 81 | public void testIncrementMinorWithPreReleaseStringNull() throws Exception { 82 | final Version v = Version.create(1, 2, 3, "pre-release", "build"); 83 | assertThrows(IllegalArgumentException.class, () -> v.nextMinor((String) null)); 84 | } 85 | 86 | @Test 87 | public void testIncrementMinorWithPreReleaseArrayNull() throws Exception { 88 | final Version v = Version.create(1, 2, 3, "pre-release", "build"); 89 | assertThrows(IllegalArgumentException.class, () -> v.nextMinor((String[]) null)); 90 | } 91 | 92 | @Test 93 | public void testIncrementPatch() throws Exception { 94 | final Version v = Version.create(1, 2, 3, "pre-release", "build"); 95 | assertEquals(Version.create(1, 2, 4), v.nextPatch()); 96 | } 97 | 98 | @Test 99 | public void testIncrementPatchWithPreReleaseString() throws Exception { 100 | final Version v = Version.create(1, 2, 3, "pre-release", "build"); 101 | assertEquals(Version.create(1, 2, 4, "new.pre.release"), 102 | v.nextPatch("new.pre.release")); 103 | } 104 | 105 | @Test 106 | public void testIncrementPatchWithPreReleaseArray() throws Exception { 107 | final Version v = Version.create(1, 2, 3, "pre-release", "build"); 108 | assertEquals(Version.create(1, 2, 4, "new.pre.release"), 109 | v.nextPatch(new String[] { "new", "pre", "release" })); 110 | } 111 | 112 | @Test 113 | public void testIncrementPatchWithPreReleaseStringNull() throws Exception { 114 | final Version v = Version.create(1, 2, 3, "pre-release", "build"); 115 | assertThrows(IllegalArgumentException.class, () -> v.nextPatch((String) null)); 116 | } 117 | 118 | @Test 119 | public void testIncrementPatchWithPreReleaseArrayNull() throws Exception { 120 | final Version v = Version.create(1, 2, 3, "pre-release", "build"); 121 | assertThrows(IllegalArgumentException.class, () -> v.nextPatch((String[]) null)); 122 | } 123 | 124 | @Test 125 | public void testIncrementPreReleaseEmpty() throws Exception { 126 | final Version v = Version.create(1, 2, 3, "", "build"); 127 | assertEquals(Version.create(1, 2, 3, "1"), v.nextPreRelease()); 128 | } 129 | 130 | @Test 131 | public void testIncrementPreReleaseNumeric() throws Exception { 132 | final Version v = Version.create(1, 2, 3, "pre-release.1", "build"); 133 | assertEquals(Version.create(1, 2, 3, "pre-release.2"), v.nextPreRelease()); 134 | } 135 | 136 | @Test 137 | public void testIncrementPreReleaseNonNumeric() throws Exception { 138 | final Version v = Version.create(1, 2, 3, "pre-release", "build"); 139 | assertEquals(Version.create(1, 2, 3, "pre-release.1"), v.nextPreRelease()); 140 | } 141 | 142 | @Test 143 | public void testIncrementBuildMDEmpty() throws Exception { 144 | final Version v = Version.create(1, 2, 3, "pre-release", ""); 145 | assertEquals(Version.create(1, 2, 3, "pre-release", "1"), 146 | v.nextBuildMetaData()); 147 | } 148 | 149 | @Test 150 | public void testIncrementBuildMDNumeric() throws Exception { 151 | final Version v = Version.create(1, 2, 3, "pre-release", "build.1"); 152 | assertEquals(Version.create(1, 2, 3, "pre-release", "build.2"), 153 | v.nextBuildMetaData()); 154 | } 155 | 156 | @Test 157 | public void testIncrementBuildMDNonNumeric() throws Exception { 158 | final Version v = Version.create(1, 2, 3, "pre-release", "build"); 159 | assertEquals(Version.create(1, 2, 3, "pre-release", "build.1"), 160 | v.nextBuildMetaData()); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/test/java/de/skuzzle/semantic/Java6CompatibilityTest.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.semantic; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.UncheckedIOException; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.nio.file.Paths; 11 | import java.nio.file.StandardOpenOption; 12 | import java.util.stream.Stream; 13 | 14 | import org.junit.jupiter.api.Test; 15 | 16 | public class Java6CompatibilityTest { 17 | 18 | @Test 19 | void testAllClassFilesAreCompatibleWithJava6() throws Exception { 20 | final int java6 = 0x32; 21 | allClassFiles() 22 | .filter(cf -> !isModuleInfo(cf)) 23 | .forEach(cf -> testClassFileForJavaVersion(cf, java6)); 24 | } 25 | 26 | @Test 27 | void testModuleInfoIsCompatibleWithJava9() throws Exception { 28 | final int java9 = 0x35; 29 | allClassFiles() 30 | .filter(this::isModuleInfo) 31 | .forEach(mi -> testClassFileForJavaVersion(mi, java9)); 32 | } 33 | 34 | private boolean isModuleInfo(Path path) { 35 | return path.getFileName().toString().equals("module-info.class"); 36 | } 37 | 38 | private Stream allClassFiles() throws IOException { 39 | final Path targetFolder = Paths.get("./target/classes"); 40 | return Files.find(targetFolder, Integer.MAX_VALUE, 41 | (path, bfa) -> path.getFileName().toString().endsWith(".class")); 42 | } 43 | 44 | private void testClassFileForJavaVersion(Path path, int expectedClassFileVersion) { 45 | final int[] expectedSequence = { 0xCA, 0xFE, 0xBA, 0xBE, 0, 0, 0, expectedClassFileVersion }; 46 | 47 | try (InputStream in = Files.newInputStream(path, StandardOpenOption.READ)) { 48 | for (int i = 0; i < expectedSequence.length; ++i) { 49 | final int expected = expectedSequence[i]; 50 | final int actual = in.read(); 51 | assertEquals(expected, actual, 52 | String.format("Class file %s: Expected byte %d at index %d but found %d", path, expected, i, 53 | actual)); 54 | } 55 | } catch (final IOException e) { 56 | throw new UncheckedIOException(e); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/de/skuzzle/semantic/ParsingTest.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.semantic; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertThrows; 6 | import static org.junit.jupiter.api.DynamicTest.dynamicTest; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collection; 10 | import java.util.List; 11 | 12 | import org.junit.jupiter.api.DynamicTest; 13 | import org.junit.jupiter.api.TestFactory; 14 | 15 | import de.skuzzle.semantic.Version.VersionFormatException; 16 | 17 | public class ParsingTest { 18 | 19 | private static final String INCOMPLETE_VERSION_PART = "Incomplete version part in %s"; 20 | 21 | private static String unexpectedChar(char c) { 22 | return "Unexpected char in %s: " + Character.toString(c); 23 | } 24 | 25 | private static final String[] LEGAL_VERSIONS = { 26 | "123.456.789", 27 | "0.1.0", 28 | "0.0.1", 29 | "0.0.1-0a", 30 | "0.0.1-0a+a0", 31 | "1.1.0-a", 32 | "1.1.0+a", 33 | "1.1.0+012", 34 | "1.1.0-112", 35 | "1.1.0-0", 36 | "1.1.0-0123a", 37 | "1.1.0-0123a+0012", 38 | }; 39 | 40 | private static final String[][] ILLEGAL_PRE_RELEASES = { 41 | { "01.1", "Illegal leading char '0' in pre-release part of %s" }, 42 | { "1.01", "Illegal leading char '0' in pre-release part of %s" }, 43 | { "pre.001", "Illegal leading char '0' in pre-release part of %s" }, 44 | { "pre.01", "Illegal leading char '0' in pre-release part of %s" }, 45 | { "pre..foo", INCOMPLETE_VERSION_PART }, 46 | { "$", unexpectedChar('$') } 47 | }; 48 | 49 | private static final String[][] ILLEGAL_VERSIONS = { 50 | { "1.", INCOMPLETE_VERSION_PART }, 51 | { "1.1.", INCOMPLETE_VERSION_PART }, 52 | { "1.0", INCOMPLETE_VERSION_PART }, 53 | { "1.2.3-pre.foo.", INCOMPLETE_VERSION_PART }, 54 | { "1", INCOMPLETE_VERSION_PART }, 55 | { "1$.1.0", unexpectedChar('$') }, 56 | { "1.1$.0", unexpectedChar('$') }, 57 | { "1.1.1$", unexpectedChar('$') }, 58 | { "$.1.1", unexpectedChar('$') }, 59 | { "1.$.1", unexpectedChar('$') }, 60 | { "1.1.$", unexpectedChar('$') }, 61 | { "0$.1.1", unexpectedChar('$') }, 62 | { "1.0$.1", unexpectedChar('$') }, 63 | { "1.1.0$", unexpectedChar('$') }, 64 | { "01.1.0", "Illegal leading char '0' in major part of %s" }, 65 | { "1.01.0", "Illegal leading char '0' in minor part of %s" }, 66 | { "1.1.01", "Illegal leading char '0' in patch part of %s" }, 67 | { "1.2.3-01.1", "Illegal leading char '0' in pre-release part of %s" }, 68 | { "1.2.3-1.01+abc", "Illegal leading char '0' in pre-release part of %s" }, 69 | { "1.2.3-1.01", "Illegal leading char '0' in pre-release part of %s" }, 70 | { "1.2.3-pre.001", "Illegal leading char '0' in pre-release part of %s" }, 71 | { "1.2.3-pre.01", "Illegal leading char '0' in pre-release part of %s" }, 72 | { "1.2.3-pre.01+a.b", "Illegal leading char '0' in pre-release part of %s" }, 73 | { "1.2.3-pre..foo", INCOMPLETE_VERSION_PART }, 74 | { "1.2.3+pre..foo", INCOMPLETE_VERSION_PART }, 75 | { "1.2.3+pre.foo.", INCOMPLETE_VERSION_PART }, 76 | { "1.2.3-$+foo", unexpectedChar('$') }, 77 | { "1.2.3+$", unexpectedChar('$') }, 78 | { "1.2.3-foo$+foo", unexpectedChar('$') }, 79 | { "1.2.3-1$+foo", unexpectedChar('$') }, 80 | { "1.2.3-1+1$", unexpectedChar('$') }, 81 | { "1.2.3-foo+foo$", unexpectedChar('$') }, 82 | { "1.2.3-1+1$", unexpectedChar('$') }, 83 | { "1.2.3-0$", unexpectedChar('$') }, 84 | { "1.2.3+0$", unexpectedChar('$') }, 85 | { "1.2.3-0123$", unexpectedChar('$') }, 86 | { "1.2.3-+", unexpectedChar('+') }, 87 | 88 | }; 89 | 90 | @TestFactory 91 | public Collection testParseWithException() { 92 | final List results = new ArrayList<>( 93 | ILLEGAL_VERSIONS.length); 94 | 95 | for (final String[] input : ILLEGAL_VERSIONS) { 96 | results.add(dynamicTest("Parse " + input[0], 97 | () -> { 98 | final VersionFormatException e = assertThrows( 99 | VersionFormatException.class, 100 | () -> Version.parseVersion(input[0])); 101 | 102 | final String expectedMessage = String.format(input[1], input[0]); 103 | assertEquals(expectedMessage, e.getMessage()); 104 | })); 105 | } 106 | 107 | return results; 108 | } 109 | 110 | @TestFactory 111 | public Collection testWithPreReleaseException() { 112 | final List results = new ArrayList<>( 113 | ILLEGAL_PRE_RELEASES.length); 114 | 115 | for (final String input[] : ILLEGAL_PRE_RELEASES) { 116 | results.add(dynamicTest("Pre release " + input[0], 117 | () -> { 118 | final VersionFormatException e = assertThrows( 119 | VersionFormatException.class, 120 | () -> Version.create(1, 2, 3).withPreRelease(input[0])); 121 | 122 | final String expectedMessage = String.format(input[1], input[0]); 123 | assertEquals(expectedMessage, e.getMessage()); 124 | })); 125 | } 126 | 127 | return results; 128 | } 129 | 130 | @TestFactory 131 | public Collection testWithPreReleaseArrayException() { 132 | final List results = new ArrayList<>( 133 | ILLEGAL_PRE_RELEASES.length); 134 | 135 | for (final String input[] : ILLEGAL_PRE_RELEASES) { 136 | results.add(dynamicTest("Pre release as array: " + input[0], 137 | () -> { 138 | final VersionFormatException e = assertThrows( 139 | VersionFormatException.class, 140 | () -> Version.create(1, 2, 3).withPreRelease( 141 | input[0].split("\\."))); 142 | 143 | final String expectedMessage = String.format(input[1], input[0]); 144 | assertEquals(expectedMessage, e.getMessage()); 145 | })); 146 | } 147 | 148 | return results; 149 | } 150 | 151 | @TestFactory 152 | public Collection testParseVerifyOnly() { 153 | final List results = new ArrayList<>(); 154 | 155 | for (final String[] input : ILLEGAL_VERSIONS) { 156 | results.add(dynamicTest(input[0], 157 | () -> assertFalse(Version.isValidVersion(input[0])))); 158 | } 159 | return results; 160 | } 161 | 162 | @TestFactory 163 | public Collection testParseLegalVersions() { 164 | final List results = new ArrayList<>(); 165 | 166 | for (final String input : LEGAL_VERSIONS) { 167 | final DynamicTest test = dynamicTest("Parse " + input, () -> { 168 | final Version parsed = Version.parseVersion(input); 169 | assertEquals(input, parsed.toString()); 170 | }); 171 | results.add(test); 172 | } 173 | return results; 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /src/test/java/de/skuzzle/semantic/VersionTest.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.semantic; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 4 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | import static org.junit.jupiter.api.Assertions.assertFalse; 7 | import static org.junit.jupiter.api.Assertions.assertSame; 8 | import static org.junit.jupiter.api.Assertions.assertThrows; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | import static org.junit.jupiter.api.Assertions.fail; 11 | 12 | import java.io.ByteArrayInputStream; 13 | import java.io.ByteArrayOutputStream; 14 | import java.io.FileOutputStream; 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.io.ObjectInputStream; 18 | import java.io.ObjectOutputStream; 19 | 20 | import org.junit.jupiter.api.Assertions; 21 | import org.junit.jupiter.api.Test; 22 | import org.junitpioneer.jupiter.DefaultLocale; 23 | 24 | import de.skuzzle.semantic.Version.VersionFormatException; 25 | 26 | public class VersionTest { 27 | 28 | private static final char[] ILLEGAL_CHAR_BOUNDS = { 'a' - 1, 'z' + 1, '0' - 1, 29 | '9' + 1, 'A' - 1, 'Z' + 1, '-' - 1 }; 30 | 31 | private static final char[] ILLEGAL_NUMERIC_BOUNDS = { '0' - 1, '9' + 1 }; 32 | 33 | private static final Version[] SEMVER_ORG_VERSIONS = new Version[] { 34 | Version.parseVersion("1.0.0-alpha"), 35 | Version.parseVersion("1.0.0-alpha.1"), 36 | Version.parseVersion("1.0.0-alpha.beta"), 37 | Version.parseVersion("1.0.0-beta"), 38 | Version.parseVersion("1.0.0-beta.2"), 39 | Version.parseVersion("1.0.0-beta.11"), 40 | Version.parseVersion("1.0.0-rc.1"), 41 | Version.parseVersion("1.0.0"), 42 | Version.parseVersion("2.0.0"), 43 | Version.parseVersion("2.1.0"), 44 | Version.parseVersion("2.1.1") 45 | }; 46 | 47 | // same as above, but exchanged pre release and build meta data 48 | private static final Version[] SEMVER_ORG_BMD_VERSIONS = new Version[] { 49 | Version.parseVersion("1.0.0-rc.1+alpha"), 50 | Version.parseVersion("1.0.0-rc.1+alpha.1"), 51 | Version.parseVersion("1.0.0-rc.1+alpha.beta"), 52 | Version.parseVersion("1.0.0-rc.1+beta"), 53 | Version.parseVersion("1.0.0-rc.1+beta.2"), 54 | Version.parseVersion("1.0.0-rc.1+beta.11"), 55 | Version.parseVersion("1.0.0-rc.1+rc.1"), 56 | Version.parseVersion("1.0.0-rc.1"), 57 | Version.parseVersion("2.0.0-rc.1"), 58 | Version.parseVersion("2.1.0-rc.1"), 59 | Version.parseVersion("2.1.1") 60 | }; 61 | 62 | private static final String[][] ILLEGAL_VERSIONS = { 63 | { "1.", "Incomplete version part in 1." }, 64 | { "1.1.", "Incomplete version part in 1.1." } 65 | }; 66 | 67 | public static void main(String[] args) throws IOException { 68 | new VersionTest().writeBinFile(); 69 | } 70 | 71 | public void writeBinFile() throws IOException { 72 | final FileOutputStream out = new FileOutputStream("versions.bin"); 73 | final ObjectOutputStream oout = new ObjectOutputStream(out); 74 | for (final Version v : SEMVER_ORG_VERSIONS) { 75 | oout.writeObject(v); 76 | } 77 | for (final Version v : SEMVER_ORG_BMD_VERSIONS) { 78 | oout.writeObject(v); 79 | } 80 | oout.close(); 81 | } 82 | 83 | @Test 84 | public void testIllegalVersions() throws Exception { 85 | for (final String[] input : ILLEGAL_VERSIONS) { 86 | try { 87 | Version.parseVersion(input[0]); 88 | fail("String '" + input[0] + "' should not be parsable"); 89 | } catch (final VersionFormatException e) { 90 | assertEquals(e.getMessage(), input[1], 91 | "Expected different exception message"); 92 | } 93 | } 94 | } 95 | 96 | @Test 97 | public void testPreReleaseEmptyString() { 98 | final Version v = Version.create(1, 1, 1, ""); 99 | assertEquals("", v.getPreRelease()); 100 | assertEquals("", v.getBuildMetaData()); 101 | } 102 | 103 | @Test 104 | public void testPreReleaseNull() { 105 | assertThrows(IllegalArgumentException.class, 106 | () -> Version.create(1, 1, 1, null)); 107 | } 108 | 109 | @Test 110 | public void testBuildMDNull() { 111 | assertThrows(IllegalArgumentException.class, 112 | () -> Version.create(1, 1, 1, "", null)); 113 | } 114 | 115 | @Test 116 | public void testNegativePatch() { 117 | assertThrows(IllegalArgumentException.class, 118 | () -> Version.create(1, 1, -1)); 119 | } 120 | 121 | @Test 122 | public void testNegativeMinor() { 123 | assertThrows(IllegalArgumentException.class, 124 | () -> Version.create(1, -1, 1)); 125 | } 126 | 127 | @Test 128 | public void testNegativeMajor() { 129 | assertThrows(IllegalArgumentException.class, 130 | () -> Version.create(-1, 1, 1)); 131 | } 132 | 133 | @Test 134 | public void parseVersionNull() { 135 | assertThrows(IllegalArgumentException.class, 136 | () -> Version.parseVersion(null)); 137 | } 138 | 139 | @Test 140 | public void testSimpleVersion() { 141 | final Version v = Version.parseVersion("1.2.3"); 142 | assertEquals(1, v.getMajor()); 143 | assertEquals(2, v.getMinor()); 144 | assertEquals(3, v.getPatch()); 145 | assertEquals("", v.getPreRelease()); 146 | assertEquals("", v.getBuildMetaData()); 147 | } 148 | 149 | @Test 150 | public void testSemVerOrgPreReleaseSamples() { 151 | final Version v1 = Version.parseVersion("1.0.0-alpha"); 152 | assertEquals("alpha", v1.getPreRelease()); 153 | 154 | final Version v2 = Version.parseVersion("1.0.0-alpha.1"); 155 | assertEquals("alpha.1", v2.getPreRelease()); 156 | 157 | final Version v3 = Version.parseVersion("1.0.0-0.3.7"); 158 | assertEquals("0.3.7", v3.getPreRelease()); 159 | 160 | final Version v4 = Version.parseVersion("1.0.0-x.7.z.92"); 161 | assertEquals("x.7.z.92", v4.getPreRelease()); 162 | } 163 | 164 | @Test 165 | public void testSemVerOrgBuildMDSamples() { 166 | final Version v1 = Version.parseVersion("1.0.0-alpha+001"); 167 | assertEquals("alpha", v1.getPreRelease()); 168 | assertEquals("001", v1.getBuildMetaData()); 169 | 170 | final Version v2 = Version.parseVersion("1.0.0+20130313144700"); 171 | assertEquals("20130313144700", v2.getBuildMetaData()); 172 | 173 | final Version v3 = Version.parseVersion("1.0.0-beta+exp.sha.5114f85"); 174 | assertEquals("beta", v3.getPreRelease()); 175 | assertEquals("exp.sha.5114f85", v3.getBuildMetaData()); 176 | } 177 | 178 | @Test 179 | public void testVersionWithBuildMD() { 180 | final Version v = Version.parseVersion("1.2.3+some.id.foo"); 181 | assertEquals("some.id.foo", v.getBuildMetaData()); 182 | } 183 | 184 | @Test 185 | public void testVersionWithBuildMD2() { 186 | final Version v = Version.create(1, 2, 3, "", "some.id-1.foo"); 187 | assertEquals("some.id-1.foo", v.getBuildMetaData()); 188 | } 189 | 190 | @Test 191 | public void testParseMajorIsZero() throws Exception { 192 | final Version version = Version.parseVersion("0.1.2"); 193 | assertEquals(0, version.getMajor()); 194 | } 195 | 196 | @Test 197 | public void testVersionWithBuildMDEmptyLastPart() { 198 | assertThrows(VersionFormatException.class, 199 | () -> Version.create(1, 2, 3, "", "some.id.")); 200 | } 201 | 202 | @Test 203 | public void testVersionWithBuildMDEmptyMiddlePart() { 204 | assertThrows(VersionFormatException.class, 205 | () -> Version.create(1, 2, 3, "", "some..id")); 206 | } 207 | 208 | @Test 209 | public void testParseBuildMDWithLeadingZeroInIdentifierPart() throws Exception { 210 | final Version v = Version.parseVersion("1.2.3+0abc"); 211 | assertEquals("0abc", v.getBuildMetaData()); 212 | } 213 | 214 | @Test 215 | public void testParsePreReleaseLastPartIsNumeric() throws Exception { 216 | final Version v = Version.parseVersion("1.2.3-a.11+buildmd"); 217 | assertEquals("a.11", v.getPreRelease()); 218 | assertEquals("buildmd", v.getBuildMetaData()); 219 | } 220 | 221 | @Test 222 | public void testVersionWithPreRelease() { 223 | final Version v = Version.parseVersion("1.2.3-pre.release-foo.1"); 224 | assertEquals("pre.release-foo.1", v.getPreRelease()); 225 | final String[] expected = { "pre", "release-foo", "1" }; 226 | assertArrayEquals(expected, v.getPreReleaseParts()); 227 | } 228 | 229 | @Test 230 | public void testVersionWithPreReleaseAndBuildMD() { 231 | final Version v = Version 232 | .parseVersion("1.2.3-pre.release-foo.1+some.id-with-hyphen"); 233 | assertEquals("pre.release-foo.1", v.getPreRelease()); 234 | assertEquals("some.id-with-hyphen", v.getBuildMetaData()); 235 | } 236 | 237 | @Test 238 | public void testIsValidVersionLeadingZeroMinor() throws Exception { 239 | assertFalse(Version.isValidVersion("1.01.1")); 240 | } 241 | 242 | private void shouldNotBeParseable(String template, char c) { 243 | final String v = String.format(template, c); 244 | 245 | try { 246 | Version.parseVersion(v); 247 | fail("Version " + v + " should not be parsable"); 248 | } catch (final VersionFormatException e) { 249 | 250 | } 251 | } 252 | 253 | @Test 254 | public void testIllegalCharNumericParts() throws Exception { 255 | for (final char c : ILLEGAL_NUMERIC_BOUNDS) { 256 | shouldNotBeParseable("%c.2.3", c); 257 | shouldNotBeParseable("1.%c.3", c); 258 | shouldNotBeParseable("1.2.%c", c); 259 | } 260 | } 261 | 262 | @Test 263 | public void testPreReleaseInvalidChar1() throws Exception { 264 | for (final char c : ILLEGAL_CHAR_BOUNDS) { 265 | shouldNotBeParseable("1.0.0-%c", c); 266 | } 267 | } 268 | 269 | @Test 270 | public void testPreReleaseInvalidChar2() throws Exception { 271 | for (final char c : ILLEGAL_CHAR_BOUNDS) { 272 | shouldNotBeParseable("1.0.0-1.a%c", c); 273 | } 274 | } 275 | 276 | @Test 277 | public void testPreReleaseInvalidChar31() throws Exception { 278 | for (final char c : ILLEGAL_CHAR_BOUNDS) { 279 | shouldNotBeParseable("1.0.0-01a%c", c); 280 | } 281 | } 282 | 283 | @Test 284 | public void testBuildMDInvalidChar1() throws Exception { 285 | for (final char c : ILLEGAL_CHAR_BOUNDS) { 286 | shouldNotBeParseable("1.0.0+%c", c); 287 | } 288 | } 289 | 290 | @Test 291 | public void testBuildMDInvalidChar2() throws Exception { 292 | for (final char c : ILLEGAL_CHAR_BOUNDS) { 293 | shouldNotBeParseable("1.0.0+1.a%c", c); 294 | } 295 | } 296 | 297 | @Test 298 | public void testBuildMetaDataHyphenOnly() throws Exception { 299 | final Version v = Version.parseVersion("1.2.3+-"); 300 | assertEquals("-", v.getBuildMetaData()); 301 | } 302 | 303 | @Test 304 | public void testPreReleaseHyphenOnly() throws Exception { 305 | final Version v = Version.parseVersion("1.2.3--"); 306 | assertEquals("-", v.getPreRelease()); 307 | } 308 | 309 | @Test 310 | public void testParseMajorUnexpectedChar() throws Exception { 311 | assertThrows(VersionFormatException.class, 312 | () -> Version.parseVersion("1$.0.0")); 313 | } 314 | 315 | @Test 316 | public void testParsePatchUnexpectedChar() throws Exception { 317 | assertThrows(VersionFormatException.class, 318 | () -> Version.parseVersion("1.0.1$")); 319 | } 320 | 321 | @Test 322 | public void testParseLeadingZeroMinor() throws Exception { 323 | assertThrows(VersionFormatException.class, 324 | () -> Version.parseVersion("1.01.1")); 325 | } 326 | 327 | @Test 328 | public void testIsValidVersionLeadingZeroPatch() throws Exception { 329 | assertFalse(Version.isValidVersion("1.1.01")); 330 | } 331 | 332 | @Test 333 | public void testParseLeadingZeroPatch() throws Exception { 334 | assertThrows(VersionFormatException.class, 335 | () -> Version.parseVersion("1.1.01")); 336 | } 337 | 338 | @Test 339 | public void testIsValidVersionLeadingZeroMajor() throws Exception { 340 | assertFalse(Version.isValidVersion("01.1.1")); 341 | } 342 | 343 | @Test 344 | public void testParseMissingPart() throws Exception { 345 | assertThrows(VersionFormatException.class, 346 | () -> Version.parseVersion("1.0")); 347 | } 348 | 349 | @Test 350 | public void testIsValidVersionMissingPart() throws Exception { 351 | assertFalse(Version.isValidVersion("1.1")); 352 | } 353 | 354 | @Test 355 | public void testParsePrematureStop() throws Exception { 356 | assertThrows(VersionFormatException.class, 357 | () -> Version.parseVersion("1.")); 358 | } 359 | 360 | @Test 361 | public void testIsValidVersionPrematureStop() throws Exception { 362 | assertFalse(Version.isValidVersion("1.")); 363 | } 364 | 365 | @Test 366 | public void testParseMajorLeadingZero() throws Exception { 367 | assertThrows(VersionFormatException.class, 368 | () -> Version.parseVersion("01.0.0")); 369 | } 370 | 371 | @Test 372 | public void testPreReleaseWithLeadingZeroes() { 373 | assertThrows(VersionFormatException.class, 374 | () -> Version.parseVersion("1.2.3-pre.001")); 375 | } 376 | 377 | @Test 378 | public void testPreReleaseWithLeadingZeroes2() { 379 | assertThrows(VersionFormatException.class, 380 | () -> Version.create(1, 2, 3, "pre.001")); 381 | } 382 | 383 | @Test 384 | public void testPreReleaseWithLeadingZeroEOS() { 385 | assertThrows(VersionFormatException.class, 386 | () -> Version.parseVersion("1.2.3-pre.01")); 387 | } 388 | 389 | @Test 390 | public void testPreReleaseWithLeadingZeroEOS2() { 391 | assertThrows(VersionFormatException.class, 392 | () -> Version.create(1, 2, 3, "pre.01")); 393 | } 394 | 395 | @Test 396 | public void testPreReleaseWithLeadingZeroAndBuildMD() { 397 | assertThrows(VersionFormatException.class, 398 | () -> Version.parseVersion("1.2.3-pre.01+a.b")); 399 | } 400 | 401 | @Test 402 | public void testPreReleaseMiddleEmptyIdentifier() { 403 | assertThrows(VersionFormatException.class, 404 | () -> Version.parseVersion("1.2.3-pre..foo")); 405 | } 406 | 407 | @Test 408 | public void testPreReleaseLastEmptyIdentifier() { 409 | assertThrows(VersionFormatException.class, 410 | () -> Version.parseVersion("1.2.3-pre.foo.")); 411 | } 412 | 413 | @Test 414 | public void testBuildMDMiddleEmptyIdentifier() { 415 | assertThrows(VersionFormatException.class, 416 | () -> Version.parseVersion("1.2.3+pre..foo")); 417 | } 418 | 419 | @Test 420 | public void testBuildMDLastEmptyIdentifier() { 421 | assertThrows(VersionFormatException.class, 422 | () -> Version.parseVersion("1.2.3+pre.foo.")); 423 | } 424 | 425 | @Test 426 | public void testParseExpectNoPrelease() { 427 | assertThrows(VersionFormatException.class, 428 | () -> Version.parseVersion("1.2.3-foo", false)); 429 | } 430 | 431 | @Test 432 | public void testParseExpectNoBuildMetaData() { 433 | assertThrows(VersionFormatException.class, 434 | () -> Version.parseVersion("1.2.3+foo", false)); 435 | } 436 | 437 | @Test 438 | public void testParseExpectNoPreReleaseAndBuildMetaData() { 439 | assertThrows(VersionFormatException.class, 440 | () -> Version.parseVersion("1.2.3-foo+foo", false)); 441 | } 442 | 443 | @Test 444 | public void testParseVersionIllegalCharInPreReleaseOnly() throws Exception { 445 | assertThrows(VersionFormatException.class, 446 | () -> Version.parseVersion("1.2.3-$+foo")); 447 | } 448 | 449 | @Test 450 | public void testParseVersionIllegalCharBuildMDOnly() throws Exception { 451 | assertThrows(VersionFormatException.class, 452 | () -> Version.parseVersion("1.2.3+$")); 453 | } 454 | 455 | @Test 456 | public void testParseVersionIllegalCharInPreRelease() throws Exception { 457 | assertThrows(VersionFormatException.class, 458 | () -> Version.parseVersion("1.2.3-foo$+foo")); 459 | } 460 | 461 | @Test 462 | public void testParseVersionIllegalCharInPreReleaseNumericPart() throws Exception { 463 | assertThrows(VersionFormatException.class, 464 | () -> Version.parseVersion("1.2.3-1$+foo")); 465 | } 466 | 467 | @Test 468 | public void testParseVersionIllegalCharInBuildMDNumericPart() throws Exception { 469 | assertThrows(VersionFormatException.class, 470 | () -> Version.parseVersion("1.2.3-1+1$")); 471 | } 472 | 473 | @Test 474 | public void testParseVersionIllegalCharInBuildMD() throws Exception { 475 | assertThrows(VersionFormatException.class, 476 | () -> Version.parseVersion("1.2.3-foo+foo$")); 477 | } 478 | 479 | @Test 480 | public void testParseVersionPreReleaseSingleZero() throws Exception { 481 | Version.parseVersion("1.2.3-0.1.0"); 482 | } 483 | 484 | @Test 485 | public void testParseVersionPreReleaseAndBuildMDSingleZero() throws Exception { 486 | Version.parseVersion("1.2.3-0.1.0+0.0.0.1"); 487 | } 488 | 489 | @Test 490 | public void testParsePreReleaseIllegalLeadingZero() throws Exception { 491 | assertThrows(VersionFormatException.class, 492 | () -> Version.parseVersion("1.2.3-01.1")); 493 | } 494 | 495 | @Test 496 | public void testParsePreReleaseIllegalLeadingZeroBeforeBuildMD() throws Exception { 497 | assertThrows(VersionFormatException.class, 498 | () -> Version.parseVersion("1.2.3-1.01+abc")); 499 | } 500 | 501 | @Test 502 | public void testParsePreReleaseIllegalLeadingZeroInLastPart() throws Exception { 503 | assertThrows(VersionFormatException.class, 504 | () -> Version.parseVersion("1.2.3-1.01")); 505 | } 506 | 507 | @Test 508 | public void testParseVersionSuccessExpectNoPreRelease() { 509 | Version.parseVersion("1.2.3", false); 510 | } 511 | 512 | @Test 513 | public void testParseVersionSuccess() { 514 | final Version version = Version.parseVersion("1.2.3-foo+bar", true); 515 | assertEquals("foo", version.getPreRelease()); 516 | assertEquals("bar", version.getBuildMetaData()); 517 | } 518 | 519 | @Test 520 | public void testPreReleaseLastEmptyIdentifier2() { 521 | assertThrows(VersionFormatException.class, 522 | () -> Version.create(1, 2, 3, "pre.foo.")); 523 | } 524 | 525 | @Test 526 | public void testVersionAll0() { 527 | assertDoesNotThrow(() -> Version.parseVersion("0.0.0")); 528 | } 529 | 530 | @Test 531 | public void testVersionAll02() { 532 | assertDoesNotThrow(() -> Version.create(0, 0, 0)); 533 | } 534 | 535 | @Test 536 | public void testVersionAll03() { 537 | assertDoesNotThrow(() -> Version.create(0, 0)); 538 | } 539 | 540 | @Test 541 | public void testVersionAll04() { 542 | assertDoesNotThrow(() -> Version.create(0)); 543 | } 544 | 545 | @Test 546 | public void testPreReleaseInvalid() { 547 | assertThrows(VersionFormatException.class, 548 | () -> Version.create(1, 2, 3, "pre.", "build")); 549 | } 550 | 551 | @Test 552 | public void testPreReleaseNullAndBuildMDGiven() { 553 | assertThrows(IllegalArgumentException.class, 554 | () -> Version.create(1, 2, 3, null, "build")); 555 | } 556 | 557 | @Test 558 | public void testOnlyBuildMdEmpty() { 559 | Version.create(1, 2, 3, "pre", ""); 560 | } 561 | 562 | @Test 563 | public void testPreReleaseWithLeadingZeroesIdentifier() { 564 | // leading zeroes allowed in string identifiers 565 | final Version v = Version.parseVersion("1.2.3-001abc"); 566 | assertEquals("001abc", v.getPreRelease()); 567 | } 568 | 569 | @Test 570 | public void testPreReleaseWithLeadingZeroesIdentifier2() { 571 | // leading zeroes allowed in string identifiers 572 | final Version v = Version.create(1, 2, 3, "001abc"); 573 | assertEquals("001abc", v.getPreRelease()); 574 | } 575 | 576 | @Test 577 | public void testNoPrecedenceChangeByBuildMD() { 578 | final Version v1 = Version.parseVersion("1.2.3+1.0"); 579 | final Version v2 = Version.parseVersion("1.2.3+2.0"); 580 | assertEquals(0, v1.compareTo(v2)); 581 | } 582 | 583 | @Test 584 | public void testSimplePrecedence() { 585 | final Version v1 = Version.parseVersion("1.0.0"); 586 | final Version v2 = Version.parseVersion("1.0.1"); 587 | final Version v3 = Version.parseVersion("1.1.0"); 588 | final Version v4 = Version.parseVersion("2.0.0"); 589 | 590 | assertTrue(v1.compareTo(v2) < 0); 591 | assertTrue(v2.compareTo(v3) < 0); 592 | assertTrue(v3.compareTo(v4) < 0); 593 | assertTrue(v2.compareTo(v1) > 0); 594 | assertTrue(v3.compareTo(v2) > 0); 595 | assertTrue(v4.compareTo(v3) > 0); 596 | assertTrue(v4.isGreaterThanOrEqualTo(v3)); 597 | assertTrue(v3.isLowerThanOrEqualTo(v4)); 598 | } 599 | 600 | @Test 601 | public void testPrecedencePreRelease() { 602 | final Version v1 = Version.parseVersion("1.0.0"); 603 | final Version v2 = Version.parseVersion("1.0.0-rc1"); 604 | assertTrue(v1.compareTo(v2) > 0); 605 | assertTrue(v2.compareTo(v1) < 0); 606 | assertTrue(v2.isLowerThan(v1)); 607 | assertTrue(v1.isGreaterThan(v2)); 608 | assertTrue(v1.isGreaterThanOrEqualTo(v2)); 609 | assertTrue(v2.isLowerThanOrEqualTo(v1)); 610 | } 611 | 612 | @Test 613 | public void testPrecedencePreRelease2() { 614 | final Version v1 = Version.parseVersion("1.0.0-rc1"); 615 | final Version v2 = Version.parseVersion("1.0.0-rc1"); 616 | assertTrue(v1.compareTo(v2) == 0); 617 | } 618 | 619 | @Test 620 | public void testPrecedencePreRelease3() { 621 | final Version v1 = Version.parseVersion("1.0.0-rc1"); 622 | final Version v2 = Version.parseVersion("1.0.0-rc1.5"); 623 | // the one with longer list is greater 624 | assertTrue(v1.compareTo(v2) < 0); 625 | assertTrue(v2.compareTo(v1) > 0); 626 | } 627 | 628 | @Test 629 | public void testPrecedencePreRelease4() { 630 | final Version v1 = Version.parseVersion("1.0.0-a"); 631 | final Version v2 = Version.parseVersion("1.0.0-b"); 632 | assertTrue(v1.compareTo(v2) < 0); 633 | assertTrue(v2.compareTo(v1) > 0); 634 | } 635 | 636 | @Test 637 | public void testPrecedencePreRelease5() { 638 | final Version v1 = Version.parseVersion("1.0.0-1"); 639 | final Version v2 = Version.parseVersion("1.0.0-2"); 640 | assertTrue(v1.compareTo(v2) < 0); 641 | assertTrue(v2.compareTo(v1) > 0); 642 | } 643 | 644 | @Test 645 | public void testPrecedencePreRelease6() { 646 | final Version v1 = Version.parseVersion("1.0.0-1.some.id-with-hyphen.a"); 647 | final Version v2 = Version.parseVersion("1.0.0-1.some.id-with-hyphen.b"); 648 | assertTrue(v1.compareTo(v2) < 0); 649 | assertTrue(v2.compareTo(v1) > 0); 650 | } 651 | 652 | @Test 653 | public void testIsGreaterNull() throws Exception { 654 | assertThrows(IllegalArgumentException.class, 655 | () -> Version.create(1, 0, 0).isGreaterThan(null)); 656 | } 657 | 658 | @Test 659 | public void testIsLowerNull() throws Exception { 660 | assertThrows(IllegalArgumentException.class, 661 | () -> Version.create(1, 0, 0).isLowerThan(null)); 662 | } 663 | 664 | @Test 665 | public void testIsGreaterThanOrEqualToNull() throws Exception { 666 | assertThrows(IllegalArgumentException.class, 667 | () -> Version.create(1, 0, 0).isGreaterThanOrEqualTo(null)); 668 | } 669 | 670 | @Test 671 | public void testIsLowerThanOrEqualToNull() throws Exception { 672 | assertThrows(IllegalArgumentException.class, 673 | () -> Version.create(1, 0, 0).isLowerThanOrEqualTo(null)); 674 | } 675 | 676 | @Test 677 | public void testInitialDevelopment() { 678 | final Version v1 = Version.create(0, 1, 0); 679 | final Version v2 = Version.create(1, 1, 0); 680 | assertTrue(v1.isInitialDevelopment()); 681 | assertFalse(v2.isInitialDevelopment()); 682 | } 683 | 684 | @Test 685 | public void testSemVerOrgPrecedenceSample() { 686 | for (int i = 1; i < SEMVER_ORG_VERSIONS.length; ++i) { 687 | final Version v1 = SEMVER_ORG_VERSIONS[i - 1]; 688 | final Version v2 = SEMVER_ORG_VERSIONS[i]; 689 | final int c = v1.compareTo(v2); 690 | assertTrue(c < 0, v1 + " is not lower than " + v2); 691 | assertTrue(v1.isLowerThan(v2)); 692 | assertTrue(v1.isLowerThanOrEqualTo(v2)); 693 | assertFalse(v1.isGreaterThan(v2)); 694 | assertFalse(v1.isGreaterThanOrEqualTo(v2)); 695 | } 696 | } 697 | 698 | @Test 699 | public void testSemVerOrgPrecedenceSampleComparator() { 700 | for (int i = 1; i < SEMVER_ORG_VERSIONS.length; ++i) { 701 | final Version v1 = SEMVER_ORG_VERSIONS[i - 1]; 702 | final Version v2 = SEMVER_ORG_VERSIONS[i]; 703 | final int c = Version.NATURAL_ORDER.compare(v1, v2); 704 | assertTrue(c < 0, v1 + " is not lower than " + v2); 705 | } 706 | } 707 | 708 | @Test 709 | public void testBuildMetaDataEquality() { 710 | final Version v1 = Version.create(0, 0, 1, "", "some.build-meta.data"); 711 | final Version v2 = Version.create(0, 0, 1, "", "some.different.build-meta.data"); 712 | assertFalse(v1.equalsWithBuildMetaData(v2)); 713 | } 714 | 715 | @Test 716 | public void testBuildMDPrecedence() { 717 | for (int i = 1; i < SEMVER_ORG_BMD_VERSIONS.length; ++i) { 718 | final Version v1 = SEMVER_ORG_BMD_VERSIONS[i - 1]; 719 | final Version v2 = SEMVER_ORG_BMD_VERSIONS[i]; 720 | final int c = v1.compareToWithBuildMetaData(v2); 721 | assertTrue(c < 0, v1 + " is not lower than " + v2); 722 | } 723 | } 724 | 725 | @Test 726 | public void testBuildMDPrecedenceComparator() { 727 | for (int i = 1; i < SEMVER_ORG_BMD_VERSIONS.length; ++i) { 728 | final Version v1 = SEMVER_ORG_BMD_VERSIONS[i - 1]; 729 | final Version v2 = SEMVER_ORG_BMD_VERSIONS[i]; 730 | final int c = Version.WITH_BUILD_META_DATA_ORDER.compare(v1, v2); 731 | assertTrue(c < 0, v1 + " is not lower than " + v2); 732 | } 733 | } 734 | 735 | @Test 736 | public void testBuildMDPrecedenceReverse() { 737 | for (int i = 1; i < SEMVER_ORG_BMD_VERSIONS.length; ++i) { 738 | final Version v1 = SEMVER_ORG_BMD_VERSIONS[i - 1]; 739 | final Version v2 = SEMVER_ORG_BMD_VERSIONS[i]; 740 | final int c = v2.compareToWithBuildMetaData(v1); 741 | assertTrue(c > 0, v2 + " is not greater than " + v1); 742 | } 743 | } 744 | 745 | @Test 746 | public void testPreReleaseEquality() throws Exception { 747 | for (final Version version : SEMVER_ORG_VERSIONS) { 748 | final Version copy = Version.create(version.getMajor(), version.getMinor(), 749 | version.getPatch(), version.getPreRelease(), 750 | version.getBuildMetaData()); 751 | assertEquals(version, copy); 752 | assertTrue(version.equalsWithBuildMetaData(copy)); 753 | assertTrue(version.compareTo(copy) == 0); 754 | assertTrue(version.compareToWithBuildMetaData(copy) == 0); 755 | assertEquals(version.hashCode(), copy.hashCode()); 756 | } 757 | } 758 | 759 | @Test 760 | public void testBuildMDEquality() throws Exception { 761 | for (final Version version : SEMVER_ORG_BMD_VERSIONS) { 762 | final Version copy = Version.create(version.getMajor(), version.getMinor(), 763 | version.getPatch(), version.getPreRelease(), 764 | version.getBuildMetaData()); 765 | assertEquals(version, copy); 766 | assertTrue(version.equalsWithBuildMetaData(copy)); 767 | assertTrue(version.compareTo(copy) == 0); 768 | assertTrue(version.compareToWithBuildMetaData(copy) == 0); 769 | assertEquals(version.hashCode(), copy.hashCode()); 770 | } 771 | } 772 | 773 | @Test 774 | public void testCompareWithBuildMDNull1() throws Exception { 775 | assertThrows(NullPointerException.class, 776 | () -> Version.compareWithBuildMetaData(null, Version.create(1, 0, 0))); 777 | } 778 | 779 | @Test 780 | public void testCompareWithBuildMDNull2() throws Exception { 781 | assertThrows(NullPointerException.class, 782 | () -> Version.compareWithBuildMetaData(Version.create(1, 0, 0), null)); 783 | } 784 | 785 | @Test 786 | public void testCompareNull1() { 787 | assertThrows(NullPointerException.class, 788 | () -> Version.compare(null, Version.create(1, 1, 1))); 789 | } 790 | 791 | @Test 792 | public void testCompareNull2() { 793 | assertThrows(NullPointerException.class, 794 | () -> Version.compare(Version.create(1, 1, 1), null)); 795 | } 796 | 797 | @Test 798 | public void testCompareIdentical() { 799 | final Version v = Version.create(1, 1, 1); 800 | assertEquals(0, Version.compare(v, v)); 801 | } 802 | 803 | @Test 804 | public void testNotEqualsNull() { 805 | final Version v = Version.create(1, 1, 1); 806 | assertFalse(v.equals(null)); 807 | } 808 | 809 | @Test 810 | public void testNotEqualsForeign() { 811 | final Version v = Version.create(1, 1, 1); 812 | assertFalse(v.equals(new Object())); 813 | } 814 | 815 | @Test 816 | public void testEqualsIdentity() { 817 | final Version v = Version.create(1, 2, 3); 818 | assertEquals(v, v); 819 | } 820 | 821 | @Test 822 | public void testNotEqualsTrivial() { 823 | final Version v1 = Version.create(1, 1, 1); 824 | final Version v2 = Version.create(1, 1, 2); 825 | assertFalse(v1.equals(v2)); 826 | } 827 | 828 | @Test 829 | public void testParseToString() { 830 | for (final Version v1 : SEMVER_ORG_VERSIONS) { 831 | final Version v2 = Version.parseVersion(v1.toString()); 832 | assertEquals(v1, v2); 833 | assertEquals(v1.hashCode(), v2.hashCode()); 834 | } 835 | } 836 | 837 | @Test 838 | public void testParseToStringUpperCase() { 839 | for (final Version v1 : SEMVER_ORG_VERSIONS) { 840 | final Version v2 = Version.parseVersion(v1.toString().toUpperCase()); 841 | assertEquals(v1.toUpperCase(), v2); 842 | assertEquals(v1.toUpperCase().hashCode(), v2.hashCode()); 843 | } 844 | } 845 | 846 | @Test 847 | public void testParseToStringLowerCase() { 848 | for (final Version v1 : SEMVER_ORG_VERSIONS) { 849 | final Version v2 = Version.parseVersion(v1.toString().toLowerCase()); 850 | assertEquals(v1.toLowerCase(), v2); 851 | assertEquals(v1.toLowerCase().hashCode(), v2.hashCode()); 852 | } 853 | } 854 | 855 | @Test 856 | public void testMin() throws Exception { 857 | final Version v1 = Version.create(1, 0, 0); 858 | final Version v2 = Version.create(0, 1, 0); 859 | 860 | assertSame(Version.min(v1, v2), Version.min(v2, v1)); 861 | assertSame(v2, Version.min(v1, v2)); 862 | assertSame(v2, v2.min(v1)); 863 | } 864 | 865 | @Test 866 | public void testMinEquals() throws Exception { 867 | final Version v1 = Version.create(1, 0, 0); 868 | final Version v2 = Version.create(1, 0, 0); 869 | 870 | final Version min = Version.min(v1, v2); 871 | assertSame(v1, min); 872 | assertSame(v1, v1.min(v2)); 873 | } 874 | 875 | @Test 876 | public void testMinNullV1() throws Exception { 877 | assertThrows(IllegalArgumentException.class, 878 | () -> Version.min(null, Version.create(1, 0, 0))); 879 | } 880 | 881 | @Test 882 | public void testMinNullV2() throws Exception { 883 | assertThrows(IllegalArgumentException.class, 884 | () -> Version.min(Version.create(1, 0, 0), null)); 885 | } 886 | 887 | @Test 888 | public void testMax() throws Exception { 889 | final Version v1 = Version.create(1, 0, 0); 890 | final Version v2 = Version.create(0, 1, 0); 891 | 892 | assertSame(Version.max(v1, v2), Version.max(v2, v1)); 893 | assertSame(v1, Version.max(v1, v2)); 894 | assertSame(v1, v1.max(v2)); 895 | } 896 | 897 | @Test 898 | public void testMaxEquals() throws Exception { 899 | final Version v1 = Version.create(1, 0, 0); 900 | final Version v2 = Version.create(1, 0, 0); 901 | 902 | final Version max = Version.max(v1, v2); 903 | assertSame(v1, max); 904 | assertSame(v1, v1.max(v2)); 905 | } 906 | 907 | @Test 908 | public void testMaxNullV1() throws Exception { 909 | assertThrows(IllegalArgumentException.class, 910 | () -> Version.max(null, Version.create(1, 0, 0))); 911 | } 912 | 913 | @Test 914 | public void testMaxNullV2() throws Exception { 915 | assertThrows(IllegalArgumentException.class, 916 | () -> Version.max(Version.create(1, 0, 0), null)); 917 | } 918 | 919 | @Test 920 | public void testSamePrereleaseAndWithBuildMD() throws Exception { 921 | final Version v1 = Version.parseVersion("1.0.0-a.b+a"); 922 | final Version v2 = Version.parseVersion("1.0.0-a.b+b"); 923 | 924 | assertTrue(v1.compareToWithBuildMetaData(v2) < 0); 925 | } 926 | 927 | @Test 928 | public void testIsNoPreReleaseIdentifierNull() throws Exception { 929 | assertFalse(Version.isValidPreRelease(null)); 930 | } 931 | 932 | @Test 933 | public void testIsPreReleaseIdentifierEmptyString() throws Exception { 934 | assertTrue(Version.isValidPreRelease("")); 935 | } 936 | 937 | @Test 938 | public void testIsValidPreReleaseIdentifier() throws Exception { 939 | for (final Version v : SEMVER_ORG_VERSIONS) { 940 | assertTrue(Version.isValidPreRelease(v.getPreRelease()), 941 | v.getPreRelease() + " should be a valid identifier"); 942 | } 943 | assertTrue(Version.isValidPreRelease("-")); 944 | } 945 | 946 | @Test 947 | public void testIsNotValidPreReleaseIdentifier() throws Exception { 948 | assertFalse(Version.isValidPreRelease("a+b")); 949 | } 950 | 951 | @Test 952 | public void testIsNotValidPreReleaseNumericIdentifier() throws Exception { 953 | assertFalse(Version.isValidPreRelease("123+b")); 954 | } 955 | 956 | @Test 957 | public void testIsNotValidBuildMDIdentifier() throws Exception { 958 | assertFalse(Version.isValidBuildMetaData("a+b")); 959 | } 960 | 961 | @Test 962 | public void testIsNotValidBuildMDNumericIdentifier() throws Exception { 963 | assertFalse(Version.isValidBuildMetaData("123+b")); 964 | } 965 | 966 | @Test 967 | public void testIsNoBuildMDIdentifierNull() throws Exception { 968 | assertFalse(Version.isValidBuildMetaData(null)); 969 | } 970 | 971 | @Test 972 | public void testIsBuildMDIdentifierEmptyString() throws Exception { 973 | assertTrue(Version.isValidBuildMetaData("")); 974 | } 975 | 976 | @Test 977 | public void testIsValidBuildMDIdentifier() throws Exception { 978 | for (final Version v : SEMVER_ORG_BMD_VERSIONS) { 979 | assertTrue(Version.isValidBuildMetaData(v.getBuildMetaData()), v.toString()); 980 | assertTrue(Version.isValidBuildMetaData(v.getPreRelease()), v.toString()); 981 | } 982 | assertTrue(Version.isValidBuildMetaData("-")); 983 | } 984 | 985 | @Test 986 | public void testNullIsNoVersion() throws Exception { 987 | assertFalse(Version.isValidVersion(null)); 988 | } 989 | 990 | @Test 991 | public void testEmptyStringIsNoVersion() throws Exception { 992 | assertFalse(Version.isValidVersion("")); 993 | } 994 | 995 | @Test 996 | public void testIsValidVersion() throws Exception { 997 | for (final Version v : SEMVER_ORG_VERSIONS) { 998 | assertTrue(Version.isValidVersion(v.toString())); 999 | } 1000 | } 1001 | 1002 | @Test 1003 | public void testSerialize() throws Exception { 1004 | final ByteArrayOutputStream bout = new ByteArrayOutputStream(); 1005 | final ObjectOutputStream out = new ObjectOutputStream(bout); 1006 | for (final Version v : SEMVER_ORG_VERSIONS) { 1007 | out.writeObject(v); 1008 | } 1009 | out.close(); 1010 | final InputStream bin = new ByteArrayInputStream(bout.toByteArray()); 1011 | final ObjectInputStream in = new ObjectInputStream(bin); 1012 | for (final Version v : SEMVER_ORG_VERSIONS) { 1013 | assertEquals(v, in.readObject()); 1014 | } 1015 | in.close(); 1016 | } 1017 | 1018 | @Test 1019 | public void testDeserialize05() throws Exception { 1020 | // Deserialize a file which has been written by version 0.6.0 1021 | final ClassLoader cl = getClass().getClassLoader(); 1022 | final InputStream inp = cl.getResourceAsStream("versions_0.6.bin"); 1023 | final ObjectInputStream oin = new ObjectInputStream(inp); 1024 | for (final Version v : SEMVER_ORG_VERSIONS) { 1025 | assertEquals(v, oin.readObject()); 1026 | } 1027 | 1028 | for (final Version v : SEMVER_ORG_BMD_VERSIONS) { 1029 | assertEquals(v, oin.readObject()); 1030 | } 1031 | oin.close(); 1032 | } 1033 | 1034 | @Test 1035 | public void testEmptyArrayPreRelease() throws Exception { 1036 | final Version v = Version.parseVersion("1.0.0"); 1037 | assertEquals(0, v.getPreReleaseParts().length); 1038 | } 1039 | 1040 | @Test 1041 | public void testEmptyArrayBuildMetaData() throws Exception { 1042 | final Version v = Version.parseVersion("1.0.0"); 1043 | assertEquals(0, v.getBuildMetaDataParts().length); 1044 | } 1045 | 1046 | @Test 1047 | public void testGetBuildMDParts() throws Exception { 1048 | final Version v = Version.parseVersion("1.0.0+a.b.c.001"); 1049 | assertArrayEquals(new String[] { "a", "b", "c", "001" }, 1050 | v.getBuildMetaDataParts()); 1051 | } 1052 | 1053 | @Test 1054 | public void testWithMajorAllWillbe0() throws Exception { 1055 | final Version v = Version.create(1, 0, 0); 1056 | 1057 | assertDoesNotThrow(() -> v.withMajor(0)); 1058 | } 1059 | 1060 | @Test 1061 | public void testWithMinorAllWillbe0() throws Exception { 1062 | final Version v = Version.create(0, 1, 0); 1063 | 1064 | assertDoesNotThrow(() -> v.withMinor(0)); 1065 | } 1066 | 1067 | @Test 1068 | public void testWithPatchAllWillbe0() throws Exception { 1069 | final Version v = Version.create(0, 0, 1); 1070 | 1071 | assertDoesNotThrow(() -> v.withPatch(0)); 1072 | } 1073 | 1074 | @Test 1075 | public void testWithMajorKeepEverythingElse() throws Exception { 1076 | final Version v = Version.create(1, 2, 3, "foo", "bar"); 1077 | assertEquals(Version.create(2, 2, 3, "foo", "bar"), v.withMajor(2)); 1078 | } 1079 | 1080 | @Test 1081 | public void testWithMinorKeepEverythingElse() throws Exception { 1082 | final Version v = Version.create(1, 2, 3, "foo", "bar"); 1083 | assertEquals(Version.create(1, 1, 3, "foo", "bar"), v.withMinor(1)); 1084 | } 1085 | 1086 | @Test 1087 | public void testWithPatchKeepEverythingElse() throws Exception { 1088 | final Version v = Version.create(1, 2, 3, "foo", "bar"); 1089 | assertEquals(Version.create(1, 2, 2, "foo", "bar"), v.withPatch(2)); 1090 | } 1091 | 1092 | @Test 1093 | public void testWithPreReleaseKeepEverythingElse() throws Exception { 1094 | final Version v = Version.create(1, 2, 3, "foo", "bar"); 1095 | assertEquals(Version.create(1, 2, 3, "bar", "bar"), v.withPreRelease("bar")); 1096 | } 1097 | 1098 | @Test 1099 | public void testWithPreReleaseEmpty() throws Exception { 1100 | final Version v = Version.create(1, 2, 3, "foo"); 1101 | assertEquals(Version.create(1, 2, 3), v.withPreRelease("")); 1102 | } 1103 | 1104 | @Test 1105 | public void testWithPreReleaseEmptyArray() throws Exception { 1106 | final Version v = Version.create(1, 2, 3, "foo"); 1107 | assertEquals(Version.create(1, 2, 3), v.withPreRelease(new String[0])); 1108 | } 1109 | 1110 | @Test 1111 | public void testWithPreReleaseModifyArray() throws Exception { 1112 | final String[] newPreRelease = new String[] { "bar" }; 1113 | final Version v = Version.create(1, 2, 3, "foo"); 1114 | final Version v2 = v.withPreRelease(newPreRelease); 1115 | newPreRelease[0] = "foo"; 1116 | assertEquals(Version.create(1, 2, 3, "bar"), v2); 1117 | } 1118 | 1119 | @Test 1120 | public void testWithPreReleaseIllegalPart() throws Exception { 1121 | final String[] newPreRelease = new String[] { "01" }; 1122 | 1123 | assertThrows(VersionFormatException.class, 1124 | () -> Version.create(1, 0, 0).withPreRelease(newPreRelease)); 1125 | } 1126 | 1127 | @Test 1128 | public void testWithPreReleaseNull() throws Exception { 1129 | assertThrows(IllegalArgumentException.class, 1130 | () -> Version.create(1, 2, 3).withPreRelease((String) null)); 1131 | } 1132 | 1133 | @Test 1134 | public void testWithPreReleaseNull2() throws Exception { 1135 | assertThrows(IllegalArgumentException.class, 1136 | () -> Version.create(1, 2, 3).withPreRelease((String[]) null)); 1137 | } 1138 | 1139 | @Test 1140 | public void testPreReleaseArrayWithDotPart() throws Exception { 1141 | final Version v = Version.create(1, 0, 0); 1142 | Assertions.assertEquals( 1143 | v.withPreRelease(new String[] { "12.a" }), 1144 | v.withPreRelease(new String[] { "12", "a" })); 1145 | } 1146 | 1147 | @Test 1148 | public void testBuildMdArrayWithDotPart() throws Exception { 1149 | assertThrows(VersionFormatException.class, 1150 | () -> Version.create(1, 0, 0).withPreRelease(new String[] { "12+a" })); 1151 | } 1152 | 1153 | @Test 1154 | public void testWithBuildMDKeepEverythingElse() throws Exception { 1155 | final Version v = Version.create(1, 2, 3, "foo", "bar"); 1156 | assertEquals(Version.create(1, 2, 3, "foo", "foo"), v.withBuildMetaData("foo")); 1157 | } 1158 | 1159 | @Test 1160 | public void testWithBuildMDEmpty() throws Exception { 1161 | final Version v = Version.create(1, 2, 3, "", "foo"); 1162 | assertEquals(Version.create(1, 2, 3), v.withBuildMetaData("")); 1163 | } 1164 | 1165 | @Test 1166 | public void testWithBuildMDEmptyArray() throws Exception { 1167 | final Version v = Version.create(1, 2, 3, "", "foo"); 1168 | assertEquals(Version.create(1, 2, 3), v.withBuildMetaData(new String[0])); 1169 | } 1170 | 1171 | @Test 1172 | public void testBuildMDArrayWithDotPart() throws Exception { 1173 | final Version v = Version.create(1, 0, 0); 1174 | Assertions.assertEquals( 1175 | v.withBuildMetaData(new String[] { "12.a" }), 1176 | v.withBuildMetaData(new String[] { "12", "a" })); 1177 | } 1178 | 1179 | @Test 1180 | public void testWithBuildMDIllegalPartPlus() throws Exception { 1181 | assertThrows(VersionFormatException.class, 1182 | () -> Version.create(1, 0, 0).withBuildMetaData(new String[] { "12+a" })); 1183 | } 1184 | 1185 | @Test 1186 | public void testWithBuildMDModifyArray() throws Exception { 1187 | final String[] newBuildMd = new String[] { "bar" }; 1188 | final Version v = Version.create(1, 2, 3, "", "foo"); 1189 | final Version v2 = v.withBuildMetaData(newBuildMd); 1190 | newBuildMd[0] = "foo"; 1191 | assertEquals(Version.create(1, 2, 3, "", "bar"), v2); 1192 | } 1193 | 1194 | @Test 1195 | public void testWithBuildMdNull() throws Exception { 1196 | assertThrows(IllegalArgumentException.class, 1197 | () -> Version.create(1, 2, 3).withBuildMetaData((String) null)); 1198 | } 1199 | 1200 | @Test 1201 | public void testWithBuildMdNull2() throws Exception { 1202 | assertThrows(IllegalArgumentException.class, 1203 | () -> Version.create(1, 2, 3).withBuildMetaData((String[]) null)); 1204 | } 1205 | 1206 | @Test 1207 | public void testParseBiggerNumbers() throws Exception { 1208 | final Version v = Version.parseVersion("1234.5678.9012"); 1209 | assertEquals(1234, v.getMajor()); 1210 | assertEquals(5678, v.getMinor()); 1211 | assertEquals(9012, v.getPatch()); 1212 | } 1213 | 1214 | @Test 1215 | void testIsNotStableWIthPreRelease() throws Exception { 1216 | assertFalse(Version.create(1, 2, 3).withPreRelease("foo").isStable()); 1217 | } 1218 | 1219 | @Test 1220 | void testIsStable() throws Exception { 1221 | assertTrue(Version.create(1, 3, 4).isStable()); 1222 | } 1223 | 1224 | @Test 1225 | void testIsStableWithBuildMetaData() throws Exception { 1226 | assertTrue(Version.create(1, 3, 4).withBuildMetaData("foo").isStable()); 1227 | } 1228 | 1229 | // This would have produced an illegal identifier when using toLowerCase with default 1230 | // locale 1231 | // https://github.com/skuzzle/semantic-version/pull/5 1232 | @Test 1233 | @DefaultLocale("tr-TR") 1234 | void testToLowerCaseWithTurkishLocale() throws Exception { 1235 | final Version lowerCase = Version.create(1, 3, 4).withPreRelease("I") 1236 | .toLowerCase(); 1237 | Version.create(1, 3, 4, lowerCase.getPreRelease()); 1238 | } 1239 | 1240 | // This would have produced an illegal identifier when using toLowerCase with default 1241 | // locale 1242 | // https://github.com/skuzzle/semantic-version/pull/5 1243 | @Test 1244 | @DefaultLocale("tr-TR") 1245 | void testToUpperCaseWithTurkishLocale() throws Exception { 1246 | final Version lowerCase = Version.create(1, 3, 4) 1247 | .withPreRelease("i") 1248 | .toUpperCase(); 1249 | Version.create(1, 3, 4, lowerCase.getPreRelease()); 1250 | } 1251 | } 1252 | -------------------------------------------------------------------------------- /src/test/resources/versions_0.5.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skuzzle/semantic-version/f817338295a94df30f24ac7013f82cb345ccdbd1/src/test/resources/versions_0.5.bin -------------------------------------------------------------------------------- /src/test/resources/versions_0.6.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skuzzle/semantic-version/f817338295a94df30f24ac7013f82cb345ccdbd1/src/test/resources/versions_0.6.bin --------------------------------------------------------------------------------