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