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 }