View Javadoc

1   package com.atlassian.plugin.util;
2   
3   import org.apache.commons.lang.StringUtils;
4   
5   import java.util.Comparator;
6   import java.util.regex.Matcher;
7   import java.util.regex.Pattern;
8   
9   /**
10   * Compares dotted version strings of varying length. Makes a best effort with
11   * other delimiters and non-numeric versions.
12   * <p/>
13   * For dotted decimals, comparison is as you'd expect: 0.1 is before 0.2 is before
14   * 1.0 is before 2.0. This works for any number of dots.
15   * <p/>
16   * More complicated version numbers are compared by splitting the version strings
17   * into components using the {@link #DELIMITER_PATTERN} and comparing each
18   * component in order. The first difference found when comparing components
19   * left-to-right is returned.
20   * <p/>
21   * Two numeric components (containing only digits) are compared as integers. A
22   * numeric component comes after any non-numeric one. Two non-numeric components
23   * are ordered by {@link String#compareToIgnoreCase(String)}.
24   */
25  public class VersionStringComparator implements Comparator<String>
26  {
27      public static final String DELIMITER_PATTERN = "[\\.-]";
28      public static final String COMPONENT_PATTERN = "[\\d\\w]+";
29      public static final String VALID_VERSION_PATTERN = COMPONENT_PATTERN + "(?:" + DELIMITER_PATTERN + COMPONENT_PATTERN + ")*";
30      private static final Pattern START_WITH_INT_PATTERN = Pattern.compile("(^\\d+)");
31  
32      public static boolean isValidVersionString(final String version)
33      {
34          return (version != null) && version.matches(VALID_VERSION_PATTERN);
35      }
36  
37      /**
38       * Compares two version strings. If either argument is not a String,
39       * this method returns 0.
40       *
41       * @throws IllegalArgumentException if either argument is a String,
42       * but does not match {@link #VALID_VERSION_PATTERN}.
43       * @see #isValidVersionString(String)
44       */
45      //    public int compare(Object o1, Object o2)
46      //    {
47      //        if (!(o1 instanceof String)) return 0;
48      //        if (!(o2 instanceof String)) return 0;
49      //
50      //        return compare((String) o1, (String) o2);
51      //    }
52      /**
53       * Compares two version strings using the algorithm described above.
54       *
55       * @return <tt>-1</tt> if version1 is before version2, <tt>1</tt> if version2 is before
56       * version1, or <tt>0</tt> if the versions are equal.
57       * @throws IllegalArgumentException if either argument does not match {@link #VALID_VERSION_PATTERN}.
58       * @see #isValidVersionString(String)
59       */
60      public int compare(final String version1, final String version2)
61      {
62          // Get the version numbers, remove all whitespaces
63          String thisVersion = "0";
64          if (StringUtils.isNotEmpty(version1))
65          {
66              thisVersion = version1.replaceAll(" ", "");
67          }
68          String compareVersion = "0";
69          if (StringUtils.isNotEmpty(version2))
70          {
71              compareVersion = version2.replaceAll(" ", "");
72          }
73  
74          if (!thisVersion.matches(VALID_VERSION_PATTERN) || !compareVersion.matches(VALID_VERSION_PATTERN))
75          {
76              throw new IllegalArgumentException("Version number '" + thisVersion + "' cannot be compared to '" + compareVersion + "'");
77          }
78  
79          // Split the version numbers
80          final String[] v1 = thisVersion.split(DELIMITER_PATTERN);
81          final String[] v2 = compareVersion.split(DELIMITER_PATTERN);
82  
83          final Comparator<String> componentComparator = new VersionStringComponentComparator();
84  
85          // Compare each place, until we find a difference and then return. If empty, assume zero.
86          for (int i = 0; i < (v1.length > v2.length ? v1.length : v2.length); i++)
87          {
88              final String component1 = i >= v1.length ? "0" : v1[i];
89              final String component2 = i >= v2.length ? "0" : v2[i];
90  
91              if (componentComparator.compare(component1, component2) != 0)
92              {
93                  return componentComparator.compare(component1, component2);
94              }
95          }
96  
97          return 0;
98      }
99  
100     private class VersionStringComponentComparator implements Comparator<String>
101     {
102         public static final int FIRST_GREATER = 1;
103         public static final int SECOND_GREATER = -1;
104 
105         public int compare(final String component1, final String component2)
106         {
107             if (component1.equalsIgnoreCase(component2))
108             {
109                 return 0;
110             }
111 
112             if (isInteger(component1) && isInteger(component2))
113             {
114                 // both numbers -- parse and compare
115                 if (Integer.parseInt(component1) > Integer.parseInt(component2))
116                 {
117                     return FIRST_GREATER;
118                 }
119                 if (Integer.parseInt(component2) > Integer.parseInt(component1))
120                 {
121                     return SECOND_GREATER;
122                 }
123                 return 0;
124             }
125 
126             // Handles the case where we are comparing 1.5 to 1.6a
127             final Integer comp1IntPart = getStartingInteger(component1);
128             final Integer comp2IntPart = getStartingInteger(component2);
129             if (comp1IntPart != null && comp2IntPart != null)
130             {
131                 if (comp1IntPart > comp2IntPart)
132                 {
133                     return FIRST_GREATER;
134                 }
135                 else if (comp2IntPart > comp1IntPart)
136                 {
137                     return SECOND_GREATER;
138                 }
139             }
140 
141             // 2.3-alpha < 2.3.0 and 2.3a < 2.3
142             // fixes PLUG-672. We are safe to do the integer check here since above we have
143             // already determined that one of the two components are not an integer and that one does not start with
144             // an int that may be larger than the other component
145             if (isInteger(component1))
146             {
147                 return FIRST_GREATER;
148             }
149             if (isInteger(component2))
150             {
151                 return SECOND_GREATER;
152             }
153 
154             // 2.3a < 2.3b
155             return component1.compareToIgnoreCase(component2);
156         }
157 
158         private boolean isInteger(final String string)
159         {
160             return string.matches("\\d+");
161         }
162 
163         private Integer getStartingInteger(final String string)
164         {
165             Matcher matcher = START_WITH_INT_PATTERN.matcher(string);
166             if (matcher.find())
167             {
168                 // If we found a starting digit group then lets return it
169                 return new Integer(matcher.group(1));
170             }
171             return null;
172         }
173 
174     }
175 }