| /* |
| * Copyright (c) OSGi Alliance (2011, 2016). All Rights Reserved. |
| * |
| * Licensed 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. |
| */ |
| |
| package org.osgi.framework; |
| |
| import java.util.NoSuchElementException; |
| import java.util.StringTokenizer; |
| |
| /** |
| * Version range. A version range is an interval describing a set of |
| * {@link Version versions}. |
| * |
| * <p> |
| * A range has a left (lower) endpoint and a right (upper) endpoint. Each |
| * endpoint can be open (excluded from the set) or closed (included in the set). |
| * |
| * <p> |
| * {@code VersionRange} objects are immutable. |
| * |
| * @since 1.7 |
| * @Immutable |
| * @author $Id: cc407ff6fb1d5252b61a033924e63b751880f580 $ |
| */ |
| |
| public class VersionRange { |
| /** |
| * The left endpoint is open and is excluded from the range. |
| * <p> |
| * The value of {@code LEFT_OPEN} is {@code '('}. |
| */ |
| public static final char LEFT_OPEN = '('; |
| /** |
| * The left endpoint is closed and is included in the range. |
| * <p> |
| * The value of {@code LEFT_CLOSED} is {@code '['}. |
| */ |
| public static final char LEFT_CLOSED = '['; |
| /** |
| * The right endpoint is open and is excluded from the range. |
| * <p> |
| * The value of {@code RIGHT_OPEN} is {@code ')'}. |
| */ |
| public static final char RIGHT_OPEN = ')'; |
| /** |
| * The right endpoint is closed and is included in the range. |
| * <p> |
| * The value of {@code RIGHT_CLOSED} is {@code ']'}. |
| */ |
| public static final char RIGHT_CLOSED = ']'; |
| |
| private final boolean leftClosed; |
| private final Version left; |
| private final Version right; |
| private final boolean rightClosed; |
| private final boolean empty; |
| |
| private transient String versionRangeString /* default to null */; |
| private transient int hash /* default to 0 */; |
| |
| private static final String LEFT_OPEN_DELIMITER = "("; |
| private static final String LEFT_CLOSED_DELIMITER = "["; |
| private static final String LEFT_DELIMITERS = LEFT_CLOSED_DELIMITER + LEFT_OPEN_DELIMITER; |
| private static final String RIGHT_OPEN_DELIMITER = ")"; |
| private static final String RIGHT_CLOSED_DELIMITER = "]"; |
| private static final String RIGHT_DELIMITERS = RIGHT_OPEN_DELIMITER + RIGHT_CLOSED_DELIMITER; |
| private static final String ENDPOINT_DELIMITER = ","; |
| |
| /** |
| * Creates a version range from the specified versions. |
| * |
| * @param leftType Must be either {@link #LEFT_CLOSED} or {@link #LEFT_OPEN} |
| * . |
| * @param leftEndpoint Left endpoint of range. Must not be {@code null}. |
| * @param rightEndpoint Right endpoint of range. May be {@code null} to |
| * indicate the right endpoint is <i>Infinity</i>. |
| * @param rightType Must be either {@link #RIGHT_CLOSED} or |
| * {@link #RIGHT_OPEN}. |
| * @throws IllegalArgumentException If the arguments are invalid. |
| */ |
| public VersionRange(char leftType, Version leftEndpoint, Version rightEndpoint, char rightType) { |
| if ((leftType != LEFT_CLOSED) && (leftType != LEFT_OPEN)) { |
| throw new IllegalArgumentException("invalid leftType \"" + leftType + "\""); |
| } |
| if ((rightType != RIGHT_OPEN) && (rightType != RIGHT_CLOSED)) { |
| throw new IllegalArgumentException("invalid rightType \"" + rightType + "\""); |
| } |
| if (leftEndpoint == null) { |
| throw new IllegalArgumentException("null leftEndpoint argument"); |
| } |
| leftClosed = leftType == LEFT_CLOSED; |
| rightClosed = rightType == RIGHT_CLOSED; |
| left = leftEndpoint; |
| right = rightEndpoint; |
| empty = isEmpty0(); |
| } |
| |
| /** |
| * Creates a version range from the specified string. |
| * |
| * <p> |
| * Version range string grammar: |
| * |
| * <pre> |
| * range ::= interval | atleast |
| * interval ::= ( '[' | '(' ) left ',' right ( ']' | ')' ) |
| * left ::= version |
| * right ::= version |
| * atleast ::= version |
| * </pre> |
| * |
| * @param range String representation of the version range. The versions in |
| * the range must contain no whitespace. Other whitespace in the |
| * range string is ignored. Must not be {@code null}. |
| * @throws IllegalArgumentException If {@code range} is improperly |
| * formatted. |
| */ |
| public VersionRange(String range) { |
| boolean closedLeft; |
| boolean closedRight; |
| Version endpointLeft; |
| Version endpointRight; |
| |
| try { |
| StringTokenizer st = new StringTokenizer(range, LEFT_DELIMITERS, true); |
| String token = st.nextToken().trim(); // whitespace or left delim |
| if (token.length() == 0) { // leading whitespace |
| token = st.nextToken(); // left delim |
| } |
| closedLeft = LEFT_CLOSED_DELIMITER.equals(token); |
| if (!closedLeft && !LEFT_OPEN_DELIMITER.equals(token)) { |
| // first token is not a delimiter, so it must be "atleast" |
| if (st.hasMoreTokens()) { // there must be no more tokens |
| throw new IllegalArgumentException("invalid range \"" + range + "\": invalid format"); |
| } |
| leftClosed = true; |
| rightClosed = false; |
| left = parseVersion(token, range); |
| right = null; |
| empty = false; |
| return; |
| } |
| String version = st.nextToken(ENDPOINT_DELIMITER); |
| endpointLeft = parseVersion(version, range); |
| token = st.nextToken(); // consume comma |
| version = st.nextToken(RIGHT_DELIMITERS); |
| token = st.nextToken(); // right delim |
| closedRight = RIGHT_CLOSED_DELIMITER.equals(token); |
| if (!closedRight && !RIGHT_OPEN_DELIMITER.equals(token)) { |
| throw new IllegalArgumentException("invalid range \"" + range + "\": invalid format"); |
| } |
| endpointRight = parseVersion(version, range); |
| |
| if (st.hasMoreTokens()) { // any more tokens have to be whitespace |
| token = st.nextToken("").trim(); |
| if (token.length() != 0) { // trailing whitespace |
| throw new IllegalArgumentException("invalid range \"" + range + "\": invalid format"); |
| } |
| } |
| } catch (NoSuchElementException e) { |
| throw new IllegalArgumentException( |
| "invalid range \"" + range + "\": invalid format", e); |
| } |
| |
| leftClosed = closedLeft; |
| rightClosed = closedRight; |
| left = endpointLeft; |
| right = endpointRight; |
| empty = isEmpty0(); |
| } |
| |
| /** |
| * Parse version component into a Version. |
| * |
| * @param version version component string |
| * @param range Complete range string for exception message, if any |
| * @return Version |
| */ |
| private static Version parseVersion(String version, String range) { |
| try { |
| return Version.valueOf(version); |
| } catch (IllegalArgumentException e) { |
| throw new IllegalArgumentException( |
| "invalid range \"" + range + "\": " + e.getMessage(), e); |
| } |
| } |
| |
| /** |
| * Returns the left endpoint of this version range. |
| * |
| * @return The left endpoint. |
| */ |
| public Version getLeft() { |
| return left; |
| } |
| |
| /** |
| * Returns the right endpoint of this version range. |
| * |
| * @return The right endpoint. May be {@code null} which indicates the right |
| * endpoint is <i>Infinity</i>. |
| */ |
| public Version getRight() { |
| return right; |
| } |
| |
| /** |
| * Returns the type of the left endpoint of this version range. |
| * |
| * @return {@link #LEFT_CLOSED} if the left endpoint is closed or |
| * {@link #LEFT_OPEN} if the left endpoint is open. |
| */ |
| public char getLeftType() { |
| return leftClosed ? LEFT_CLOSED : LEFT_OPEN; |
| } |
| |
| /** |
| * Returns the type of the right endpoint of this version range. |
| * |
| * @return {@link #RIGHT_CLOSED} if the right endpoint is closed or |
| * {@link #RIGHT_OPEN} if the right endpoint is open. |
| */ |
| public char getRightType() { |
| return rightClosed ? RIGHT_CLOSED : RIGHT_OPEN; |
| } |
| |
| /** |
| * Returns whether this version range includes the specified version. |
| * |
| * @param version The version to test for inclusion in this version range. |
| * @return {@code true} if the specified version is included in this version |
| * range; {@code false} otherwise. |
| */ |
| public boolean includes(Version version) { |
| if (empty) { |
| return false; |
| } |
| if (left.compareTo(version) >= (leftClosed ? 1 : 0)) { |
| return false; |
| } |
| if (right == null) { |
| return true; |
| } |
| return right.compareTo(version) >= (rightClosed ? 0 : 1); |
| } |
| |
| /** |
| * Returns the intersection of this version range with the specified version |
| * ranges. |
| * |
| * @param ranges The version ranges to intersect with this version range. |
| * @return A version range representing the intersection of this version |
| * range and the specified version ranges. If no version ranges are |
| * specified, then this version range is returned. |
| */ |
| public VersionRange intersection(VersionRange... ranges) { |
| if ((ranges == null) || (ranges.length == 0)) { |
| return this; |
| } |
| // prime with data from this version range |
| boolean closedLeft = leftClosed; |
| boolean closedRight = rightClosed; |
| Version endpointLeft = left; |
| Version endpointRight = right; |
| |
| for (VersionRange range : ranges) { |
| int comparison = endpointLeft.compareTo(range.left); |
| if (comparison == 0) { |
| closedLeft = closedLeft && range.leftClosed; |
| } else { |
| if (comparison < 0) { // move endpointLeft to the right |
| endpointLeft = range.left; |
| closedLeft = range.leftClosed; |
| } |
| } |
| if (range.right != null) { |
| if (endpointRight == null) { |
| endpointRight = range.right; |
| closedRight = range.rightClosed; |
| } else { |
| comparison = endpointRight.compareTo(range.right); |
| if (comparison == 0) { |
| closedRight = closedRight && range.rightClosed; |
| } else { |
| if (comparison > 0) { // move endpointRight to the left |
| endpointRight = range.right; |
| closedRight = range.rightClosed; |
| } |
| } |
| } |
| } |
| } |
| |
| return new VersionRange(closedLeft ? LEFT_CLOSED : LEFT_OPEN, endpointLeft, endpointRight, closedRight ? RIGHT_CLOSED : RIGHT_OPEN); |
| } |
| |
| /** |
| * Returns whether this version range is empty. A version range is empty if |
| * the set of versions defined by the interval is empty. |
| * |
| * @return {@code true} if this version range is empty; {@code false} |
| * otherwise. |
| */ |
| public boolean isEmpty() { |
| return empty; |
| } |
| |
| /** |
| * Internal isEmpty behavior. |
| * |
| * @return {@code true} if this version range is empty; {@code false} |
| * otherwise. |
| */ |
| private boolean isEmpty0() { |
| if (right == null) { // infinity |
| return false; |
| } |
| int comparison = left.compareTo(right); |
| if (comparison == 0) { // endpoints equal |
| return !leftClosed || !rightClosed; |
| } |
| return comparison > 0; // true if left > right |
| } |
| |
| /** |
| * Returns whether this version range contains only a single version. |
| * |
| * @return {@code true} if this version range contains only a single |
| * version; {@code false} otherwise. |
| */ |
| public boolean isExact() { |
| if (empty || (right == null)) { |
| return false; |
| } |
| if (leftClosed) { |
| if (rightClosed) { |
| // [l,r]: exact if l == r |
| return left.equals(right); |
| } else { |
| // [l,r): exact if l++ >= r |
| Version adjacent1 = new Version(left.getMajor(), left.getMinor(), left.getMicro(), left.getQualifier() + "-"); |
| return adjacent1.compareTo(right) >= 0; |
| } |
| } else { |
| if (rightClosed) { |
| // (l,r] is equivalent to [l++,r]: exact if l++ == r |
| Version adjacent1 = new Version(left.getMajor(), left.getMinor(), left.getMicro(), left.getQualifier() + "-"); |
| return adjacent1.equals(right); |
| } else { |
| // (l,r) is equivalent to [l++,r): exact if (l++)++ >=r |
| Version adjacent2 = new Version(left.getMajor(), left.getMinor(), left.getMicro(), left.getQualifier() + "--"); |
| return adjacent2.compareTo(right) >= 0; |
| } |
| } |
| } |
| |
| /** |
| * Returns the string representation of this version range. |
| * |
| * <p> |
| * The format of the version range string will be a version string if the |
| * right end point is <i>Infinity</i> ({@code null}) or an interval string. |
| * |
| * @return The string representation of this version range. |
| */ |
| @Override |
| public String toString() { |
| String s = versionRangeString; |
| if (s != null) { |
| return s; |
| } |
| String leftVersion = left.toString(); |
| if (right == null) { |
| StringBuilder result = new StringBuilder(leftVersion.length() + 1); |
| result.append(left.toString0()); |
| return versionRangeString = result.toString(); |
| } |
| String rightVerion = right.toString(); |
| StringBuilder result = new StringBuilder( |
| leftVersion.length() + rightVerion.length() + 5); |
| result.append(leftClosed ? LEFT_CLOSED : LEFT_OPEN); |
| result.append(left.toString0()); |
| result.append(ENDPOINT_DELIMITER); |
| result.append(right.toString0()); |
| result.append(rightClosed ? RIGHT_CLOSED : RIGHT_OPEN); |
| return versionRangeString = result.toString(); |
| } |
| |
| /** |
| * Returns a hash code value for the object. |
| * |
| * @return An integer which is a hash code value for this object. |
| */ |
| @Override |
| public int hashCode() { |
| int h = hash; |
| if (h != 0) { |
| return h; |
| } |
| if (empty) { |
| return hash = 31; |
| } |
| h = 31 + (leftClosed ? 7 : 5); |
| h = 31 * h + left.hashCode(); |
| if (right != null) { |
| h = 31 * h + right.hashCode(); |
| h = 31 * h + (rightClosed ? 7 : 5); |
| } |
| return hash = h; |
| } |
| |
| /** |
| * Compares this {@code VersionRange} object to another object. |
| * |
| * <p> |
| * A version range is considered to be <b>equal to </b> another version |
| * range if both the endpoints and their types are equal or if both version |
| * ranges are {@link #isEmpty() empty}. |
| * |
| * @param object The {@code VersionRange} object to be compared. |
| * @return {@code true} if {@code object} is a {@code VersionRange} and is |
| * equal to this object; {@code false} otherwise. |
| */ |
| @Override |
| public boolean equals(Object object) { |
| if (object == this) { // quicktest |
| return true; |
| } |
| if (!(object instanceof VersionRange)) { |
| return false; |
| } |
| VersionRange other = (VersionRange) object; |
| if (empty && other.empty) { |
| return true; |
| } |
| if (right == null) { |
| return (leftClosed == other.leftClosed) && (other.right == null) && left.equals(other.left); |
| } |
| return (leftClosed == other.leftClosed) && (rightClosed == other.rightClosed) && left.equals(other.left) && right.equals(other.right); |
| } |
| |
| /** |
| * Returns the filter string for this version range using the specified |
| * attribute name. |
| * |
| * @param attributeName The attribute name to use in the returned filter |
| * string. |
| * @return A filter string for this version range using the specified |
| * attribute name. |
| * @throws IllegalArgumentException If the specified attribute name is not a |
| * valid attribute name. |
| * |
| * @see "Core Specification, Filters, for a description of the filter string syntax." |
| */ |
| public String toFilterString(String attributeName) { |
| if (attributeName.length() == 0) { |
| throw new IllegalArgumentException("invalid attributeName \"" + attributeName + "\""); |
| } |
| for (char ch : attributeName.toCharArray()) { |
| if ((ch == '=') || (ch == '>') || (ch == '<') || (ch == '~') || (ch == '(') || (ch == ')')) { |
| throw new IllegalArgumentException("invalid attributeName \"" + attributeName + "\""); |
| } |
| } |
| |
| StringBuilder result = new StringBuilder(128); |
| final boolean needPresence = !leftClosed && ((right == null) || !rightClosed); |
| final boolean multipleTerms = needPresence || (right != null); |
| if (multipleTerms) { |
| result.append("(&"); |
| } |
| if (needPresence) { |
| result.append('('); |
| result.append(attributeName); |
| result.append("=*)"); |
| } |
| if (leftClosed) { |
| result.append('('); |
| result.append(attributeName); |
| result.append(">="); |
| result.append(left.toString0()); |
| result.append(')'); |
| } else { |
| result.append("(!("); |
| result.append(attributeName); |
| result.append("<="); |
| result.append(left.toString0()); |
| result.append("))"); |
| } |
| if (right != null) { |
| if (rightClosed) { |
| result.append('('); |
| result.append(attributeName); |
| result.append("<="); |
| result.append(right.toString0()); |
| result.append(')'); |
| } else { |
| result.append("(!("); |
| result.append(attributeName); |
| result.append(">="); |
| result.append(right.toString0()); |
| result.append("))"); |
| } |
| } |
| if (multipleTerms) { |
| result.append(')'); |
| } |
| |
| return result.toString(); |
| } |
| |
| /** |
| * Returns a {@code VersionRange} object holding the version range in the |
| * specified {@code String}. |
| * |
| * <p> |
| * See {@link #VersionRange(String)} for the format of the version range |
| * string. |
| * |
| * @param range String representation of the version range. The versions in |
| * the range must contain no whitespace. Other whitespace in the |
| * range string is ignored. Must not be {@code null}. |
| * @return A {@code VersionRange} object representing the version range. |
| * @throws IllegalArgumentException If {@code range} is improperly |
| * formatted. |
| * @since 1.8 |
| */ |
| public static VersionRange valueOf(String range) { |
| return new VersionRange(range); |
| } |
| } |