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