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