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