View Javadoc
1   package com.atlassian.plugin.util;
2   
3   import com.google.common.base.Objects;
4   
5   import java.util.Comparator;
6   import java.util.regex.Matcher;
7   import java.util.regex.Pattern;
8   
9   import static com.atlassian.plugin.util.VersionStringComparator.VALID_VERSION_PATTERN;
10  import static com.google.common.base.Preconditions.checkNotNull;
11  import static com.google.common.base.Preconditions.checkState;
12  
13  /**
14   * Represents a version range. Version ranges can be built programmatically or parsed with the
15   * following definition:
16   * <table>
17   * <thead>
18   * <th>Range</th>
19   * <th>Meaning</th>
20   * </thead>
21   * <tbody>
22   * <tr>
23   * <td>1.0</td>
24   * <td>x >= 1.0</td>
25   * </tr>
26   * <tr>
27   * <td>(,1.0]</td>
28   * <td>x <= 1.0</td>
29   * </tr>
30   * <tr>
31   * <td>(,1.0)</td>
32   * <td>x < 1.0</td>
33   * </tr>
34   * <tr>
35   * <td>[1.0]</td>
36   * <td>x == 1.0</td>
37   * </tr>
38   * <tr>
39   * <td>[1.0,)</td>
40   * <td>x >= 1.0</td>
41   * </tr>
42   * <tr>
43   * <td>(1.0,)</td>
44   * <td>x > 1.0</td>
45   * </tr>
46   * <tr>
47   * <td>(1.0,2.0)</td>
48   * <td>1.0 < x < 2.0</td>
49   * </tr>
50   * <tr>
51   * <td>[1.0,2.0]</td>
52   * <td>1.0 <= x <= 2.0</td>
53   * </tr>
54   * </tbody>
55   * </table>
56   *
57   * @since 3.0
58   */
59  public abstract class VersionRange {
60      private static final Pattern RANGE_PATTERN = Pattern.compile("(\\(|\\[)?(" + VALID_VERSION_PATTERN + ")?(?:,(" + VALID_VERSION_PATTERN + ")?)?(\\)|\\])?");
61  
62      private VersionRange() {
63      }
64  
65      abstract boolean isInRange(String version);
66  
67      public VersionRange or(VersionRange other) {
68          return new OrVersionRange(this, other);
69      }
70  
71      public static VersionRange empty() {
72          return new EmptyVersionRange();
73      }
74  
75      public static VersionRange all() {
76          return new AllVersionRange();
77      }
78  
79      public static VersionRange parse(String range) {
80          final Matcher matcher = RANGE_PATTERN.matcher(range);
81          checkState(matcher.matches(), "Range '" + range + "' doesn't match pattern " + RANGE_PATTERN.pattern());
82  
83          final String leftParenthesis = matcher.group(1);
84          final String leftVersion = matcher.group(2);
85          final String rightVersion = matcher.group(3);
86          final String rightParenthesis = matcher.group(4);
87  
88          checkState(leftVersion != null || rightVersion != null, "No version configured for range!");
89  
90          if (leftParenthesis == null) {
91              checkState(leftVersion != null);
92              checkState(rightParenthesis == null);
93              checkState(rightVersion == null);
94              return VersionRange.include(leftVersion).unbounded();
95          } else if (leftParenthesis.equals("[") && rightParenthesis.equals("]") && rightVersion == null) // single version
96          {
97              return VersionRange.single(leftVersion);
98          } else {
99              final ActualVersionRangeBuilder builder;
100             if (leftParenthesis.equals("[")) {
101                 checkState(leftVersion != null);
102                 builder = VersionRange.include(leftVersion);
103             } else if (leftParenthesis.equals("(")) {
104                 if (leftVersion != null) {
105                     builder = VersionRange.exclude(leftVersion);
106                 } else {
107                     builder = VersionRange.unbounded();
108                 }
109             } else {
110                 throw new IllegalStateException("Incorrect start of range! " + leftParenthesis);
111             }
112             if (rightParenthesis.equals("]")) {
113                 checkState(rightVersion != null);
114                 return builder.include(rightVersion);
115             } else if (rightParenthesis.equals(")")) {
116                 if (rightVersion != null) {
117                     return builder.exclude(rightVersion);
118                 } else {
119                     return builder.unbounded();
120                 }
121             } else {
122                 throw new IllegalStateException("Incorrect ent of range! " + rightParenthesis);
123             }
124         }
125     }
126 
127     public static VersionRange single(String version) {
128         return new SingleVersionRange(version);
129     }
130 
131     public static ActualVersionRangeBuilder include(String version) {
132         return new ActualVersionRangeBuilder(true, version);
133     }
134 
135     public static ActualVersionRangeBuilder exclude(String version) {
136         return new ActualVersionRangeBuilder(false, version);
137     }
138 
139     public static ActualVersionRangeBuilder unbounded() {
140         return new ActualVersionRangeBuilder(true, null);
141     }
142 
143     private static class SingleVersionRange extends VersionRange {
144         private final String version;
145 
146         private SingleVersionRange(String version) {
147             this.version = checkNotNull(version);
148         }
149 
150         @Override
151         boolean isInRange(String v) {
152             return newVersionComparator().compare(this.version, v) == 0;
153         }
154 
155         @Override
156         public int hashCode() {
157             return Objects.hashCode(version);
158         }
159 
160         @Override
161         public boolean equals(Object obj) {
162             if (obj == null) {
163                 return false;
164             }
165             if (getClass() != obj.getClass()) {
166                 return false;
167             }
168             final SingleVersionRange that = (SingleVersionRange) obj;
169             return Objects.equal(this.version, that.version);
170         }
171 
172         @Override
173         public String toString() {
174             return "[" + version + "]";
175         }
176     }
177 
178     public static final class ActualVersionRangeBuilder {
179         private final boolean leftIncluded;
180         private final String leftVersion;
181 
182         public ActualVersionRangeBuilder(boolean leftIncluded, String leftVersion) {
183             this.leftIncluded = leftIncluded;
184             this.leftVersion = leftVersion;
185         }
186 
187         public VersionRange include(String version) {
188             return newRange(version, true);
189         }
190 
191         public VersionRange exclude(String version) {
192             return newRange(version, false);
193         }
194 
195         private VersionRange newRange(String version, boolean rightIncluded) {
196             if (leftVersion != null) {
197                 return newActualRange(version, rightIncluded);
198             } else {
199                 return newLeftUnboundedRange(version, rightIncluded);
200             }
201         }
202 
203         private LeftUnboundedVersionRange newLeftUnboundedRange(String version, boolean rightIncluded) {
204             return new LeftUnboundedVersionRange(rightIncluded, version);
205         }
206 
207         private ActualVersionRange newActualRange(String version, boolean rightIncluded) {
208             return new ActualVersionRange(leftIncluded, leftVersion, rightIncluded, version);
209         }
210 
211         public VersionRange unbounded() {
212             if (leftVersion == null) {
213                 throw new IllegalStateException();
214             }
215             return new RightUnboundedVersionRange(leftIncluded, leftVersion);
216         }
217     }
218 
219     private static final class ActualVersionRange extends VersionRange {
220         private final boolean leftIncluded;
221         private final String leftVersion;
222         private final boolean rightIncluded;
223         private final String rightVersion;
224 
225         private ActualVersionRange(
226                 boolean leftIncluded, String leftVersion,
227                 boolean rightIncluded, String rightVersion) {
228             this.leftIncluded = leftIncluded;
229             this.leftVersion = checkNotNull(leftVersion);
230             this.rightIncluded = rightIncluded;
231             this.rightVersion = checkNotNull(rightVersion);
232         }
233 
234         @Override
235         boolean isInRange(String v) {
236             return isGreaterThan(leftIncluded, leftVersion, v)
237                     && isLowerThan(v, rightVersion, rightIncluded);
238         }
239 
240         @Override
241         public int hashCode() {
242             return Objects.hashCode(leftIncluded, leftVersion, rightIncluded, rightVersion);
243         }
244 
245         @Override
246         public boolean equals(Object obj) {
247             if (obj == null) {
248                 return false;
249             }
250             if (getClass() != obj.getClass()) {
251                 return false;
252             }
253             final ActualVersionRange that = (ActualVersionRange) obj;
254             return Objects.equal(this.leftIncluded, that.leftIncluded)
255                     && Objects.equal(this.leftVersion, that.leftVersion)
256                     && Objects.equal(this.rightIncluded, that.rightIncluded)
257                     && Objects.equal(this.rightVersion, that.rightVersion);
258         }
259 
260         @Override
261         public String toString() {
262             return (leftIncluded ? "[" : "(") + leftVersion + "," + rightVersion + (rightIncluded ? "]" : ")");
263         }
264     }
265 
266     private static final class LeftUnboundedVersionRange extends VersionRange {
267         private final boolean rightIncluded;
268         private final String rightVersion;
269 
270         private LeftUnboundedVersionRange(boolean rightIncluded, String rightVersion) {
271             this.rightIncluded = rightIncluded;
272             this.rightVersion = checkNotNull(rightVersion);
273         }
274 
275         @Override
276         boolean isInRange(String v) {
277             return isLowerThan(v, rightVersion, rightIncluded);
278         }
279 
280         @Override
281         public int hashCode() {
282             return Objects.hashCode(rightIncluded, rightVersion);
283         }
284 
285         @Override
286         public boolean equals(Object obj) {
287             if (obj == null) {
288                 return false;
289             }
290             if (getClass() != obj.getClass()) {
291                 return false;
292             }
293             final LeftUnboundedVersionRange that = (LeftUnboundedVersionRange) obj;
294             return Objects.equal(this.rightIncluded, that.rightIncluded)
295                     && Objects.equal(this.rightVersion, that.rightVersion);
296         }
297 
298         @Override
299         public String toString() {
300             return "(," + rightVersion + (rightIncluded ? "]" : ")");
301         }
302     }
303 
304     private static final class RightUnboundedVersionRange extends VersionRange {
305         private final boolean leftIncluded;
306         private final String leftVersion;
307 
308         private RightUnboundedVersionRange(boolean leftIncluded, String leftVersion) {
309             this.leftIncluded = leftIncluded;
310             this.leftVersion = checkNotNull(leftVersion);
311         }
312 
313         @Override
314         boolean isInRange(String v) {
315             return isGreaterThan(leftIncluded, leftVersion, v);
316         }
317 
318         @Override
319         public int hashCode() {
320             return Objects.hashCode(leftIncluded, leftVersion);
321         }
322 
323         @Override
324         public boolean equals(Object obj) {
325             if (obj == null) {
326                 return false;
327             }
328             if (getClass() != obj.getClass()) {
329                 return false;
330             }
331             final RightUnboundedVersionRange that = (RightUnboundedVersionRange) obj;
332             return Objects.equal(this.leftIncluded, that.leftIncluded)
333                     && Objects.equal(this.leftVersion, that.leftVersion);
334         }
335 
336         @Override
337         public String toString() {
338             return (leftIncluded ? "[" : "(") + leftVersion + ",)";
339         }
340     }
341 
342     private static final class AllVersionRange extends VersionRange {
343         @Override
344         boolean isInRange(String version) {
345             return true;
346         }
347 
348         @Override
349         public int hashCode() {
350             return 1;
351         }
352 
353         @Override
354         public boolean equals(Object obj) {
355             return obj != null && getClass() == obj.getClass();
356         }
357 
358         @Override
359         public String toString() {
360             return "(,)";
361         }
362     }
363 
364     private static final class EmptyVersionRange extends VersionRange {
365         @Override
366         boolean isInRange(String version) {
367             return false;
368         }
369 
370         @Override
371         public int hashCode() {
372             return 2;
373         }
374 
375         @Override
376         public boolean equals(Object obj) {
377             return obj != null && getClass() == obj.getClass();
378         }
379 
380         @Override
381         public String toString() {
382             return "()";
383         }
384     }
385 
386     private static final class OrVersionRange extends VersionRange {
387         private final VersionRange or1;
388         private final VersionRange or2;
389 
390         private OrVersionRange(VersionRange or1, VersionRange or2) {
391             this.or1 = checkNotNull(or1);
392             this.or2 = checkNotNull(or2);
393         }
394 
395         @Override
396         boolean isInRange(String v) {
397             return or1.isInRange(v) || or2.isInRange(v);
398         }
399 
400         @Override
401         public int hashCode() {
402             return Objects.hashCode(or1, or2);
403         }
404 
405         @Override
406         public boolean equals(Object obj) {
407             if (obj == null) {
408                 return false;
409             }
410             if (getClass() != obj.getClass()) {
411                 return false;
412             }
413             final OrVersionRange that = (OrVersionRange) obj;
414             return Objects.equal(this.or1, that.or1)
415                     && Objects.equal(this.or2, that.or2);
416         }
417 
418         @Override
419         public String toString() {
420             return or1 + "," + or2;
421         }
422     }
423 
424 
425     private static boolean isLowerThan(String version, String rightVersion, boolean rightIncluded) {
426         final int rightCompare = newVersionComparator().compare(rightVersion, version);
427         return rightCompare > 0 || (rightIncluded && rightCompare == 0);
428     }
429 
430     private static boolean isGreaterThan(boolean leftIncluded, String leftVersion, String version) {
431         final int leftCompare = newVersionComparator().compare(version, leftVersion);
432         return leftCompare > 0 || (leftIncluded && leftCompare == 0);
433     }
434 
435     private static Comparator<String> newVersionComparator() {
436         return new VersionStringComparator();
437     }
438 }