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