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