View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.artifact.versioning;
20  
21  import java.util.ArrayList;
22  import java.util.Collections;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Objects;
27  import java.util.WeakHashMap;
28  
29  import org.apache.maven.artifact.Artifact;
30  
31  /**
32   * Construct a version range from a specification.
33   *
34   */
35  public class VersionRange {
36      private static final Map<String, VersionRange> CACHE_SPEC = Collections.synchronizedMap(new WeakHashMap<>());
37  
38      private static final Map<String, VersionRange> CACHE_VERSION = Collections.synchronizedMap(new WeakHashMap<>());
39  
40      private final ArtifactVersion recommendedVersion;
41  
42      private final List<Restriction> restrictions;
43  
44      private VersionRange(ArtifactVersion recommendedVersion, List<Restriction> restrictions) {
45          this.recommendedVersion = recommendedVersion;
46          this.restrictions = restrictions;
47      }
48  
49      public ArtifactVersion getRecommendedVersion() {
50          return recommendedVersion;
51      }
52  
53      public List<Restriction> getRestrictions() {
54          return restrictions;
55      }
56  
57      /**
58       * @deprecated VersionRange is immutable, cloning is not useful and even more an issue against the cache
59       * @return a clone
60       */
61      @Deprecated
62      public VersionRange cloneOf() {
63          List<Restriction> copiedRestrictions = null;
64  
65          if (restrictions != null) {
66              copiedRestrictions = new ArrayList<>();
67  
68              if (!restrictions.isEmpty()) {
69                  copiedRestrictions.addAll(restrictions);
70              }
71          }
72  
73          return new VersionRange(recommendedVersion, copiedRestrictions);
74      }
75  
76      /**
77       * <p>
78       * Create a version range from a string representation
79       * </p>
80       * Some spec examples are:
81       * <ul>
82       * <li><code>1.0</code> Version 1.0 as a recommended version</li>
83       * <li><code>[1.0]</code> Version 1.0 explicitly only</li>
84       * <li><code>[1.0,2.0)</code> Versions 1.0 (included) to 2.0 (not included)</li>
85       * <li><code>[1.0,2.0]</code> Versions 1.0 to 2.0 (both included)</li>
86       * <li><code>[1.5,)</code> Versions 1.5 and higher</li>
87       * <li><code>(,1.0],[1.2,)</code> Versions up to 1.0 (included) and 1.2 or higher</li>
88       * </ul>
89       *
90       * @param spec string representation of a version or version range
91       * @return a new {@link VersionRange} object that represents the spec
92       * @throws InvalidVersionSpecificationException if invalid version specification
93       */
94      public static VersionRange createFromVersionSpec(String spec) throws InvalidVersionSpecificationException {
95          if (spec == null) {
96              return null;
97          }
98  
99          VersionRange cached = CACHE_SPEC.get(spec);
100         if (cached != null) {
101             return cached;
102         }
103 
104         List<Restriction> restrictions = new ArrayList<>();
105         String process = spec;
106         ArtifactVersion version = null;
107         ArtifactVersion upperBound = null;
108         ArtifactVersion lowerBound = null;
109 
110         while (process.startsWith("[") || process.startsWith("(")) {
111             int index1 = process.indexOf(')');
112             int index2 = process.indexOf(']');
113 
114             int index = index2;
115             if (index2 < 0 || index1 < index2) {
116                 if (index1 >= 0) {
117                     index = index1;
118                 }
119             }
120 
121             if (index < 0) {
122                 throw new InvalidVersionSpecificationException("Unbounded range: " + spec);
123             }
124 
125             Restriction restriction = parseRestriction(process.substring(0, index + 1));
126             if (lowerBound == null) {
127                 lowerBound = restriction.getLowerBound();
128             }
129             if (upperBound != null) {
130                 if (restriction.getLowerBound() == null
131                         || restriction.getLowerBound().compareTo(upperBound) < 0) {
132                     throw new InvalidVersionSpecificationException("Ranges overlap: " + spec);
133                 }
134             }
135             restrictions.add(restriction);
136             upperBound = restriction.getUpperBound();
137 
138             process = process.substring(index + 1).trim();
139 
140             if (process.startsWith(",")) {
141                 process = process.substring(1).trim();
142             }
143         }
144 
145         if (!process.isEmpty()) {
146             if (!restrictions.isEmpty()) {
147                 throw new InvalidVersionSpecificationException(
148                         "Only fully-qualified sets allowed in multiple set scenario: " + spec);
149             } else {
150                 version = new DefaultArtifactVersion(process);
151                 restrictions.add(Restriction.EVERYTHING);
152             }
153         }
154 
155         cached = new VersionRange(version, restrictions);
156         CACHE_SPEC.put(spec, cached);
157         return cached;
158     }
159 
160     private static Restriction parseRestriction(String spec) throws InvalidVersionSpecificationException {
161         boolean lowerBoundInclusive = spec.startsWith("[");
162         boolean upperBoundInclusive = spec.endsWith("]");
163 
164         String process = spec.substring(1, spec.length() - 1).trim();
165 
166         Restriction restriction;
167 
168         int index = process.indexOf(',');
169 
170         if (index < 0) {
171             if (!lowerBoundInclusive || !upperBoundInclusive) {
172                 throw new InvalidVersionSpecificationException("Single version must be surrounded by []: " + spec);
173             }
174 
175             ArtifactVersion version = new DefaultArtifactVersion(process);
176 
177             restriction = new Restriction(version, lowerBoundInclusive, version, upperBoundInclusive);
178         } else {
179             String lowerBound = process.substring(0, index).trim();
180             String upperBound = process.substring(index + 1).trim();
181 
182             ArtifactVersion lowerVersion = null;
183             if (!lowerBound.isEmpty()) {
184                 lowerVersion = new DefaultArtifactVersion(lowerBound);
185             }
186             ArtifactVersion upperVersion = null;
187             if (!upperBound.isEmpty()) {
188                 upperVersion = new DefaultArtifactVersion(upperBound);
189             }
190 
191             if (upperVersion != null && lowerVersion != null) {
192                 int result = upperVersion.compareTo(lowerVersion);
193                 if (result < 0 || (result == 0 && (!lowerBoundInclusive || !upperBoundInclusive))) {
194                     throw new InvalidVersionSpecificationException("Range defies version ordering: " + spec);
195                 }
196             }
197 
198             restriction = new Restriction(lowerVersion, lowerBoundInclusive, upperVersion, upperBoundInclusive);
199         }
200 
201         return restriction;
202     }
203 
204     public static VersionRange createFromVersion(String version) {
205         VersionRange cached = CACHE_VERSION.get(version);
206         if (cached == null) {
207             List<Restriction> restrictions = Collections.emptyList();
208             cached = new VersionRange(new DefaultArtifactVersion(version), restrictions);
209             CACHE_VERSION.put(version, cached);
210         }
211         return cached;
212     }
213 
214     /**
215      * Creates and returns a new <code>VersionRange</code> that is a restriction of this
216      * version range and the specified version range.
217      * <p>
218      * Note: Precedence is given to the recommended version from this version range over the
219      * recommended version from the specified version range.
220      * </p>
221      *
222      * @param restriction the <code>VersionRange</code> that will be used to restrict this version
223      *                    range.
224      * @return the <code>VersionRange</code> that is a restriction of this version range and the
225      *         specified version range.
226      *         <p>
227      *         The restrictions of the returned version range will be an intersection of the restrictions
228      *         of this version range and the specified version range if both version ranges have
229      *         restrictions. Otherwise, the restrictions on the returned range will be empty.
230      *         </p>
231      *         <p>
232      *         The recommended version of the returned version range will be the recommended version of
233      *         this version range, provided that ranges falls within the intersected restrictions. If
234      *         the restrictions are empty, this version range's recommended version is used if it is not
235      *         <code>null</code>. If it is <code>null</code>, the specified version range's recommended
236      *         version is used (provided it is non-<code>null</code>). If no recommended version can be
237      *         obtained, the returned version range's recommended version is set to <code>null</code>.
238      *         </p>
239      * @throws NullPointerException if the specified <code>VersionRange</code> is
240      *                              <code>null</code>.
241      */
242     public VersionRange restrict(VersionRange restriction) {
243         List<Restriction> r1 = this.restrictions;
244         List<Restriction> r2 = restriction.restrictions;
245         List<Restriction> restrictions;
246 
247         if (r1.isEmpty() || r2.isEmpty()) {
248             restrictions = Collections.emptyList();
249         } else {
250             restrictions = Collections.unmodifiableList(intersection(r1, r2));
251         }
252 
253         ArtifactVersion version = null;
254         if (restrictions.size() > 0) {
255             for (Restriction r : restrictions) {
256                 if (recommendedVersion != null && r.containsVersion(recommendedVersion)) {
257                     // if we find the original, use that
258                     version = recommendedVersion;
259                     break;
260                 } else if (version == null
261                         && restriction.getRecommendedVersion() != null
262                         && r.containsVersion(restriction.getRecommendedVersion())) {
263                     // use this if we can, but prefer the original if possible
264                     version = restriction.getRecommendedVersion();
265                 }
266             }
267         }
268         // Either the original or the specified version ranges have no restrictions
269         else if (recommendedVersion != null) {
270             // Use the original recommended version since it exists
271             version = recommendedVersion;
272         } else if (restriction.recommendedVersion != null) {
273             // Use the recommended version from the specified VersionRange since there is no
274             // original recommended version
275             version = restriction.recommendedVersion;
276         }
277         /* TODO should throw this immediately, but need artifact
278                 else
279                 {
280                     throw new OverConstrainedVersionException( "Restricting incompatible version ranges" );
281                 }
282         */
283 
284         return new VersionRange(version, restrictions);
285     }
286 
287     private List<Restriction> intersection(List<Restriction> r1, List<Restriction> r2) {
288         List<Restriction> restrictions = new ArrayList<>(r1.size() + r2.size());
289         Iterator<Restriction> i1 = r1.iterator();
290         Iterator<Restriction> i2 = r2.iterator();
291         Restriction res1 = i1.next();
292         Restriction res2 = i2.next();
293 
294         boolean done = false;
295         while (!done) {
296             if (res1.getLowerBound() == null
297                     || res2.getUpperBound() == null
298                     || res1.getLowerBound().compareTo(res2.getUpperBound()) <= 0) {
299                 if (res1.getUpperBound() == null
300                         || res2.getLowerBound() == null
301                         || res1.getUpperBound().compareTo(res2.getLowerBound()) >= 0) {
302                     ArtifactVersion lower;
303                     ArtifactVersion upper;
304                     boolean lowerInclusive;
305                     boolean upperInclusive;
306 
307                     // overlaps
308                     if (res1.getLowerBound() == null) {
309                         lower = res2.getLowerBound();
310                         lowerInclusive = res2.isLowerBoundInclusive();
311                     } else if (res2.getLowerBound() == null) {
312                         lower = res1.getLowerBound();
313                         lowerInclusive = res1.isLowerBoundInclusive();
314                     } else {
315                         int comparison = res1.getLowerBound().compareTo(res2.getLowerBound());
316                         if (comparison < 0) {
317                             lower = res2.getLowerBound();
318                             lowerInclusive = res2.isLowerBoundInclusive();
319                         } else if (comparison == 0) {
320                             lower = res1.getLowerBound();
321                             lowerInclusive = res1.isLowerBoundInclusive() && res2.isLowerBoundInclusive();
322                         } else {
323                             lower = res1.getLowerBound();
324                             lowerInclusive = res1.isLowerBoundInclusive();
325                         }
326                     }
327 
328                     if (res1.getUpperBound() == null) {
329                         upper = res2.getUpperBound();
330                         upperInclusive = res2.isUpperBoundInclusive();
331                     } else if (res2.getUpperBound() == null) {
332                         upper = res1.getUpperBound();
333                         upperInclusive = res1.isUpperBoundInclusive();
334                     } else {
335                         int comparison = res1.getUpperBound().compareTo(res2.getUpperBound());
336                         if (comparison < 0) {
337                             upper = res1.getUpperBound();
338                             upperInclusive = res1.isUpperBoundInclusive();
339                         } else if (comparison == 0) {
340                             upper = res1.getUpperBound();
341                             upperInclusive = res1.isUpperBoundInclusive() && res2.isUpperBoundInclusive();
342                         } else {
343                             upper = res2.getUpperBound();
344                             upperInclusive = res2.isUpperBoundInclusive();
345                         }
346                     }
347 
348                     // don't add if they are equal and one is not inclusive
349                     if (lower == null || upper == null || lower.compareTo(upper) != 0) {
350                         restrictions.add(new Restriction(lower, lowerInclusive, upper, upperInclusive));
351                     } else if (lowerInclusive && upperInclusive) {
352                         restrictions.add(new Restriction(lower, lowerInclusive, upper, upperInclusive));
353                     }
354 
355                     //noinspection ObjectEquality
356                     if (upper == res2.getUpperBound()) {
357                         // advance res2
358                         if (i2.hasNext()) {
359                             res2 = i2.next();
360                         } else {
361                             done = true;
362                         }
363                     } else {
364                         // advance res1
365                         if (i1.hasNext()) {
366                             res1 = i1.next();
367                         } else {
368                             done = true;
369                         }
370                     }
371                 } else {
372                     // move on to next in r1
373                     if (i1.hasNext()) {
374                         res1 = i1.next();
375                     } else {
376                         done = true;
377                     }
378                 }
379             } else {
380                 // move on to next in r2
381                 if (i2.hasNext()) {
382                     res2 = i2.next();
383                 } else {
384                     done = true;
385                 }
386             }
387         }
388 
389         return restrictions;
390     }
391 
392     public ArtifactVersion getSelectedVersion(Artifact artifact) throws OverConstrainedVersionException {
393         ArtifactVersion version;
394         if (recommendedVersion != null) {
395             version = recommendedVersion;
396         } else {
397             if (restrictions.size() == 0) {
398                 throw new OverConstrainedVersionException("The artifact has no valid ranges", artifact);
399             }
400 
401             version = null;
402         }
403         return version;
404     }
405 
406     public boolean isSelectedVersionKnown(Artifact artifact) throws OverConstrainedVersionException {
407         boolean value = false;
408         if (recommendedVersion != null) {
409             value = true;
410         } else {
411             if (restrictions.size() == 0) {
412                 throw new OverConstrainedVersionException("The artifact has no valid ranges", artifact);
413             }
414         }
415         return value;
416     }
417 
418     public String toString() {
419         if (recommendedVersion != null) {
420             return recommendedVersion.toString();
421         } else {
422             StringBuilder buf = new StringBuilder();
423             for (Iterator<Restriction> i = restrictions.iterator(); i.hasNext(); ) {
424                 Restriction r = i.next();
425 
426                 buf.append(r.toString());
427 
428                 if (i.hasNext()) {
429                     buf.append(',');
430                 }
431             }
432             return buf.toString();
433         }
434     }
435 
436     public ArtifactVersion matchVersion(List<ArtifactVersion> versions) {
437         // TODO could be more efficient by sorting the list and then moving along the restrictions in order?
438 
439         ArtifactVersion matched = null;
440         for (ArtifactVersion version : versions) {
441             if (containsVersion(version)) {
442                 // valid - check if it is greater than the currently matched version
443                 if (matched == null || version.compareTo(matched) > 0) {
444                     matched = version;
445                 }
446             }
447         }
448         return matched;
449     }
450 
451     public boolean containsVersion(ArtifactVersion version) {
452         for (Restriction restriction : restrictions) {
453             if (restriction.containsVersion(version)) {
454                 return true;
455             }
456         }
457         return false;
458     }
459 
460     public boolean hasRestrictions() {
461         return !restrictions.isEmpty() && recommendedVersion == null;
462     }
463 
464     public boolean equals(Object obj) {
465         if (this == obj) {
466             return true;
467         }
468         if (!(obj instanceof VersionRange)) {
469             return false;
470         }
471         VersionRange other = (VersionRange) obj;
472 
473         return Objects.equals(recommendedVersion, other.recommendedVersion)
474                 && Objects.equals(restrictions, other.restrictions);
475     }
476 
477     public int hashCode() {
478         int hash = 7;
479         hash = 31 * hash + (recommendedVersion == null ? 0 : recommendedVersion.hashCode());
480         hash = 31 * hash + (restrictions == null ? 0 : restrictions.hashCode());
481         return hash;
482     }
483 }