View Javadoc

1   package com.atlassian.plugin.util;
2   
3   import org.apache.commons.lang.StringUtils;
4   
5   import java.util.Comparator;
6   
7   /**
8    * Compares dotted version strings of varying length. Makes a best effort with
9    * other delimiters and non-numeric versions.
10   * <p/>
11   * For dotted decimals, comparison is as you'd expect: 0.1 is before 0.2 is before
12   * 1.0 is before 2.0. This works for any number of dots.
13   * <p/>
14   * More complicated version numbers are compared by splitting the version strings
15   * into components using the {@link #DELIMITER_PATTERN} and comparing each
16   * component in order. The first difference found when comparing components
17   * left-to-right is returned.
18   * <p/>
19   * Two numeric components (containing only digits) are compared as integers. A
20   * numeric component comes after any non-numeric one. Two non-numeric components
21   * are ordered by {@link String#compareToIgnoreCase(String)}.
22   */
23  public class VersionStringComparator implements Comparator
24  {
25      public static final String DELIMITER_PATTERN = "[\\.,-]";
26      public static final String COMPONENT_PATTERN = "[\\d\\w]+";
27      public static final String VALID_VERSION_PATTERN =
28          COMPONENT_PATTERN + "(" + DELIMITER_PATTERN + COMPONENT_PATTERN + ")*";
29  
30      public static boolean isValidVersionString(String version)
31      {
32          return version != null && version.matches(VALID_VERSION_PATTERN);
33      }
34  
35      /**
36       * Compares two version strings. If either argument is not a String,
37       * this method returns 0.
38       *
39       * @throws IllegalArgumentException if either argument is a String,
40       * but does not match {@link #VALID_VERSION_PATTERN}.
41       * @see #isValidVersionString(String)
42       */
43      public int compare(Object o1, Object o2)
44      {
45          if (!(o1 instanceof String)) return 0;
46          if (!(o2 instanceof String)) return 0;
47  
48          return compare((String) o1, (String) o2);
49      }
50  
51      /**
52       * Compares two version strings using the algorithm described above.
53       *
54       * @return <tt>-1</tt> if version1 is before version2, <tt>1</tt> if version2 is before
55       * version1, or <tt>0</tt> if the versions are equal.
56       * @throws IllegalArgumentException if either argument does not match {@link #VALID_VERSION_PATTERN}.
57       * @see #isValidVersionString(String)
58       */
59      public int compare(String version1, String version2)
60      {
61          // Get the version numbers, remove all whitespaces
62          String thisVersion = "0";
63          if (StringUtils.isNotEmpty(version1))
64          {
65              thisVersion = version1.replaceAll(" ", "");
66          }
67          String compareVersion = "0";
68          if (StringUtils.isNotEmpty(version2))
69          {
70              compareVersion = version2.replaceAll(" ", "");
71          }
72  
73          if (!thisVersion.matches(VALID_VERSION_PATTERN) || !compareVersion.matches(VALID_VERSION_PATTERN))
74          {
75              throw new IllegalArgumentException(
76                  "Version number '" + thisVersion + "' cannot be compared to '" + compareVersion + "'");
77          }
78  
79          // Split the version numbers
80          String[] v1 = thisVersion.split(DELIMITER_PATTERN);
81          String[] v2 = compareVersion.split(DELIMITER_PATTERN);
82  
83          Comparator 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              String component1 = i >= v1.length ? "0" : v1[i];
89              String component2 = i >= v2.length ? "0" : v2[i];
90  
91              if (componentComparator.compare(component1, component2) != 0)
92                  return componentComparator.compare(component1, component2);
93          }
94  
95          return 0;
96      }
97  
98      private class VersionStringComponentComparator implements Comparator
99      {
100         public static final int FIRST_GREATER = 1;
101         public static final int SECOND_GREATER = -1;
102 
103         public int compare(Object o1, Object o2)
104         {
105             if (!(o1 instanceof String)) return 0;
106             if (!(o2 instanceof String)) return 0;
107 
108             return compare((String) o1, (String) o2);
109         }
110 
111         public int compare(String component1, String component2)
112         {
113             if (component1.equalsIgnoreCase(component2)) return 0;
114 
115             if (isInteger(component1) && isInteger(component2))
116             {
117                 // both numbers -- parse and compare
118                 if (Integer.parseInt(component1) > Integer.parseInt(component2)) return FIRST_GREATER;
119                 if (Integer.parseInt(component2) > Integer.parseInt(component1)) return SECOND_GREATER;
120                 return 0;
121             }
122 
123             // 2.3-alpha < 2.3.0
124             if ("0".equals(component1)) return FIRST_GREATER;
125             if ("0".equals(component2)) return SECOND_GREATER;
126 
127             // 2.3a < 2.3
128             if (isInteger(component1) && component2.startsWith(component1)) return FIRST_GREATER;
129             if (isInteger(component2) && component1.startsWith(component2)) return SECOND_GREATER;
130 
131             // 2.3a < 2.3b
132             return component1.compareToIgnoreCase(component2);
133         }
134 
135         private boolean isInteger(String string)
136         {
137             return string.matches("\\d+");
138         }
139     }
140 }