package org.apache.maven.artifact.versioning;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

import org.apache.maven.artifact.Artifact;

Construct a version range from a specification.
Author:Brett Porter
/** * Construct a version range from a specification. * * @author <a href="mailto:brett@apache.org">Brett Porter</a> */
public class VersionRange { private static final Map<String, VersionRange> CACHE_SPEC = Collections.<String, VersionRange>synchronizedMap( new WeakHashMap<String, VersionRange>() ); private static final Map<String, VersionRange> CACHE_VERSION = Collections.<String, VersionRange>synchronizedMap( new WeakHashMap<String, VersionRange>() ); private final ArtifactVersion recommendedVersion; private final List<Restriction> restrictions; private VersionRange( ArtifactVersion recommendedVersion, List<Restriction> restrictions ) { this.recommendedVersion = recommendedVersion; this.restrictions = restrictions; } public ArtifactVersion getRecommendedVersion() { return recommendedVersion; } public List<Restriction> getRestrictions() { return restrictions; }
Deprecated:VersionRange is immutable, cloning is not useful and even more an issue against the cache
Returns:a clone
/** * @deprecated VersionRange is immutable, cloning is not useful and even more an issue against the cache * @return a clone */
@Deprecated public VersionRange cloneOf() { List<Restriction> copiedRestrictions = null; if ( restrictions != null ) { copiedRestrictions = new ArrayList<>(); if ( !restrictions.isEmpty() ) { copiedRestrictions.addAll( restrictions ); } } return new VersionRange( recommendedVersion, copiedRestrictions ); }

Create a version range from a string representation

Some spec examples are:
  • 1.0 Version 1.0
  • [1.0,2.0) Versions 1.0 (included) to 2.0 (not included)
  • [1.0,2.0] Versions 1.0 to 2.0 (both included)
  • [1.5,) Versions 1.5 and higher
  • (,1.0],[1.2,) Versions up to 1.0 (included) and 1.2 or higher
Params:
  • spec – string representation of a version or version range
Throws:
Returns:a new VersionRange object that represents the spec
/** * <p> * Create a version range from a string representation * </p> * Some spec examples are: * <ul> * <li><code>1.0</code> Version 1.0</li> * <li><code>[1.0,2.0)</code> Versions 1.0 (included) to 2.0 (not included)</li> * <li><code>[1.0,2.0]</code> Versions 1.0 to 2.0 (both included)</li> * <li><code>[1.5,)</code> Versions 1.5 and higher</li> * <li><code>(,1.0],[1.2,)</code> Versions up to 1.0 (included) and 1.2 or higher</li> * </ul> * * @param spec string representation of a version or version range * @return a new {@link VersionRange} object that represents the spec * @throws InvalidVersionSpecificationException * */
public static VersionRange createFromVersionSpec( String spec ) throws InvalidVersionSpecificationException { if ( spec == null ) { return null; } VersionRange cached = CACHE_SPEC.get( spec ); if ( cached != null ) { return cached; } List<Restriction> restrictions = new ArrayList<>(); String process = spec; ArtifactVersion version = null; ArtifactVersion upperBound = null; ArtifactVersion lowerBound = null; while ( process.startsWith( "[" ) || process.startsWith( "(" ) ) { int index1 = process.indexOf( ')' ); int index2 = process.indexOf( ']' ); int index = index2; if ( index2 < 0 || index1 < index2 ) { if ( index1 >= 0 ) { index = index1; } } if ( index < 0 ) { throw new InvalidVersionSpecificationException( "Unbounded range: " + spec ); } Restriction restriction = parseRestriction( process.substring( 0, index + 1 ) ); if ( lowerBound == null ) { lowerBound = restriction.getLowerBound(); } if ( upperBound != null ) { if ( restriction.getLowerBound() == null || restriction.getLowerBound().compareTo( upperBound ) < 0 ) { throw new InvalidVersionSpecificationException( "Ranges overlap: " + spec ); } } restrictions.add( restriction ); upperBound = restriction.getUpperBound(); process = process.substring( index + 1 ).trim(); if ( process.length() > 0 && process.startsWith( "," ) ) { process = process.substring( 1 ).trim(); } } if ( process.length() > 0 ) { if ( restrictions.size() > 0 ) { throw new InvalidVersionSpecificationException( "Only fully-qualified sets allowed in multiple set scenario: " + spec ); } else { version = new DefaultArtifactVersion( process ); restrictions.add( Restriction.EVERYTHING ); } } cached = new VersionRange( version, restrictions ); CACHE_SPEC.put( spec, cached ); return cached; } private static Restriction parseRestriction( String spec ) throws InvalidVersionSpecificationException { boolean lowerBoundInclusive = spec.startsWith( "[" ); boolean upperBoundInclusive = spec.endsWith( "]" ); String process = spec.substring( 1, spec.length() - 1 ).trim(); Restriction restriction; int index = process.indexOf( ',' ); if ( index < 0 ) { if ( !lowerBoundInclusive || !upperBoundInclusive ) { throw new InvalidVersionSpecificationException( "Single version must be surrounded by []: " + spec ); } ArtifactVersion version = new DefaultArtifactVersion( process ); restriction = new Restriction( version, lowerBoundInclusive, version, upperBoundInclusive ); } else { String lowerBound = process.substring( 0, index ).trim(); String upperBound = process.substring( index + 1 ).trim(); if ( lowerBound.equals( upperBound ) ) { throw new InvalidVersionSpecificationException( "Range cannot have identical boundaries: " + spec ); } ArtifactVersion lowerVersion = null; if ( lowerBound.length() > 0 ) { lowerVersion = new DefaultArtifactVersion( lowerBound ); } ArtifactVersion upperVersion = null; if ( upperBound.length() > 0 ) { upperVersion = new DefaultArtifactVersion( upperBound ); } if ( upperVersion != null && lowerVersion != null && upperVersion.compareTo( lowerVersion ) < 0 ) { throw new InvalidVersionSpecificationException( "Range defies version ordering: " + spec ); } restriction = new Restriction( lowerVersion, lowerBoundInclusive, upperVersion, upperBoundInclusive ); } return restriction; } public static VersionRange createFromVersion( String version ) { VersionRange cached = CACHE_VERSION.get( version ); if ( cached == null ) { List<Restriction> restrictions = Collections.emptyList(); cached = new VersionRange( new DefaultArtifactVersion( version ), restrictions ); CACHE_VERSION.put( version, cached ); } return cached; }
Creates and returns a new VersionRange that is a restriction of this version range and the specified version range.

Note: Precedence is given to the recommended version from this version range over the recommended version from the specified version range.

Params:
  • restriction – the VersionRange that will be used to restrict this version range.
Throws:
Returns:the VersionRange that is a restriction of this version range and the specified version range.

The restrictions of the returned version range will be an intersection of the restrictions of this version range and the specified version range if both version ranges have restrictions. Otherwise, the restrictions on the returned range will be empty.

The recommended version of the returned version range will be the recommended version of this version range, provided that ranges falls within the intersected restrictions. If the restrictions are empty, this version range's recommended version is used if it is not null. If it is null, the specified version range's recommended version is used (provided it is non-null). If no recommended version can be obtained, the returned version range's recommended version is set to null.

/** * Creates and returns a new <code>VersionRange</code> that is a restriction of this * version range and the specified version range. * <p> * Note: Precedence is given to the recommended version from this version range over the * recommended version from the specified version range. * </p> * * @param restriction the <code>VersionRange</code> that will be used to restrict this version * range. * @return the <code>VersionRange</code> that is a restriction of this version range and the * specified version range. * <p> * The restrictions of the returned version range will be an intersection of the restrictions * of this version range and the specified version range if both version ranges have * restrictions. Otherwise, the restrictions on the returned range will be empty. * </p> * <p> * The recommended version of the returned version range will be the recommended version of * this version range, provided that ranges falls within the intersected restrictions. If * the restrictions are empty, this version range's recommended version is used if it is not * <code>null</code>. If it is <code>null</code>, the specified version range's recommended * version is used (provided it is non-<code>null</code>). If no recommended version can be * obtained, the returned version range's recommended version is set to <code>null</code>. * </p> * @throws NullPointerException if the specified <code>VersionRange</code> is * <code>null</code>. */
public VersionRange restrict( VersionRange restriction ) { List<Restriction> r1 = this.restrictions; List<Restriction> r2 = restriction.restrictions; List<Restriction> restrictions; if ( r1.isEmpty() || r2.isEmpty() ) { restrictions = Collections.emptyList(); } else { restrictions = Collections.unmodifiableList( intersection( r1, r2 ) ); } ArtifactVersion version = null; if ( restrictions.size() > 0 ) { for ( Restriction r : restrictions ) { if ( recommendedVersion != null && r.containsVersion( recommendedVersion ) ) { // if we find the original, use that version = recommendedVersion; break; } else if ( version == null && restriction.getRecommendedVersion() != null && r.containsVersion( restriction.getRecommendedVersion() ) ) { // use this if we can, but prefer the original if possible version = restriction.getRecommendedVersion(); } } } // Either the original or the specified version ranges have no restrictions else if ( recommendedVersion != null ) { // Use the original recommended version since it exists version = recommendedVersion; } else if ( restriction.recommendedVersion != null ) { // Use the recommended version from the specified VersionRange since there is no // original recommended version version = restriction.recommendedVersion; } /* TODO should throw this immediately, but need artifact else { throw new OverConstrainedVersionException( "Restricting incompatible version ranges" ); } */ return new VersionRange( version, restrictions ); } private List<Restriction> intersection( List<Restriction> r1, List<Restriction> r2 ) { List<Restriction> restrictions = new ArrayList<>( r1.size() + r2.size() ); Iterator<Restriction> i1 = r1.iterator(); Iterator<Restriction> i2 = r2.iterator(); Restriction res1 = i1.next(); Restriction res2 = i2.next(); boolean done = false; while ( !done ) { if ( res1.getLowerBound() == null || res2.getUpperBound() == null || res1.getLowerBound().compareTo( res2.getUpperBound() ) <= 0 ) { if ( res1.getUpperBound() == null || res2.getLowerBound() == null || res1.getUpperBound().compareTo( res2.getLowerBound() ) >= 0 ) { ArtifactVersion lower; ArtifactVersion upper; boolean lowerInclusive; boolean upperInclusive; // overlaps if ( res1.getLowerBound() == null ) { lower = res2.getLowerBound(); lowerInclusive = res2.isLowerBoundInclusive(); } else if ( res2.getLowerBound() == null ) { lower = res1.getLowerBound(); lowerInclusive = res1.isLowerBoundInclusive(); } else { int comparison = res1.getLowerBound().compareTo( res2.getLowerBound() ); if ( comparison < 0 ) { lower = res2.getLowerBound(); lowerInclusive = res2.isLowerBoundInclusive(); } else if ( comparison == 0 ) { lower = res1.getLowerBound(); lowerInclusive = res1.isLowerBoundInclusive() && res2.isLowerBoundInclusive(); } else { lower = res1.getLowerBound(); lowerInclusive = res1.isLowerBoundInclusive(); } } if ( res1.getUpperBound() == null ) { upper = res2.getUpperBound(); upperInclusive = res2.isUpperBoundInclusive(); } else if ( res2.getUpperBound() == null ) { upper = res1.getUpperBound(); upperInclusive = res1.isUpperBoundInclusive(); } else { int comparison = res1.getUpperBound().compareTo( res2.getUpperBound() ); if ( comparison < 0 ) { upper = res1.getUpperBound(); upperInclusive = res1.isUpperBoundInclusive(); } else if ( comparison == 0 ) { upper = res1.getUpperBound(); upperInclusive = res1.isUpperBoundInclusive() && res2.isUpperBoundInclusive(); } else { upper = res2.getUpperBound(); upperInclusive = res2.isUpperBoundInclusive(); } } // don't add if they are equal and one is not inclusive if ( lower == null || upper == null || lower.compareTo( upper ) != 0 ) { restrictions.add( new Restriction( lower, lowerInclusive, upper, upperInclusive ) ); } else if ( lowerInclusive && upperInclusive ) { restrictions.add( new Restriction( lower, lowerInclusive, upper, upperInclusive ) ); } //noinspection ObjectEquality if ( upper == res2.getUpperBound() ) { // advance res2 if ( i2.hasNext() ) { res2 = i2.next(); } else { done = true; } } else { // advance res1 if ( i1.hasNext() ) { res1 = i1.next(); } else { done = true; } } } else { // move on to next in r1 if ( i1.hasNext() ) { res1 = i1.next(); } else { done = true; } } } else { // move on to next in r2 if ( i2.hasNext() ) { res2 = i2.next(); } else { done = true; } } } return restrictions; } public ArtifactVersion getSelectedVersion( Artifact artifact ) throws OverConstrainedVersionException { ArtifactVersion version; if ( recommendedVersion != null ) { version = recommendedVersion; } else { if ( restrictions.size() == 0 ) { throw new OverConstrainedVersionException( "The artifact has no valid ranges", artifact ); } version = null; } return version; } public boolean isSelectedVersionKnown( Artifact artifact ) throws OverConstrainedVersionException { boolean value = false; if ( recommendedVersion != null ) { value = true; } else { if ( restrictions.size() == 0 ) { throw new OverConstrainedVersionException( "The artifact has no valid ranges", artifact ); } } return value; } public String toString() { if ( recommendedVersion != null ) { return recommendedVersion.toString(); } else { StringBuilder buf = new StringBuilder(); for ( Iterator<Restriction> i = restrictions.iterator(); i.hasNext(); ) { Restriction r = i.next(); buf.append( r.toString() ); if ( i.hasNext() ) { buf.append( ',' ); } } return buf.toString(); } } public ArtifactVersion matchVersion( List<ArtifactVersion> versions ) { // TODO could be more efficient by sorting the list and then moving along the restrictions in order? ArtifactVersion matched = null; for ( ArtifactVersion version : versions ) { if ( containsVersion( version ) ) { // valid - check if it is greater than the currently matched version if ( matched == null || version.compareTo( matched ) > 0 ) { matched = version; } } } return matched; } public boolean containsVersion( ArtifactVersion version ) { for ( Restriction restriction : restrictions ) { if ( restriction.containsVersion( version ) ) { return true; } } return false; } public boolean hasRestrictions() { return !restrictions.isEmpty() && recommendedVersion == null; } public boolean equals( Object obj ) { if ( this == obj ) { return true; } if ( !( obj instanceof VersionRange ) ) { return false; } VersionRange other = (VersionRange) obj; boolean equals = recommendedVersion == other.recommendedVersion || ( ( recommendedVersion != null ) && recommendedVersion.equals( other.recommendedVersion ) ); equals &= restrictions == other.restrictions || ( ( restrictions != null ) && restrictions.equals( other.restrictions ) ); return equals; } public int hashCode() { int hash = 7; hash = 31 * hash + ( recommendedVersion == null ? 0 : recommendedVersion.hashCode() ); hash = 31 * hash + ( restrictions == null ? 0 : restrictions.hashCode() ); return hash; } }