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