blob: abe8d312388bda398da76ab742690907fe45fefc [file] [log] [blame]
/*
* 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);
}
}