/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2013 ForgeRock AS. All Rights Reserved * * The contents of this file are subject to the terms * of the Common Development and Distribution License * (the License). You may not use this file except in * compliance with the License. * * You can obtain a copy of the License at * http://forgerock.org/license/CDDLv1.0.html * See the License for the specific language governing * permission and limitations under the License. * * When distributing Covered Code, include this CDDL * Header Notice in each file and include the License file * at http://forgerock.org/license/CDDLv1.0.html * If applicable, add the following below the CDDL Header, * with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" */ package org.identityconnectors.common; /** * 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. * * @author Laszlo Hordos * @Immutable * @since 1.4 */ 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 static final String ENDPOINT_DELIMITER = ","; private final Version floorVersion; private final boolean isFloorInclusive; private final Version ceilingVersion; private final boolean isCeilingInclusive; private final boolean empty; /** * 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.parse(version); } catch (IllegalArgumentException e) { throw new IllegalArgumentException( "invalid range \"" + range + "\": " + e.getMessage(), e); } } /** * Creates a version range from the specified string. * * <p> * Version range string grammar: * * <pre> * range ::= interval | at least * interval ::= ( '[' | '(' ) left ',' right ( ']' | ')' ) * left ::= version * right ::= version * at least ::= 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 static VersionRange parse(String range) { Assertions.blankCheck(range, "range"); int idx = range.indexOf(ENDPOINT_DELIMITER); // Check if the version is an interval. if (idx > 1 && idx == range.lastIndexOf(ENDPOINT_DELIMITER)) { String vlo = range.substring(0, idx).trim(); String vhi = range.substring(idx + 1).trim(); boolean isLowInclusive = true; boolean isHighInclusive = true; if (vlo.charAt(0) == LEFT_OPEN) { isLowInclusive = false; } else if (vlo.charAt(0) != LEFT_CLOSED) { throw new IllegalArgumentException("invalid range \"" + range + "\": invalid format"); } vlo = vlo.substring(1).trim(); if (vhi.charAt(vhi.length() - 1) == RIGHT_OPEN) { isHighInclusive = false; } else if (vhi.charAt(vhi.length() - 1) != RIGHT_CLOSED) { throw new IllegalArgumentException("invalid range \"" + range + "\": invalid format"); } vhi = vhi.substring(0, vhi.length() - 1).trim(); return new VersionRange(parseVersion(vlo, range), isLowInclusive, parseVersion(vhi, range), isHighInclusive); } else if (idx == -1) { return new VersionRange(VersionRange.parseVersion(range.trim(), range), true, null, false); } else { throw new IllegalArgumentException("invalid range \"" + range + "\": invalid format"); } } public VersionRange(Version low, boolean isLowInclusive, Version high, boolean isHighInclusive) { Assertions.nullCheck(low, "floorVersion"); floorVersion = low; isFloorInclusive = isLowInclusive; ceilingVersion = high; isCeilingInclusive = isHighInclusive; empty = isEmpty0(); } public Version getFloor() { return floorVersion; } public boolean isFloorInclusive() { return isFloorInclusive; } public Version getCeiling() { return ceilingVersion; } public boolean isCeilingInclusive() { return isCeilingInclusive; } public boolean isInRange(Version version) { if (empty) { return false; } if (floorVersion.compareTo(version) >= (isFloorInclusive ? 1 : 0)) { return false; } if (ceilingVersion == null) { return true; } return ceilingVersion.compareTo(version) >= (isCeilingInclusive ? 0 : 1); } /** * 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) { return false; } else if (ceilingVersion == null) { return true; } if (isFloorInclusive) { if (isCeilingInclusive) { // [f,c]: exact if f == c return floorVersion.equals(ceilingVersion); } else { // [f,c): exact if f++ >= c Version adjacent1 = new Version(floorVersion.getMajor(), floorVersion.getMinor(), floorVersion .getMicro(), floorVersion.getRevision() + 1); return adjacent1.compareTo(ceilingVersion) >= 0; } } else { if (isCeilingInclusive) { // (f,c] is equivalent to [f++,c]: exact if f++ == c Version adjacent1 = new Version(floorVersion.getMajor(), floorVersion.getMinor(), floorVersion .getMicro(), floorVersion.getRevision() + 1); return adjacent1.equals(ceilingVersion); } else { // (f,c) is equivalent to [f++,c): exact if (f++)++ >=c Version adjacent2 = new Version(floorVersion.getMajor(), floorVersion.getMinor(), floorVersion .getMicro(), floorVersion.getRevision() + 2); return adjacent2.compareTo(ceilingVersion) >= 0; } } } /** * 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 (ceilingVersion == null) { // infinity return false; } int comparison = floorVersion.compareTo(ceilingVersion); if (comparison == 0) { // endpoints equal return !isFloorInclusive || !isCeilingInclusive; } return comparison > 0; // true if left > right } public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final VersionRange other = (VersionRange) obj; if (floorVersion != other.floorVersion && (floorVersion == null || !floorVersion.equals(other.floorVersion))) { return false; } if (isFloorInclusive != other.isFloorInclusive) { return false; } if (ceilingVersion != other.ceilingVersion && (ceilingVersion == null || !ceilingVersion.equals(other.ceilingVersion))) { return false; } if (isCeilingInclusive != other.isCeilingInclusive) { return false; } return true; } @Override public int hashCode() { int result = floorVersion.hashCode(); result = 31 * result + (isFloorInclusive ? 1 : 0); result = 31 * result + (ceilingVersion != null ? ceilingVersion.hashCode() : 0); result = 31 * result + (isCeilingInclusive ? 1 : 0); return result; } public String toString() { if (ceilingVersion != null) { StringBuilder sb = new StringBuilder(); sb.append(isFloorInclusive ? LEFT_CLOSED : LEFT_OPEN); sb.append(floorVersion.getVersion()).append(ENDPOINT_DELIMITER).append( ceilingVersion.getVersion()); sb.append(isCeilingInclusive ? RIGHT_CLOSED : RIGHT_OPEN); return sb.toString(); } else { return floorVersion.getVersion(); } } }