001 package org.apache.maven.artifact.versioning;
002
003 /*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements. See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership. The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License. You may obtain a copy of the License at
011 *
012 * http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied. See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022 import java.util.ArrayList;
023 import java.util.Collections;
024 import java.util.Iterator;
025 import java.util.List;
026
027 import org.apache.maven.artifact.Artifact;
028
029 /**
030 * Construct a version range from a specification.
031 *
032 * @author <a href="mailto:brett@apache.org">Brett Porter</a>
033 */
034 public class VersionRange
035 {
036 private final ArtifactVersion recommendedVersion;
037
038 private final List<Restriction> restrictions;
039
040 private VersionRange( ArtifactVersion recommendedVersion,
041 List<Restriction> restrictions )
042 {
043 this.recommendedVersion = recommendedVersion;
044 this.restrictions = restrictions;
045 }
046
047 public ArtifactVersion getRecommendedVersion()
048 {
049 return recommendedVersion;
050 }
051
052 public List<Restriction> getRestrictions()
053 {
054 return restrictions;
055 }
056
057 public VersionRange cloneOf()
058 {
059 List<Restriction> copiedRestrictions = null;
060
061 if ( restrictions != null )
062 {
063 copiedRestrictions = new ArrayList<Restriction>();
064
065 if ( !restrictions.isEmpty() )
066 {
067 copiedRestrictions.addAll( restrictions );
068 }
069 }
070
071 return new VersionRange( recommendedVersion, copiedRestrictions );
072 }
073
074 /**
075 * Create a version range from a string representation
076 * <p/>
077 * Some spec examples are
078 * <ul>
079 * <li><code>1.0</code> Version 1.0</li>
080 * <li><code>[1.0,2.0)</code> Versions 1.0 (included) to 2.0 (not included)</li>
081 * <li><code>[1.0,2.0]</code> Versions 1.0 to 2.0 (both included)</li>
082 * <li><code>[1.5,)</code> Versions 1.5 and higher</li>
083 * <li><code>(,1.0],[1.2,)</code> Versions up to 1.0 (included) and 1.2 or higher</li>
084 * </ul>
085 *
086 * @param spec string representation of a version or version range
087 * @return a new {@link VersionRange} object that represents the spec
088 * @throws InvalidVersionSpecificationException
089 *
090 */
091 public static VersionRange createFromVersionSpec( String spec )
092 throws InvalidVersionSpecificationException
093 {
094 if ( spec == null )
095 {
096 return null;
097 }
098
099 List<Restriction> restrictions = new ArrayList<Restriction>();
100 String process = spec;
101 ArtifactVersion version = null;
102 ArtifactVersion upperBound = null;
103 ArtifactVersion lowerBound = null;
104
105 while ( process.startsWith( "[" ) || process.startsWith( "(" ) )
106 {
107 int index1 = process.indexOf( ")" );
108 int index2 = process.indexOf( "]" );
109
110 int index = index2;
111 if ( index2 < 0 || index1 < index2 )
112 {
113 if ( index1 >= 0 )
114 {
115 index = index1;
116 }
117 }
118
119 if ( index < 0 )
120 {
121 throw new InvalidVersionSpecificationException( "Unbounded range: " + spec );
122 }
123
124 Restriction restriction = parseRestriction( process.substring( 0, index + 1 ) );
125 if ( lowerBound == null )
126 {
127 lowerBound = restriction.getLowerBound();
128 }
129 if ( upperBound != null )
130 {
131 if ( restriction.getLowerBound() == null || restriction.getLowerBound().compareTo( upperBound ) < 0 )
132 {
133 throw new InvalidVersionSpecificationException( "Ranges overlap: " + spec );
134 }
135 }
136 restrictions.add( restriction );
137 upperBound = restriction.getUpperBound();
138
139 process = process.substring( index + 1 ).trim();
140
141 if ( process.length() > 0 && process.startsWith( "," ) )
142 {
143 process = process.substring( 1 ).trim();
144 }
145 }
146
147 if ( process.length() > 0 )
148 {
149 if ( restrictions.size() > 0 )
150 {
151 throw new InvalidVersionSpecificationException(
152 "Only fully-qualified sets allowed in multiple set scenario: " + spec );
153 }
154 else
155 {
156 version = new DefaultArtifactVersion( process );
157 restrictions.add( Restriction.EVERYTHING );
158 }
159 }
160
161 return new VersionRange( version, restrictions );
162 }
163
164 private static Restriction parseRestriction( String spec )
165 throws InvalidVersionSpecificationException
166 {
167 boolean lowerBoundInclusive = spec.startsWith( "[" );
168 boolean upperBoundInclusive = spec.endsWith( "]" );
169
170 String process = spec.substring( 1, spec.length() - 1 ).trim();
171
172 Restriction restriction;
173
174 int index = process.indexOf( "," );
175
176 if ( index < 0 )
177 {
178 if ( !lowerBoundInclusive || !upperBoundInclusive )
179 {
180 throw new InvalidVersionSpecificationException( "Single version must be surrounded by []: " + spec );
181 }
182
183 ArtifactVersion version = new DefaultArtifactVersion( process );
184
185 restriction = new Restriction( version, lowerBoundInclusive, version, upperBoundInclusive );
186 }
187 else
188 {
189 String lowerBound = process.substring( 0, index ).trim();
190 String upperBound = process.substring( index + 1 ).trim();
191 if ( lowerBound.equals( upperBound ) )
192 {
193 throw new InvalidVersionSpecificationException( "Range cannot have identical boundaries: " + spec );
194 }
195
196 ArtifactVersion lowerVersion = null;
197 if ( lowerBound.length() > 0 )
198 {
199 lowerVersion = new DefaultArtifactVersion( lowerBound );
200 }
201 ArtifactVersion upperVersion = null;
202 if ( upperBound.length() > 0 )
203 {
204 upperVersion = new DefaultArtifactVersion( upperBound );
205 }
206
207 if ( upperVersion != null && lowerVersion != null && upperVersion.compareTo( lowerVersion ) < 0 )
208 {
209 throw new InvalidVersionSpecificationException( "Range defies version ordering: " + spec );
210 }
211
212 restriction = new Restriction( lowerVersion, lowerBoundInclusive, upperVersion, upperBoundInclusive );
213 }
214
215 return restriction;
216 }
217
218 public static VersionRange createFromVersion( String version )
219 {
220 List<Restriction> restrictions = Collections.emptyList();
221 return new VersionRange( new DefaultArtifactVersion( version ), restrictions );
222 }
223
224 /**
225 * Creates and returns a new <code>VersionRange</code> that is a restriction of this
226 * version range and the specified version range.
227 * <p>
228 * Note: Precedence is given to the recommended version from this version range over the
229 * recommended version from the specified version range.
230 * </p>
231 *
232 * @param restriction the <code>VersionRange</code> that will be used to restrict this version
233 * range.
234 * @return the <code>VersionRange</code> that is a restriction of this version range and the
235 * specified version range.
236 * <p>
237 * The restrictions of the returned version range will be an intersection of the restrictions
238 * of this version range and the specified version range if both version ranges have
239 * restrictions. Otherwise, the restrictions on the returned range will be empty.
240 * </p>
241 * <p>
242 * The recommended version of the returned version range will be the recommended version of
243 * this version range, provided that ranges falls within the intersected restrictions. If
244 * the restrictions are empty, this version range's recommended version is used if it is not
245 * <code>null</code>. If it is <code>null</code>, the specified version range's recommended
246 * version is used (provided it is non-<code>null</code>). If no recommended version can be
247 * obtained, the returned version range's recommended version is set to <code>null</code>.
248 * </p>
249 * @throws NullPointerException if the specified <code>VersionRange</code> is
250 * <code>null</code>.
251 */
252 public VersionRange restrict( VersionRange restriction )
253 {
254 List<Restriction> r1 = this.restrictions;
255 List<Restriction> r2 = restriction.restrictions;
256 List<Restriction> restrictions;
257
258 if ( r1.isEmpty() || r2.isEmpty() )
259 {
260 restrictions = Collections.emptyList();
261 }
262 else
263 {
264 restrictions = intersection( r1, r2 );
265 }
266
267 ArtifactVersion version = null;
268 if ( restrictions.size() > 0 )
269 {
270 for ( Restriction r : restrictions )
271 {
272 if ( recommendedVersion != null && r.containsVersion( recommendedVersion ) )
273 {
274 // if we find the original, use that
275 version = recommendedVersion;
276 break;
277 }
278 else if ( version == null && restriction.getRecommendedVersion() != null
279 && r.containsVersion( restriction.getRecommendedVersion() ) )
280 {
281 // use this if we can, but prefer the original if possible
282 version = restriction.getRecommendedVersion();
283 }
284 }
285 }
286 // Either the original or the specified version ranges have no restrictions
287 else if ( recommendedVersion != null )
288 {
289 // Use the original recommended version since it exists
290 version = recommendedVersion;
291 }
292 else if ( restriction.recommendedVersion != null )
293 {
294 // Use the recommended version from the specified VersionRange since there is no
295 // original recommended version
296 version = restriction.recommendedVersion;
297 }
298 /* TODO: should throw this immediately, but need artifact
299 else
300 {
301 throw new OverConstrainedVersionException( "Restricting incompatible version ranges" );
302 }
303 */
304
305 return new VersionRange( version, restrictions );
306 }
307
308 private List<Restriction> intersection( List<Restriction> r1, List<Restriction> r2 )
309 {
310 List<Restriction> restrictions = new ArrayList<Restriction>( r1.size() + r2.size() );
311 Iterator<Restriction> i1 = r1.iterator();
312 Iterator<Restriction> i2 = r2.iterator();
313 Restriction res1 = i1.next();
314 Restriction res2 = i2.next();
315
316 boolean done = false;
317 while ( !done )
318 {
319 if ( res1.getLowerBound() == null || res2.getUpperBound() == null
320 || res1.getLowerBound().compareTo( res2.getUpperBound() ) <= 0 )
321 {
322 if ( res1.getUpperBound() == null || res2.getLowerBound() == null
323 || res1.getUpperBound().compareTo( res2.getLowerBound() ) >= 0 )
324 {
325 ArtifactVersion lower;
326 ArtifactVersion upper;
327 boolean lowerInclusive;
328 boolean upperInclusive;
329
330 // overlaps
331 if ( res1.getLowerBound() == null )
332 {
333 lower = res2.getLowerBound();
334 lowerInclusive = res2.isLowerBoundInclusive();
335 }
336 else if ( res2.getLowerBound() == null )
337 {
338 lower = res1.getLowerBound();
339 lowerInclusive = res1.isLowerBoundInclusive();
340 }
341 else
342 {
343 int comparison = res1.getLowerBound().compareTo( res2.getLowerBound() );
344 if ( comparison < 0 )
345 {
346 lower = res2.getLowerBound();
347 lowerInclusive = res2.isLowerBoundInclusive();
348 }
349 else if ( comparison == 0 )
350 {
351 lower = res1.getLowerBound();
352 lowerInclusive = res1.isLowerBoundInclusive() && res2.isLowerBoundInclusive();
353 }
354 else
355 {
356 lower = res1.getLowerBound();
357 lowerInclusive = res1.isLowerBoundInclusive();
358 }
359 }
360
361 if ( res1.getUpperBound() == null )
362 {
363 upper = res2.getUpperBound();
364 upperInclusive = res2.isUpperBoundInclusive();
365 }
366 else if ( res2.getUpperBound() == null )
367 {
368 upper = res1.getUpperBound();
369 upperInclusive = res1.isUpperBoundInclusive();
370 }
371 else
372 {
373 int comparison = res1.getUpperBound().compareTo( res2.getUpperBound() );
374 if ( comparison < 0 )
375 {
376 upper = res1.getUpperBound();
377 upperInclusive = res1.isUpperBoundInclusive();
378 }
379 else if ( comparison == 0 )
380 {
381 upper = res1.getUpperBound();
382 upperInclusive = res1.isUpperBoundInclusive() && res2.isUpperBoundInclusive();
383 }
384 else
385 {
386 upper = res2.getUpperBound();
387 upperInclusive = res2.isUpperBoundInclusive();
388 }
389 }
390
391 // don't add if they are equal and one is not inclusive
392 if ( lower == null || upper == null || lower.compareTo( upper ) != 0 )
393 {
394 restrictions.add( new Restriction( lower, lowerInclusive, upper, upperInclusive ) );
395 }
396 else if ( lowerInclusive && upperInclusive )
397 {
398 restrictions.add( new Restriction( lower, lowerInclusive, upper, upperInclusive ) );
399 }
400
401 //noinspection ObjectEquality
402 if ( upper == res2.getUpperBound() )
403 {
404 // advance res2
405 if ( i2.hasNext() )
406 {
407 res2 = i2.next();
408 }
409 else
410 {
411 done = true;
412 }
413 }
414 else
415 {
416 // advance res1
417 if ( i1.hasNext() )
418 {
419 res1 = i1.next();
420 }
421 else
422 {
423 done = true;
424 }
425 }
426 }
427 else
428 {
429 // move on to next in r1
430 if ( i1.hasNext() )
431 {
432 res1 = i1.next();
433 }
434 else
435 {
436 done = true;
437 }
438 }
439 }
440 else
441 {
442 // move on to next in r2
443 if ( i2.hasNext() )
444 {
445 res2 = i2.next();
446 }
447 else
448 {
449 done = true;
450 }
451 }
452 }
453
454 return restrictions;
455 }
456
457 public ArtifactVersion getSelectedVersion( Artifact artifact )
458 throws OverConstrainedVersionException
459 {
460 ArtifactVersion version;
461 if ( recommendedVersion != null )
462 {
463 version = recommendedVersion;
464 }
465 else
466 {
467 if ( restrictions.size() == 0 )
468 {
469 throw new OverConstrainedVersionException( "The artifact has no valid ranges", artifact );
470 }
471
472 version = null;
473 }
474 return version;
475 }
476
477 public boolean isSelectedVersionKnown( Artifact artifact )
478 throws OverConstrainedVersionException
479 {
480 boolean value = false;
481 if ( recommendedVersion != null )
482 {
483 value = true;
484 }
485 else
486 {
487 if ( restrictions.size() == 0 )
488 {
489 throw new OverConstrainedVersionException( "The artifact has no valid ranges", artifact );
490 }
491 }
492 return value;
493 }
494
495 public String toString()
496 {
497 if ( recommendedVersion != null )
498 {
499 return recommendedVersion.toString();
500 }
501 else
502 {
503 StringBuilder buf = new StringBuilder();
504 for ( Iterator<Restriction> i = restrictions.iterator(); i.hasNext(); )
505 {
506 Restriction r = i.next();
507
508 buf.append( r.toString() );
509
510 if ( i.hasNext() )
511 {
512 buf.append( ',' );
513 }
514 }
515 return buf.toString();
516 }
517 }
518
519 public ArtifactVersion matchVersion( List<ArtifactVersion> versions )
520 {
521 // TODO: could be more efficient by sorting the list and then moving along the restrictions in order?
522
523 ArtifactVersion matched = null;
524 for ( ArtifactVersion version : versions )
525 {
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 ( Restriction restriction : restrictions )
541 {
542 if ( restriction.containsVersion( version ) )
543 {
544 return true;
545 }
546 }
547 return false;
548 }
549
550 public boolean hasRestrictions()
551 {
552 return !restrictions.isEmpty() && recommendedVersion == null;
553 }
554
555 public boolean equals( Object obj )
556 {
557 if ( this == obj )
558 {
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 }