/* * Copyright © 2015 Cask Data, Inc. * * 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 co.cask.cdap.proto.artifact; import co.cask.cdap.api.artifact.ArtifactVersion; import co.cask.cdap.proto.Id; import java.util.Objects; /** * Represents a range of versions for an artifact. The lower version is inclusive and the upper version is exclusive. */ public class ArtifactRange { private final Id.Namespace namespace; private final String name; private final ArtifactVersion lower; private final ArtifactVersion upper; private final boolean isLowerInclusive; private final boolean isUpperInclusive; public ArtifactRange(Id.Namespace namespace, String name, ArtifactVersion lower, ArtifactVersion upper) { this(namespace, name, lower, true, upper, false); } public ArtifactRange(Id.Namespace namespace, String name, ArtifactVersion lower, boolean isLowerInclusive, ArtifactVersion upper, boolean isUpperInclusive) { this.namespace = namespace; this.name = name; this.lower = lower; this.upper = upper; this.isLowerInclusive = isLowerInclusive; this.isUpperInclusive = isUpperInclusive; } public Id.Namespace getNamespace() { return namespace; } public String getName() { return name; } public ArtifactVersion getLower() { return lower; } public ArtifactVersion getUpper() { return upper; } public boolean versionIsInRange(ArtifactVersion version) { int lowerCompare = version.compareTo(lower); boolean lowerSatisfied = isLowerInclusive ? lowerCompare >= 0 : lowerCompare > 0; int upperCompare = version.compareTo(upper); boolean upperSatisfied = isUpperInclusive ? upperCompare <= 0 : upperCompare < 0; return lowerSatisfied && upperSatisfied; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ArtifactRange that = (ArtifactRange) o; return Objects.equals(namespace, that.namespace) && Objects.equals(name, that.name) && Objects.equals(lower, that.lower) && Objects.equals(upper, that.upper) && isLowerInclusive == that.isLowerInclusive && isUpperInclusive == that.isUpperInclusive; } @Override public int hashCode() { return Objects.hash(namespace, name, lower, isLowerInclusive, upper, isUpperInclusive); } /** * Return the range as a string without the namespace. For example, 'my-functions[1.0.0,2.0.0)'. * * @return the range as a string without the namespace. */ public String toNonNamespacedString() { return toString(new StringBuilder()); } @Override public String toString() { return toString(new StringBuilder().append(namespace.getId()).append(':')); } private String toString(StringBuilder builder) { return builder .append(name) .append(isLowerInclusive ? '[' : '(') .append(lower.getVersion()) .append(',') .append(upper.getVersion()) .append(isUpperInclusive ? ']' : ')') .toString(); } /** * Parses the string representation of an artifact range, which is of the form: * {namespace}:{name}[{lower-version},{upper-version}]. This is what is returned by {@link #toString()}. * For example, default:my-functions[1.0.0,2.0.0) will correspond to an artifact name of my-functions with a * lower version of 1.0.0 and an upper version of 2.0.0 in the default namespace. * * @param artifactRangeStr the string representation to parse * @return the ArtifactRange corresponding to the given string * @throws InvalidArtifactRangeException if the string is malformed, or if the lower version is higher than the upper */ public static ArtifactRange parse(String artifactRangeStr) throws InvalidArtifactRangeException { // get the namespace int nameStartIndex = artifactRangeStr.indexOf(':'); if (nameStartIndex < 0) { throw new InvalidArtifactRangeException(String.format("Invalid artifact range %s. " + "Could not find ':' separating namespace from artifact name.", artifactRangeStr)); } String namespaceStr = artifactRangeStr.substring(0, nameStartIndex); Id.Namespace namespace; try { namespace = Id.Namespace.from(namespaceStr); } catch (Exception e) { throw new InvalidArtifactRangeException(String.format("Invalid namespace %s: %s", namespaceStr, e.getMessage())); } // check not at the end of the string if (nameStartIndex == artifactRangeStr.length()) { throw new InvalidArtifactRangeException( String.format("Invalid artifact range %s. Nothing found after namespace.", artifactRangeStr)); } return parse(namespace, artifactRangeStr.substring(nameStartIndex + 1)); } /** * Parses an unnamespaced string representation of an artifact range. It is expected to be of the form: * {name}[{lower-version},{upper-version}]. Square brackets are inclusive, and parentheses are exclusive. * For example, my-functions[1.0.0,2.0.0) will correspond to an artifact name of my-functions with a * lower version of 1.0.0 and an upper version of 2.0.0. * * @param namespace the namespace of the artifact range * @param artifactRangeStr the string representation to parse * @return the ArtifactRange corresponding to the given string * @throws InvalidArtifactRangeException if the string is malformed, or if the lower version is higher than the upper */ public static ArtifactRange parse(Id.Namespace namespace, String artifactRangeStr) throws InvalidArtifactRangeException { // search for the '[' or '(' between the artifact name and lower version int versionStartIndex = indexOf(artifactRangeStr, '[', '(', 0); if (versionStartIndex < 0) { throw new InvalidArtifactRangeException( String.format("Invalid artifact range %s. " + "Could not find '[' or '(' indicating start of artifact lower version.", artifactRangeStr)); } String name = artifactRangeStr.substring(0, versionStartIndex); if (!Id.Artifact.isValidName(name)) { throw new InvalidArtifactRangeException( String.format("Invalid artifact range %s. Artifact name '%s' is invalid.", artifactRangeStr, name)); } boolean isLowerInclusive = artifactRangeStr.charAt(versionStartIndex) == '['; // search for the comma separating versions int commaIndex = artifactRangeStr.indexOf(',', versionStartIndex + 1); if (commaIndex < 0) { throw new InvalidArtifactRangeException( String.format("Invalid artifact range %s. Could not find ',' separating lower and upper verions.", artifactRangeStr)); } String lowerStr = artifactRangeStr.substring(versionStartIndex + 1, commaIndex).trim(); ArtifactVersion lower = new ArtifactVersion(lowerStr); if (lower.getVersion() == null) { throw new InvalidArtifactRangeException(String.format( "Invalid artifact range %s. Lower version %s is invalid.", artifactRangeStr, lowerStr)); } // search for the ']' or ')' marking the end of the upper version int versionEndIndex = indexOf(artifactRangeStr, ']', ')', commaIndex + 1); if (versionEndIndex < 0) { throw new InvalidArtifactRangeException(String.format( "Invalid artifact range %s. Could not find enclosing ']' or ')'.", artifactRangeStr)); } String upperStr = artifactRangeStr.substring(commaIndex + 1, versionEndIndex).trim(); ArtifactVersion upper = new ArtifactVersion(upperStr); if (upper.getVersion() == null) { throw new InvalidArtifactRangeException(String.format( "Invalid artifact range %s. Upper version %s is invalid.", artifactRangeStr, upperStr)); } boolean isUpperInclusive = artifactRangeStr.charAt(versionEndIndex) == ']'; // check that lower is not greater than upper int comp = lower.compareTo(upper); if (comp > 0) { throw new InvalidArtifactRangeException(String.format( "Invalid artifact range %s. Lower version %s is greater than upper version %s.", artifactRangeStr, lowerStr, upperStr)); } else if (comp == 0 && isLowerInclusive && !isUpperInclusive) { // if lower and upper are equal, but lower is inclusive and upper is exclusive, this is also invalid throw new InvalidArtifactRangeException(String.format( "Invalid artifact range %s. Lower and upper versions %s are equal, " + "but lower is inclusive and upper is exclusive.", artifactRangeStr, lowerStr)); } return new ArtifactRange(namespace, name, lower, isLowerInclusive, upper, isUpperInclusive); } // like String's indexOf(char, int), except it looks for either one of 2 characters private static int indexOf(String str, char option1, char option2, int startIndex) { for (int i = startIndex; i < str.length(); i++) { char charAtIndex = str.charAt(i); if (charAtIndex == option1 || charAtIndex == option2) { return i; } } return -1; } }