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}