001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.util.version;
020
021import java.util.Objects;
022
023import org.eclipse.aether.version.InvalidVersionSpecificationException;
024import org.eclipse.aether.version.Version;
025import org.eclipse.aether.version.VersionRange;
026import org.eclipse.aether.version.VersionScheme;
027
028import static java.util.Objects.requireNonNull;
029
030/**
031 * A version range inspired by mathematical range syntax. For example, "[1.0,2.0)", "[1.0,)" or "[1.0]".
032 * <p>
033 * Despite its name, this class is generic in a sense it works with any {@link Version}
034 */
035public final class GenericVersionRange implements VersionRange {
036    private final VersionScheme versionScheme;
037
038    private final Bound lowerBound;
039
040    private final Bound upperBound;
041
042    /**
043     * Creates a version range from the specified range specification.
044     *
045     * @param range the range specification to parse, must not be {@code null}
046     * @throws InvalidVersionSpecificationException if the range could not be parsed
047     */
048    GenericVersionRange(VersionScheme versionScheme, String range) throws InvalidVersionSpecificationException {
049        this.versionScheme = requireNonNull(versionScheme, "versionScheme cannot be null");
050        String process = requireNonNull(range, "version range cannot be null");
051
052        boolean lowerBoundInclusive, upperBoundInclusive;
053        Version lowerBound, upperBound;
054
055        if (range.startsWith("[")) {
056            lowerBoundInclusive = true;
057        } else if (range.startsWith("(")) {
058            lowerBoundInclusive = false;
059        } else {
060            throw new InvalidVersionSpecificationException(
061                    range, "Invalid version range " + range + ", a range must start with either [ or (");
062        }
063
064        if (range.endsWith("]")) {
065            upperBoundInclusive = true;
066        } else if (range.endsWith(")")) {
067            upperBoundInclusive = false;
068        } else {
069            throw new InvalidVersionSpecificationException(
070                    range, "Invalid version range " + range + ", a range must end with either [ or (");
071        }
072
073        process = process.substring(1, process.length() - 1);
074
075        int index = process.indexOf(",");
076
077        if (index < 0) {
078            if (!lowerBoundInclusive || !upperBoundInclusive) {
079                throw new InvalidVersionSpecificationException(
080                        range, "Invalid version range " + range + ", single version must be surrounded by []");
081            }
082
083            String version = process.trim();
084            if (version.endsWith(".*")) {
085                String prefix = version.substring(0, version.length() - 1);
086                lowerBound = parse(prefix + "min");
087                upperBound = parse(prefix + "max");
088            } else {
089                lowerBound = parse(version);
090                upperBound = lowerBound;
091            }
092        } else {
093            String parsedLowerBound = process.substring(0, index).trim();
094            String parsedUpperBound = process.substring(index + 1).trim();
095
096            // more than two bounds, e.g. (1,2,3)
097            if (parsedUpperBound.contains(",")) {
098                throw new InvalidVersionSpecificationException(
099                        range, "Invalid version range " + range + ", bounds may not contain additional ','");
100            }
101
102            lowerBound = !parsedLowerBound.isEmpty() ? parse(parsedLowerBound) : null;
103            upperBound = !parsedUpperBound.isEmpty() ? parse(parsedUpperBound) : null;
104
105            if (upperBound != null && lowerBound != null) {
106                if (upperBound.compareTo(lowerBound) < 0) {
107                    throw new InvalidVersionSpecificationException(
108                            range,
109                            "Invalid version range " + range + ", lower bound must not be greater than upper bound");
110                }
111            }
112        }
113
114        this.lowerBound = (lowerBound != null) ? new Bound(lowerBound, lowerBoundInclusive) : null;
115        this.upperBound = (upperBound != null) ? new Bound(upperBound, upperBoundInclusive) : null;
116    }
117
118    private Version parse(String version) throws InvalidVersionSpecificationException {
119        return versionScheme.parseVersion(version);
120    }
121
122    @Override
123    public Bound getLowerBound() {
124        return lowerBound;
125    }
126
127    @Override
128    public Bound getUpperBound() {
129        return upperBound;
130    }
131
132    @Override
133    public boolean containsVersion(Version version) {
134        if (lowerBound != null) {
135            int comparison = lowerBound.getVersion().compareTo(version);
136
137            if (comparison == 0 && !lowerBound.isInclusive()) {
138                return false;
139            }
140            if (comparison > 0) {
141                return false;
142            }
143        }
144
145        if (upperBound != null) {
146            int comparison = upperBound.getVersion().compareTo(version);
147
148            if (comparison == 0 && !upperBound.isInclusive()) {
149                return false;
150            }
151            if (comparison < 0) {
152                return false;
153            }
154        }
155
156        return true;
157    }
158
159    @Override
160    public boolean equals(Object obj) {
161        if (obj == this) {
162            return true;
163        } else if (obj == null || !getClass().equals(obj.getClass())) {
164            return false;
165        }
166
167        VersionRange that = (VersionRange) obj;
168
169        return Objects.equals(upperBound, that.getUpperBound()) && Objects.equals(lowerBound, that.getLowerBound());
170    }
171
172    @Override
173    public int hashCode() {
174        int hash = 17;
175        hash = hash * 31 + hash(upperBound);
176        hash = hash * 31 + hash(lowerBound);
177        return hash;
178    }
179
180    private static int hash(Object obj) {
181        return obj != null ? obj.hashCode() : 0;
182    }
183
184    @Override
185    public String toString() {
186        StringBuilder buffer = new StringBuilder(64);
187        if (lowerBound != null) {
188            buffer.append(lowerBound.isInclusive() ? '[' : '(');
189            buffer.append(lowerBound.getVersion());
190        } else {
191            buffer.append('(');
192        }
193        buffer.append(',');
194        if (upperBound != null) {
195            buffer.append(upperBound.getVersion());
196            buffer.append(upperBound.isInclusive() ? ']' : ')');
197        } else {
198            buffer.append(')');
199        }
200        return buffer.toString();
201    }
202}