package jenkins.plugins.nodejs.tools;
import java.text.MessageFormat;
import java.util.StringTokenizer;
/**
* Version range. A version range is an interval describing a set of
* {@link NodeJSVersion 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 NodeJSVersionRange} objects are immutable.
*/
public class NodeJSVersionRange {
/**
* 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 static final String MSG_INVALID_FORMAT = "invalid range \"{0}\": invalid format";
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 = ",";
private final boolean leftClosed;
private final NodeJSVersion left;
private final NodeJSVersion right;
private final boolean rightClosed;
private final boolean empty;
private transient String versionRangeString /* default to null */; // NOSONAR
private transient int hash /* default to 0 */; // NOSONAR
/**
* 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.
* @throws IllegalArgumentException
* If {@code range} is improperly formatted.
*/
public NodeJSVersionRange(String range) {
boolean closedLeft;
boolean closedRight;
NodeJSVersion endpointLeft;
NodeJSVersion 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(MessageFormat.format(MSG_INVALID_FORMAT, range));
}
leftClosed = true;
rightClosed = false;
left = NodeJSVersion.parseVersion(token);
right = null;
empty = false;
return;
}
String version = st.nextToken(ENDPOINT_DELIMITER);
endpointLeft = NodeJSVersion.parseVersion(version);
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(MessageFormat.format(MSG_INVALID_FORMAT, range));
}
endpointRight = NodeJSVersion.parseVersion(version);
if (st.hasMoreTokens()) { // any more tokens have to be whitespace
token = st.nextToken("").trim();
if (token.length() != 0) { // trailing whitespace
throw new IllegalArgumentException(MessageFormat.format(MSG_INVALID_FORMAT, range));
}
}
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("invalid range \"" + range + "\": " + e.getMessage(), e);
}
leftClosed = closedLeft;
rightClosed = closedRight;
left = endpointLeft;
right = endpointRight;
empty = isEmpty0();
}
/**
* Returns the left endpoint of this version range.
*
* @return The left endpoint.
*/
public NodeJSVersion 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 NodeJSVersion 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(NodeJSVersion 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 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 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() {
if (versionRangeString != null) {
return versionRangeString;
}
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() {
if (hash != 0) {
return hash;
}
if (empty) {
return hash = 31;
}
int 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 NodeJSVersionRange)) {
return false;
}
NodeJSVersionRange other = (NodeJSVersionRange) 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);
}
}