001    package 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    
022    import java.util.ArrayList;
023    import java.util.Collections;
024    import java.util.Iterator;
025    import java.util.List;
026    
027    import 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     */
034    public 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<Restriction>();
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<Restriction>();
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<Restriction>( 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    }