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 }