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.versions;
20  
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.List;
24  import java.util.Locale;
25  import java.util.Objects;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  import org.apache.maven.artifact.Artifact;
30  import org.apache.maven.artifact.ArtifactUtils;
31  import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
32  import org.codehaus.plexus.util.StringUtils;
33  
34  /**
35   * This compares and increments versions for a common java versioning scheme.
36   * <p>
37   * The supported version scheme has the following parts.<br>
38   * <code><i>component-digits-annotation-annotationRevision-buildSpecifier</i></code><br>
39   * Example:<br>
40   * <code>my-component-1.0.1-alpha-2-SNAPSHOT</code>
41   * <b>Terms:</b>
42   * <ul>
43   * <li><i>component</i> - name of the versioned component (log4j, commons-lang, etc)
44   * <li><i>digits</i> - Numeric digits with at least one "." period. (1.0, 1.1, 1.01, 1.2.3, etc)
45   * <li><i>annotationRevision</i> - Integer qualifier for the annotation. (4 as in RC-4)
46   * <li><i>buildSpecifier</i> - Additional specifier for build. (SNAPSHOT, or build number like "20041114.081234-2")
47   * </ul>
48   * <b>Digits is the only required piece of the version string, and must contain at lease one "." period.</b>
49   * Implementation details:<br>
50   * The separators "_" and "-" between components are also optional (though they are usually recommended).<br>
51   * Example:<br>
52   * <code>log4j-1.2.9-beta-9-SNAPSHOT == log4j1.2.9beta9SNAPSHOT == log4j_1.2.9_beta_9_SNAPSHOT</code>
53   * Leading zeros are significant when performing comparisons.
54   * TODO: this parser is better than DefaultArtifactVersion - replace it with this (but align naming) and then remove
55   * this from here.
56   */
57  public class DefaultVersionInfo implements VersionInfo {
58      private final String strVersion;
59  
60      private final List<String> digits;
61  
62      private String annotation;
63  
64      private String annotationRevision;
65  
66      private final String buildSpecifier;
67  
68      private String annotationSeparator;
69  
70      private String annotationRevSeparator;
71  
72      private final String buildSeparator;
73  
74      private static final int DIGITS_INDEX = 1;
75  
76      private static final int ANNOTATION_SEPARATOR_INDEX = 2;
77  
78      private static final int ANNOTATION_INDEX = 3;
79  
80      private static final int ANNOTATION_REV_SEPARATOR_INDEX = 4;
81  
82      private static final int ANNOTATION_REVISION_INDEX = 5;
83  
84      private static final int BUILD_SEPARATOR_INDEX = 6;
85  
86      private static final int BUILD_SPECIFIER_INDEX = 7;
87  
88      private static final String SNAPSHOT_IDENTIFIER = "SNAPSHOT";
89  
90      private static final String DIGIT_SEPARATOR_STRING = ".";
91  
92      /** Constant <code>STANDARD_PATTERN</code> */
93      public static final Pattern STANDARD_PATTERN = Pattern.compile(
94              "^((?:\\d+\\.)*\\d+)" // digit(s) and '.' repeated - followed by digit (version digits 1.22.0, etc)
95                      + "([-_])?" // optional - or _  (annotation separator)
96                      + "([a-zA-Z]*)" // alpha characters (looking for annotation - alpha, beta, RC, etc.)
97                      + "([-_])?" // optional - or _  (annotation revision separator)
98                      + "(\\d*)" // digits  (any digits after rc or beta is an annotation revision)
99                      + "(?:([-_])?(.*?))?$"); // - or _ followed everything else (build specifier)
100 
101     /* *
102      * cmaki 02242009
103      * FIX for non-digit release numbers, e.g. trunk-SNAPSHOT or just SNAPSHOT
104      * This alternate pattern supports version numbers like:
105      * trunk-SNAPSHOT
106      * branchName-SNAPSHOT
107      * SNAPSHOT
108      */
109     // for SNAPSHOT releases only (possible versions include: trunk-SNAPSHOT or SNAPSHOT)
110     /** Constant <code>ALTERNATE_PATTERN</code> */
111     public static final Pattern ALTERNATE_PATTERN = Pattern.compile("^(SNAPSHOT|[a-zA-Z]+[_-]SNAPSHOT)");
112 
113     /**
114      * Constructs this object and parses the supplied version string.
115      *
116      * @param version the version string
117      * @throws org.apache.maven.shared.release.versions.VersionParseException if an exception during parsing the input
118      */
119     public DefaultVersionInfo(String version) throws VersionParseException {
120         strVersion = version;
121 
122         // FIX for non-digit release numbers, e.g. trunk-SNAPSHOT or just SNAPSHOT
123         Matcher matcher = ALTERNATE_PATTERN.matcher(strVersion);
124         // TODO: hack because it didn't support "SNAPSHOT"
125         if (matcher.matches()) {
126             annotation = null;
127             digits = null;
128             buildSpecifier = version;
129             buildSeparator = null;
130             return;
131         }
132 
133         Matcher m = STANDARD_PATTERN.matcher(strVersion);
134         if (m.matches()) {
135             digits = parseDigits(m.group(DIGITS_INDEX));
136             if (!SNAPSHOT_IDENTIFIER.equals(m.group(ANNOTATION_INDEX))) {
137                 annotationSeparator = m.group(ANNOTATION_SEPARATOR_INDEX);
138                 annotation = nullIfEmpty(m.group(ANNOTATION_INDEX));
139 
140                 if (StringUtils.isNotEmpty(m.group(ANNOTATION_REV_SEPARATOR_INDEX))
141                         && StringUtils.isEmpty(m.group(ANNOTATION_REVISION_INDEX))) {
142                     // The build separator was picked up as the annotation revision separator
143                     buildSeparator = m.group(ANNOTATION_REV_SEPARATOR_INDEX);
144                     buildSpecifier = nullIfEmpty(m.group(BUILD_SPECIFIER_INDEX));
145                 } else {
146                     annotationRevSeparator = m.group(ANNOTATION_REV_SEPARATOR_INDEX);
147                     annotationRevision = nullIfEmpty(m.group(ANNOTATION_REVISION_INDEX));
148 
149                     buildSeparator = m.group(BUILD_SEPARATOR_INDEX);
150                     buildSpecifier = nullIfEmpty(m.group(BUILD_SPECIFIER_INDEX));
151                 }
152             } else {
153                 // Annotation was "SNAPSHOT" so populate the build specifier with that data
154                 buildSeparator = m.group(ANNOTATION_SEPARATOR_INDEX);
155                 buildSpecifier = nullIfEmpty(m.group(ANNOTATION_INDEX));
156             }
157         } else {
158             throw new VersionParseException("Unable to parse the version string: \"" + version + "\"");
159         }
160     }
161 
162     /**
163      * <p>Constructor for DefaultVersionInfo.</p>
164      *
165      * @param digits a {@link java.util.List} object
166      * @param annotation a {@link java.lang.String} object
167      * @param annotationRevision a {@link java.lang.String} object
168      * @param buildSpecifier a {@link java.lang.String} object
169      * @param annotationSeparator a {@link java.lang.String} object
170      * @param annotationRevSeparator a {@link java.lang.String} object
171      * @param buildSeparator a {@link java.lang.String} object
172      */
173     public DefaultVersionInfo(
174             List<String> digits,
175             String annotation,
176             String annotationRevision,
177             String buildSpecifier,
178             String annotationSeparator,
179             String annotationRevSeparator,
180             String buildSeparator) {
181         this.digits = digits;
182         this.annotation = annotation;
183         this.annotationRevision = annotationRevision;
184         this.buildSpecifier = buildSpecifier;
185         this.annotationSeparator = annotationSeparator;
186         this.annotationRevSeparator = annotationRevSeparator;
187         this.buildSeparator = buildSeparator;
188         this.strVersion = getVersionString(this, buildSpecifier, buildSeparator);
189     }
190 
191     @Override
192     public boolean isSnapshot() {
193         return ArtifactUtils.isSnapshot(strVersion);
194     }
195 
196     @Override
197     public VersionInfo getNextVersion() {
198         DefaultVersionInfo version = null;
199         if (digits != null) {
200             List<String> digits = new ArrayList<>(this.digits);
201             String annotationRevision = this.annotationRevision;
202             if (StringUtils.isNumeric(annotationRevision)) {
203                 annotationRevision = incrementVersionString(annotationRevision);
204             } else {
205                 digits.set(digits.size() - 1, incrementVersionString(digits.get(digits.size() - 1)));
206             }
207 
208             version = new DefaultVersionInfo(
209                     digits,
210                     annotation,
211                     annotationRevision,
212                     buildSpecifier,
213                     annotationSeparator,
214                     annotationRevSeparator,
215                     buildSeparator);
216         }
217         return version;
218     }
219 
220     /**
221      * {@inheritDoc}
222      *
223      * Compares this {@link DefaultVersionInfo} to the supplied {@link DefaultVersionInfo} to determine which version is
224      * greater.
225      */
226     @Override
227     public int compareTo(VersionInfo obj) {
228         DefaultVersionInfo that = (DefaultVersionInfo) obj;
229 
230         int result;
231         // TODO: this is a workaround for a bug in DefaultArtifactVersion - fix there - 1.01 < 1.01.01
232         if (strVersion.startsWith(that.strVersion)
233                 && !strVersion.equals(that.strVersion)
234                 && strVersion.charAt(that.strVersion.length()) != '-') {
235             result = 1;
236         } else if (that.strVersion.startsWith(strVersion)
237                 && !strVersion.equals(that.strVersion)
238                 && that.strVersion.charAt(strVersion.length()) != '-') {
239             result = -1;
240         } else {
241             // TODO: this is a workaround for a bug in DefaultArtifactVersion - fix there - it should not consider case
242             // in comparing the qualifier
243             // NOTE: The combination of upper-casing and lower-casing is an approximation of String.equalsIgnoreCase()
244             String thisVersion = strVersion.toUpperCase(Locale.ENGLISH).toLowerCase(Locale.ENGLISH);
245             String thatVersion = that.strVersion.toUpperCase(Locale.ENGLISH).toLowerCase(Locale.ENGLISH);
246 
247             result = new DefaultArtifactVersion(thisVersion).compareTo(new DefaultArtifactVersion(thatVersion));
248         }
249         return result;
250     }
251 
252     @Override
253     public boolean equals(Object obj) {
254         if (!(obj instanceof DefaultVersionInfo)) {
255             return false;
256         }
257 
258         return compareTo((VersionInfo) obj) == 0;
259     }
260 
261     @Override
262     public int hashCode() {
263         return strVersion.toLowerCase(Locale.ENGLISH).hashCode();
264     }
265 
266     /**
267      * Takes a string and increments it as an integer.
268      * Preserves any lpad of "0" zeros.
269      *
270      * @param s the version number
271      * @return {@code String} increments the input {@code String} as an integer
272      * and Preserves any lpad of "0" zeros.
273      */
274     protected String incrementVersionString(String s) {
275         int n = Integer.valueOf(s).intValue() + 1;
276         String value = String.valueOf(n);
277         if (value.length() < s.length()) {
278             // String was left-padded with zeros
279             value = StringUtils.leftPad(value, s.length(), "0");
280         }
281         return value;
282     }
283 
284     @Override
285     public String getSnapshotVersionString() {
286         if (strVersion.equals(Artifact.SNAPSHOT_VERSION)) {
287             return strVersion;
288         }
289 
290         String baseVersion = getReleaseVersionString();
291 
292         if (baseVersion.length() > 0) {
293             baseVersion += "-";
294         }
295 
296         return baseVersion + Artifact.SNAPSHOT_VERSION;
297     }
298 
299     @Override
300     public String getReleaseVersionString() {
301         String baseVersion = strVersion;
302 
303         Matcher m = Artifact.VERSION_FILE_PATTERN.matcher(baseVersion);
304         if (m.matches()) {
305             baseVersion = m.group(1);
306         }
307         // MRELEASE-623 SNAPSHOT is case-insensitive
308         else if (StringUtils.right(baseVersion, 9).equalsIgnoreCase("-" + Artifact.SNAPSHOT_VERSION)) {
309             baseVersion = baseVersion.substring(0, baseVersion.length() - Artifact.SNAPSHOT_VERSION.length() - 1);
310         } else if (baseVersion.equals(Artifact.SNAPSHOT_VERSION)) {
311             baseVersion = "1.0";
312         }
313         return baseVersion;
314     }
315 
316     @Override
317     public String toString() {
318         return strVersion;
319     }
320 
321     /**
322      * <p>getVersionString.</p>
323      *
324      * @param info a {@link org.apache.maven.shared.release.versions.DefaultVersionInfo} object
325      * @param buildSpecifier a {@link java.lang.String} object
326      * @param buildSeparator a {@link java.lang.String} object
327      * @return a {@link java.lang.String} object
328      */
329     protected static String getVersionString(DefaultVersionInfo info, String buildSpecifier, String buildSeparator) {
330         StringBuilder sb = new StringBuilder();
331 
332         if (info.digits != null) {
333             sb.append(joinDigitString(info.digits));
334         }
335 
336         if (info.annotation != null && !info.annotation.isEmpty()) {
337             sb.append(Objects.toString(info.annotationSeparator, ""));
338             sb.append(info.annotation);
339         }
340 
341         if (info.annotationRevision != null && !info.annotationRevision.isEmpty()) {
342             if (info.annotation == null || info.annotation.isEmpty()) {
343                 sb.append(Objects.toString(info.annotationSeparator, ""));
344             } else {
345                 sb.append(Objects.toString(info.annotationRevSeparator, ""));
346             }
347             sb.append(info.annotationRevision);
348         }
349 
350         if (buildSpecifier != null && !buildSpecifier.isEmpty()) {
351             sb.append(Objects.toString(buildSeparator, ""));
352             sb.append(buildSpecifier);
353         }
354 
355         return sb.toString();
356     }
357 
358     /**
359      * Simply joins the items in the list with "." period
360      *
361      * @return a single {@code String} of the items in the passed list, joined with a "."
362      * @param digits {@code List<String>} of digits
363      */
364     protected static String joinDigitString(List<String> digits) {
365         return digits != null ? StringUtils.join(digits.iterator(), DIGIT_SEPARATOR_STRING) : null;
366     }
367 
368     /**
369      * Splits the string on "." and returns a list
370      * containing each digit.
371      *
372      * @param strDigits
373      */
374     private List<String> parseDigits(String strDigits) {
375         return Arrays.asList(StringUtils.split(strDigits, DIGIT_SEPARATOR_STRING));
376     }
377 
378     // --------------------------------------------------
379     // Getters & Setters
380     // --------------------------------------------------
381 
382     private static String nullIfEmpty(String s) {
383         return (s == null || s.isEmpty()) ? null : s;
384     }
385 
386     /**
387      * <p>Getter for the field <code>digits</code>.</p>
388      *
389      * @return a {@link java.util.List} object
390      */
391     public List<String> getDigits() {
392         return digits;
393     }
394 
395     /**
396      * <p>Getter for the field <code>annotation</code>.</p>
397      *
398      * @return a {@link java.lang.String} object
399      */
400     public String getAnnotation() {
401         return annotation;
402     }
403 
404     /**
405      * <p>Getter for the field <code>annotationRevision</code>.</p>
406      *
407      * @return a {@link java.lang.String} object
408      */
409     public String getAnnotationRevision() {
410         return annotationRevision;
411     }
412 
413     /**
414      * <p>Getter for the field <code>buildSpecifier</code>.</p>
415      *
416      * @return a {@link java.lang.String} object
417      */
418     public String getBuildSpecifier() {
419         return buildSpecifier;
420     }
421 }