001package 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 022import java.util.ArrayList; 023import java.util.Collections; 024import java.util.Iterator; 025import java.util.List; 026 027import 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 */ 034public 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<>(); 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<>(); 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<>( 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}