View Javadoc

1   package de.matthias_burbach.deputy.core.project;
2   
3   import java.util.ArrayList;
4   import java.util.Comparator;
5   import java.util.Iterator;
6   import java.util.List;
7   
8   /***
9    * Compares versions of Maven projects.
10   *
11   * @author Matthias Burbach
12   */
13  public class VersionComparator implements Comparator {
14      /*
15       * (non-Javadoc)
16       * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
17       */
18      /***
19       * {@inheritDoc}
20       * <p/>
21       * Note: o1 and o2 must both be instances of {@link java.lang.String}.
22       * <p/>
23       * Comparison is such that newer versions are sorted to the tops of lists.
24       */
25      public int compare(final Object o1, final Object o2) {
26          int result = 0;
27          /*
28           * Swap versions to get descending order of versions
29           */
30          String version1 = (String) o2;
31          String version2 = (String) o1;
32          if (!version1.equals(version2)) {
33              /*
34               * OPENEDITION is deprecated, put it at the end of any sorting
35               */
36              if (version1.trim().equalsIgnoreCase("OPENEDITION")) {
37                  return -1;
38              } else if (version2.trim().equalsIgnoreCase("OPENEDITION")) {
39                  return 1;
40              }
41              /*
42               * SNAPSHOT is king
43               */
44              if (version1.trim().equals("SNAPSHOT")) {
45                  return 1;
46              } else if (version2.trim().equals("SNAPSHOT")) {
47                  return -1;
48              }
49              /*
50               * Split the versions into their atomic parts, which are either
51               * integers or alpha strings without any digits.
52               * The characters '.', '-', ' ' and '_' are considered to be
53               * separators.
54               */
55              Iterator version1Parts = getVersionParts(version1).iterator();
56              Iterator version2Parts = getVersionParts(version2).iterator();
57              /*
58               * Now, compare parts in lexicographical order
59               */
60              while (result == 0
61                      && version1Parts.hasNext() && version2Parts.hasNext()) {
62                  String part1 = (String) version1Parts.next();
63                  String part2 = (String) version2Parts.next();
64                  result = compareVersionParts(part1, part2);
65              }
66              if (result == 0) {
67                  /*
68                   * One of the versions has more parts than the other and both
69                   * versions are equal in the common number of parts
70                   */
71                  if (version1Parts.hasNext()) {
72                      /*
73                       * version 1 has more parts than version 2
74                       */
75                      String part1 = (String) version1Parts.next();
76                      if (additionalPartIsUpgrade(part1)) {
77                          result = 1;
78                      } else {
79                          result = -1;
80                      }
81                  } else if (version2Parts.hasNext()) {
82                      /*
83                       * version 2 has more parts than version 1
84                       */
85                      String part2 = (String) version2Parts.next();
86                      if (additionalPartIsUpgrade(part2)) {
87                          result = -1;
88                      } else {
89                          result = 1;
90                      }
91                  }
92              }
93          }
94          return result;
95      }
96  
97      /***
98       * If two versions have different numbers of parts one of the versions
99       * has at least one part more than the other. This one is called the
100      * additional part.
101      * <p/>
102      * Normally, the additional part expresses an upgrade of the version
103      * without that part. Exceptionally, there are additional parts like
104      * 'alpha' or 'beta' which actually downgrade a version.
105      *
106      * @param additionalPart The addtional part to check.
107      * @return <code>true</code> if the additional part has an upgrade meaning.
108      */
109     private boolean additionalPartIsUpgrade(final String additionalPart) {
110         boolean result = true;
111         String value = additionalPart.trim().toLowerCase();
112         if (value.equals("alpha")
113                 || value.equals("beta")
114                 || value.equals("snapshot")) {
115             result = false;
116         }
117         return result;
118     }
119 
120     /***
121      * Compares two version parts with one another.
122      *
123      * @param part1 A version part to compare with part2
124      * @param part2 A version part to compare with part1
125      * @return 0 if the parts are equals, -1 if part1 &lt; part2, 1 else.
126      */
127     private int compareVersionParts(final String part1, final String part2) {
128         final int equal = 0;
129         final int part1IsGreater = 1;
130         final int part2IsGreater = -1;
131 
132         if (part1.equals(part2)) {
133             return equal;
134         }
135 
136         if (part1.equals("SNAPSHOT")) {
137             return part1IsGreater;
138         }
139         if (part2.equals(("SNAPSHOT"))) {
140             return part2IsGreater;
141         }
142 
143         Integer part1AsInt = getAsInteger(part1);
144         Integer part2AsInt = getAsInteger(part2);
145         if (part1AsInt != null && part2AsInt != null) {
146             /*
147              * Both parts are non-numerical,
148              * alphanumerical order is used to decide.
149              * This works fine for a comparison of 'alpha' and 'beta',
150              * for example.
151              */
152             return part1AsInt.compareTo(part2AsInt);
153         }
154 
155         if (part1AsInt == null && part2AsInt == null) {
156             return compareNonNumericalVersionParts(part1, part2);
157         } else if (part1AsInt != null) {
158             return compareNumericalWithNonNumericalPart(part1AsInt, part2);
159         } else {
160             return -1 * compareNumericalWithNonNumericalPart(part2AsInt, part1);
161         }
162     }
163 
164     /***
165      * Compares two version parts where one of them is numerical and the other
166      * is not.
167      *
168      * @param part1AsInt The numerical part to compare with part2.
169      * @param part2 The non-numerical part to compare with part1.
170      * @return 0 if the parts are equals, -1 if part1AsInt &lt; part2, 1 else.
171      */
172     private int compareNumericalWithNonNumericalPart(
173             final Integer part1AsInt, final String part2) {
174         int result = 1;
175         return result;
176     }
177 
178     /***
179      * Compares two version parts wich both are non-numerical.
180      *
181      * @param part1 A part to compare with part2.
182      * @param part2 A part to compare with part1.
183      * @return 0 if the parts are equals, -1 if part1 &lt; part2, 1 else.
184      */
185     private int compareNonNumericalVersionParts(
186             final String part1, final String part2) {
187         int result = 0;
188         result = part1.compareTo(part2);
189         return result;
190     }
191 
192     /***
193      * @param value A string to transform into an integer.
194      * @return The integer parsed from value or <code>null</code>.
195      */
196     private Integer getAsInteger(final String value) {
197         Integer result = null;
198         try {
199             result = new Integer(Integer.parseInt(value));
200         } catch (NumberFormatException e) {
201             //ignore
202             result = null;
203         }
204         return result;
205     }
206 
207     /***
208      * Splits a version string into its version parts.
209      * <p/>
210      * Parts are separated by '.', or '-', or '_'.
211      * <p/>
212      * Additionally are part boundary without a separator is assumed
213      * when a digit is followed by a non-digit and vice versa.
214      *
215      * @param version The version to split into its parts.
216      * @return The list of parts of type {@link java.lang.String}.
217      */
218     private List getVersionParts(final String version) {
219         List result = new ArrayList();
220         String currentPart = null;
221         boolean isDigitMode = true;
222         for (int i = 0; i < version.length(); i++) {
223             char c = version.charAt(i);
224             if (currentPart == null) {
225                 currentPart = c + "";
226                 isDigitMode = Character.isDigit(c);
227             } else if (isVersionPartSeparator(c)) {
228                 result.add(currentPart);
229                 currentPart = null;
230             } else if (Character.isDigit(c)) {
231                 if (isDigitMode) {
232                     currentPart += c + "";
233                 } else {
234                     result.add(currentPart);
235                     currentPart = c + "";
236                     isDigitMode = true;
237                 }
238             } else { // c is not a digit
239                 if (isDigitMode) {
240                     result.add(currentPart);
241                     currentPart = c + "";
242                     isDigitMode = false;
243                 } else {
244                     currentPart += c + "";
245                 }
246             }
247         }
248         if (currentPart != null) {
249             result.add(currentPart);
250         }
251         return result;
252     }
253 
254     /***
255      * @param c The character to test for being a version part separator.
256      * @return <code>true</code> if c is a version part separator.
257      */
258     private boolean isVersionPartSeparator(final char c) {
259         return c == '.' || c == '-' || c == '_' || c == ' ';
260     }
261 }