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<String>
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 = COMPONENT_PATTERN + "(" + DELIMITER_PATTERN + COMPONENT_PATTERN + ")*";
28  
29      public static boolean isValidVersionString(final String version)
30      {
31          return (version != null) && version.matches(VALID_VERSION_PATTERN);
32      }
33  
34      /**
35       * Compares two version strings. If either argument is not a String,
36       * this method returns 0.
37       *
38       * @throws IllegalArgumentException if either argument is a String,
39       * but does not match {@link #VALID_VERSION_PATTERN}.
40       * @see #isValidVersionString(String)
41       */
42      //    public int compare(Object o1, Object o2)
43      //    {
44      //        if (!(o1 instanceof String)) return 0;
45      //        if (!(o2 instanceof String)) return 0;
46      //
47      //        return compare((String) o1, (String) o2);
48      //    }
49      /**
50       * Compares two version strings using the algorithm described above.
51       *
52       * @return <tt>-1</tt> if version1 is before version2, <tt>1</tt> if version2 is before
53       * version1, or <tt>0</tt> if the versions are equal.
54       * @throws IllegalArgumentException if either argument does not match {@link #VALID_VERSION_PATTERN}.
55       * @see #isValidVersionString(String)
56       */
57      public int compare(final String version1, final String version2)
58      {
59          // Get the version numbers, remove all whitespaces
60          String thisVersion = "0";
61          if (StringUtils.isNotEmpty(version1))
62          {
63              thisVersion = version1.replaceAll(" ", "");
64          }
65          String compareVersion = "0";
66          if (StringUtils.isNotEmpty(version2))
67          {
68              compareVersion = version2.replaceAll(" ", "");
69          }
70  
71          if (!thisVersion.matches(VALID_VERSION_PATTERN) || !compareVersion.matches(VALID_VERSION_PATTERN))
72          {
73              throw new IllegalArgumentException("Version number '" + thisVersion + "' cannot be compared to '" + compareVersion + "'");
74          }
75  
76          // Split the version numbers
77          final String[] v1 = thisVersion.split(DELIMITER_PATTERN);
78          final String[] v2 = compareVersion.split(DELIMITER_PATTERN);
79  
80          final Comparator<String> componentComparator = new VersionStringComponentComparator();
81  
82          // Compare each place, until we find a difference and then return. If empty, assume zero.
83          for (int i = 0; i < (v1.length > v2.length ? v1.length : v2.length); i++)
84          {
85              final String component1 = i >= v1.length ? "0" : v1[i];
86              final String component2 = i >= v2.length ? "0" : v2[i];
87  
88              if (componentComparator.compare(component1, component2) != 0)
89              {
90                  return componentComparator.compare(component1, component2);
91              }
92          }
93  
94          return 0;
95      }
96  
97      private class VersionStringComponentComparator implements Comparator<String>
98      {
99          public static final int FIRST_GREATER = 1;
100         public static final int SECOND_GREATER = -1;
101 
102         //        public int compare(Object o1, Object o2)
103         //        {
104         //            if (!(o1 instanceof String)) return 0;
105         //            if (!(o2 instanceof String)) return 0;
106         //
107         //            return compare((String) o1, (String) o2);
108         //        }
109 
110         public int compare(final String component1, final String component2)
111         {
112             if (component1.equalsIgnoreCase(component2))
113             {
114                 return 0;
115             }
116 
117             if (isInteger(component1) && isInteger(component2))
118             {
119                 // both numbers -- parse and compare
120                 if (Integer.parseInt(component1) > Integer.parseInt(component2))
121                 {
122                     return FIRST_GREATER;
123                 }
124                 if (Integer.parseInt(component2) > Integer.parseInt(component1))
125                 {
126                     return SECOND_GREATER;
127                 }
128                 return 0;
129             }
130 
131             // 2.3-alpha < 2.3.0
132             if ("0".equals(component1))
133             {
134                 return FIRST_GREATER;
135             }
136             if ("0".equals(component2))
137             {
138                 return SECOND_GREATER;
139             }
140 
141             // 2.3a < 2.3
142             if (isInteger(component1) && component2.startsWith(component1))
143             {
144                 return FIRST_GREATER;
145             }
146             if (isInteger(component2) && component1.startsWith(component2))
147             {
148                 return SECOND_GREATER;
149             }
150 
151             // 2.3a < 2.3b
152             return component1.compareToIgnoreCase(component2);
153         }
154 
155         private boolean isInteger(final String string)
156         {
157             return string.matches("\\d+");
158         }
159     }
160 }