View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.shared.release.policy.semver;
20  
21  import java.util.Objects;
22  import java.util.regex.Matcher;
23  import java.util.regex.Pattern;
24  
25  /**
26   * A simple Semantic Versioning 2.0.0 implementation.
27   * <p>
28   * This class provides basic parsing and manipulation of semantic version strings
29   * following the format: MAJOR.MINOR.PATCH[-PRERELEASE][+METADATA]
30   * </p>
31   *
32   * @see <a href="https://semver.org/">Semantic Versioning 2.0.0</a>
33   */
34  class SemVer {
35  
36      /**
37       * Regex pattern for parsing semantic versions from semver.org.
38       * Groups: 1=major, 2=minor, 3=patch, 4=prerelease, 5=metadata
39       *
40       * @see <a href="https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string">SemVer Regex</a>
41       */
42      private static final Pattern SEMVER_PATTERN = Pattern.compile("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)"
43              + "(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)"
44              + "(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$");
45  
46      private final int major;
47      private final int minor;
48      private final int patch;
49      private final String preRelease;
50      private final String metadata;
51  
52      /**
53       * Version element types that can be incremented.
54       */
55      public enum Element {
56          MAJOR,
57          MINOR,
58          PATCH
59      }
60  
61      /**
62       * Constructs a new SemVer instance.
63       *
64       * @param major the major version number
65       * @param minor the minor version number
66       * @param patch the patch version number
67       * @param preRelease the pre-release identifier (can be null)
68       * @param metadata the build metadata (can be null)
69       */
70      protected SemVer(int major, int minor, int patch, String preRelease, String metadata) {
71          if (major < 0 || minor < 0 || patch < 0) {
72              throw new IllegalArgumentException("Version numbers must be non-negative");
73          }
74          this.major = major;
75          this.minor = minor;
76          this.patch = patch;
77          this.preRelease = preRelease;
78          this.metadata = metadata;
79      }
80  
81      /**
82       * Parses a version string into a SemVer object.
83       *
84       * @param version the version string to parse
85       * @return the parsed SemVer object
86       * @throws IllegalArgumentException if the version string is invalid
87       */
88      static SemVer parse(String version) {
89          if (version == null || version.trim().isEmpty()) {
90              throw new IllegalArgumentException("Version string cannot be null or empty");
91          }
92  
93          Matcher matcher = SEMVER_PATTERN.matcher(version.trim());
94          if (!matcher.matches()) {
95              throw new IllegalArgumentException("Invalid semantic version format: " + version);
96          }
97  
98          int major = Integer.parseInt(matcher.group(1));
99          int minor = Integer.parseInt(matcher.group(2));
100         int patch = Integer.parseInt(matcher.group(3));
101         String preRelease = matcher.group(4);
102         String metadata = matcher.group(5);
103 
104         return new SemVer(major, minor, patch, preRelease, metadata);
105     }
106 
107     /**
108      * Returns a new SemVer with the release version (removes -SNAPSHOT and any pre-release/metadata).
109      * <p>
110      * Examples:
111      * <ul>
112      *   <li>1.2.3-SNAPSHOT → 1.2.3</li>
113      *   <li>1.2.3-beta+build → 1.2.3</li>
114      *   <li>1.2.3 → 1.2.3</li>
115      * </ul>
116      *
117      * @return a new SemVer representing the release version
118      */
119     SemVer toReleaseVersion() {
120         return new SemVer(major, minor, patch, null, null);
121     }
122 
123     /**
124      * Returns a new SemVer with -SNAPSHOT appended as pre-release identifier.
125      * If the version already has a pre-release identifier, it will be replaced with SNAPSHOT.
126      * Metadata is removed.
127      * <p>
128      * Examples:
129      * <ul>
130      *   <li>1.2.3 → 1.2.3-SNAPSHOT</li>
131      *   <li>1.2.3-beta → 1.2.3-SNAPSHOT</li>
132      *   <li>1.2.3+build → 1.2.3-SNAPSHOT</li>
133      *   <li>1.2.3-SNAPSHOT → 1.2.3-SNAPSHOT (no change)</li>
134      * </ul>
135      *
136      * @return a new SemVer with SNAPSHOT pre-release identifier, or this instance if already a SNAPSHOT version
137      */
138     SemVer toSnapshotVersion() {
139         if ("SNAPSHOT".equals(preRelease) && metadata == null) {
140             return this;
141         }
142         return new SemVer(major, minor, patch, "SNAPSHOT", null);
143     }
144 
145     /**
146      * Returns a new SemVer with the specified element incremented.
147      * If the version has pre-release or metadata, returns the release version without incrementing.
148      * Otherwise, increments the specified element and all lower elements are reset to 0.
149      * Pre-release and metadata are always cleared in the result.
150      *
151      * @param element the element to increment (MAJOR, MINOR, or PATCH)
152      * @return a new SemVer with the specified element incremented (or release version if pre-release/metadata present)
153      */
154     SemVer next(Element element) {
155         Objects.requireNonNull(element, "Element cannot be null");
156 
157         // If version has pre-release or metadata, just return release version without incrementing
158         if (hasPreRelease() || hasMetadata()) {
159             return toReleaseVersion();
160         }
161 
162         switch (element) {
163             case MAJOR:
164                 return new SemVer(major + 1, 0, 0, null, null);
165             case MINOR:
166                 return new SemVer(major, minor + 1, 0, null, null);
167             case PATCH:
168                 return new SemVer(major, minor, patch + 1, null, null);
169             default:
170                 throw new IllegalArgumentException("Unknown element: " + element);
171         }
172     }
173 
174     /**
175      * Checks if this version has a pre-release identifier.
176      *
177      * @return true if pre-release identifier is present, false otherwise
178      */
179     private boolean hasPreRelease() {
180         return preRelease != null && !preRelease.isEmpty();
181     }
182 
183     /**
184      * Checks if this version has build metadata.
185      *
186      * @return true if build metadata is present, false otherwise
187      */
188     private boolean hasMetadata() {
189         return metadata != null && !metadata.isEmpty();
190     }
191 
192     /**
193      * Returns the major version number.
194      *
195      * @return the major version
196      */
197     int getMajor() {
198         return major;
199     }
200 
201     /**
202      * Returns the minor version number.
203      *
204      * @return the minor version
205      */
206     int getMinor() {
207         return minor;
208     }
209 
210     /**
211      * Returns the patch version number.
212      *
213      * @return the patch version
214      */
215     int getPatch() {
216         return patch;
217     }
218 
219     /**
220      * Returns the pre-release identifier.
221      *
222      * @return the pre-release identifier, or null if not present
223      */
224     String getPreRelease() {
225         return preRelease;
226     }
227 
228     /**
229      * Returns the build metadata.
230      *
231      * @return the build metadata, or null if not present
232      */
233     String getMetadata() {
234         return metadata;
235     }
236 
237     @Override
238     public String toString() {
239         StringBuilder sb = new StringBuilder();
240         sb.append(major).append('.').append(minor).append('.').append(patch);
241 
242         if (preRelease != null && !preRelease.isEmpty()) {
243             sb.append('-').append(preRelease);
244         }
245 
246         if (metadata != null && !metadata.isEmpty()) {
247             sb.append('+').append(metadata);
248         }
249 
250         return sb.toString();
251     }
252 }