/* Copyright (c) 2008 Google 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 com.google.gdata.util; import com.google.gdata.util.common.base.Preconditions; import com.google.gdata.client.Service; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * The Version class is a helper class that describes version information * about a particular type of service. */ public class Version { /** * The ANY value indicates a version component that will match any revision. */ public static final int ANY = -1; /** * Finds a matching version for {@code serviceClass} in a list of versions, * or returns {@code null} otherwise. * @param versionList the list of versions to search. * @param serviceClass the service class to match. * @return the matching version or {@code null}. */ public static Version findServiceVersion( Collection<? extends Version> versionList, Class<? extends Service> serviceClass) { for (Version v : versionList) { if (v.getServiceClass().equals(serviceClass)) { return v; } } return null; } /** * Returns a version that matches the input service type and major version but * {@link #ANY} minor version. * * @param v input version type * @return equivalent version with any minor version */ public static Version anyMinorVersionOf(Version v) { return new Version(v.getServiceClass(), v.getMajor(), Version.ANY); } private Class<? extends Service> serviceClass; private int major; private int minor; private List<Version> impliedVersions = new ArrayList<Version>(); /** * Regex that description the format of a version description. The * first (optional) group is the service name, followed by a group * containing the major version, and then another optional group that * contains the minor version number preceded by a dot ('.'). */ private static final Pattern VERSION_PROPERTY_PATTERN = Pattern.compile("([^\\d]+-)?(\\d+)(\\.\\d+)?"); /** * Creates a new Version instance for the specified service and defines * the major and minor versions for the service. * @param serviceClass the service type. * @param major the major revision number of the service. * @param minor the minor revision number of the service. * @throws NullPointerException if the service type is {@code null}. * @throws IllegalArgumentException if revision values are invalid. */ public Version(Class<? extends Service> serviceClass, int major, int minor, Version ... impliedVersions) throws NullPointerException, IllegalArgumentException { if (serviceClass == null) { throw new NullPointerException("Null service class"); } if (major < 0 && major != ANY) { throw new IllegalArgumentException("Invalid major version:" + major); } if (minor < 0 && minor != ANY) { throw new IllegalArgumentException("Invalid minor version:" + minor); } this.serviceClass = serviceClass; this.major = major; this.minor = minor; // Compute the full list of implied versions computeImpliedVersions(impliedVersions); } /** * Creates a new Version instance using a version description with the * format <code>[{service}]{major}.{minor}</code>. * * @param serviceClass the service type. * @param versionDescription the service description. * @throws IllegalArgumentException if the versionDescription has an invalid * syntax or includes a service name that does not match the service * type. */ public Version(Class<? extends Service> serviceClass, String versionDescription, Version ... impliedVersions) throws IllegalArgumentException { this.serviceClass = serviceClass; Matcher matcher = VERSION_PROPERTY_PATTERN.matcher(versionDescription); if (!matcher.matches()) { throw new IllegalArgumentException( "Version description does not match expected format" + "[{service}]{major}[.{minor}]:" + versionDescription); } String minorValue = matcher.group(3); major = Integer.parseInt(matcher.group(2)); minor = (minorValue != null) ? Integer.parseInt(minorValue.substring(1)) : ANY; // Compute the full list of implied versions computeImpliedVersions(impliedVersions); } /** * Returns the service type of the version. * @return service type. */ public final Class<? extends Service> getServiceClass() { return serviceClass; } /** * Returns the major revision of the version. * @return major revision. */ public final int getMajor() { return major; } /** * Returns the minor revision of the version. * @return minor revision. */ public final int getMinor() { return minor; } /** * Returns the String representation of the version. */ public final String getVersionString() { StringBuilder sb = new StringBuilder(); if (major != ANY) { sb.append(major); } if (minor != ANY) { sb.append('.'); sb.append(minor); } return sb.toString(); } /** * Returns {@code true} if the target version is for the same service. * @param v target version to check. * @return {@code true} if service matches. */ public final boolean isSameService(Version v) { return v != null && serviceClass.equals(v.serviceClass); } /** * Returns {@code true} if the specified version is compatible with this * version or one of its implied versions. Two versions are compatible if they * are for the same service and have a matching major version number (or one * of them has a major version of {@link #ANY}). */ public final boolean isCompatible(Version v) { if (isSameService(v) && (major == v.major || major == ANY || v.major == ANY)) { return true; } else { for (Version impliedVersion : impliedVersions) { if (impliedVersion == this) { continue; } if (impliedVersion.isCompatible(v)) { return true; } } } return false; } /** * If the version number is {@link #ANY}, returns {@link Integer#MAX_VALUE}; * otherwise returns the input version number */ private int raiseAny(int versionNumber) { return (versionNumber != ANY) ? versionNumber : Integer.MAX_VALUE; } /** * Returns {@code true} if the specified version is a match with this version * or one of its implied versions. Two versions are compatible if they are for * the same service and have matching major and minor version numbers. A * version number is a match if it is the same or one is {@link #ANY}. */ public final boolean matches(Version v) { if (isSameService(v) && (major == v.major || major == ANY || v.major == ANY) && (minor == v.minor || minor == ANY || v.minor == ANY)) { return true; } for (Version impliedVersion : impliedVersions) { if (impliedVersion == this) { continue; } if (impliedVersion.matches(v)) { return true; } } return false; } /** * Returns {@code true} if this version is a later version than the argument, * on the basis of comparing the major and minor versions. For the purposes of * comparison, a value of {@link #ANY} is considered to be * {@link Integer#MAX_VALUE}, so no finite version number may come after it. * * @param v version to compare against * @return {@code true} if this version is later than the argument * @throws IllegalArgumentException if the provided version is not for the * same service as this version or for one implied by this version. */ public final boolean isAfter(Version v) { Version serviceVersion = findServiceVersion(impliedVersions, v.getServiceClass()); Preconditions.checkArgument(serviceVersion != null, "No relationship between versions"); int serviceMajor = raiseAny(serviceVersion.major); int vMajor = raiseAny(v.major); if (serviceMajor != vMajor) { return serviceMajor > vMajor; } else { return raiseAny(serviceVersion.minor) > raiseAny(v.minor); } } /** * Returns {@code true} if this version is a earlier version than the * argument, on the basis of comparing the major and minor versions. For the * purposes of comparison, a value of {@link #ANY} is considered to be * {@link Integer#MIN_VALUE}, so no finite version number may come before it. * * @param v version to compare against * @return {@code true} if this version is later than the argument * @throws IllegalArgumentException if the provided version is not for the * same service as this version or for one implied by this version. */ public final boolean isBefore(Version v) { Version serviceVersion = findServiceVersion(impliedVersions, v.getServiceClass()); Preconditions.checkArgument(serviceVersion != null, "No relationship between versions"); // No need to normalize the value of ANY here, since it's value (-1) // is already less than all concrete versions if (serviceVersion.major != v.major) { return serviceVersion.major < v.major; } else { return serviceVersion.minor < v.minor; } } /** * Returns the list of related services versions that are implied by this * version. */ public List<Version> getImpliedVersions() { return impliedVersions; } /** * Compute the fully resolved list of implied versions, including the * local instance and all directly and indirectly implied versions. * @param versionList the list of directly implied versions. */ private void computeImpliedVersions(Version ... versionList) { impliedVersions.add(this); for (Version v : versionList) { addImpliedVersion(v); } } /** * Adds an implied version (plus any of its nested dependencies) to the * implied versions list for this version. * @param v the implied version. */ private void addImpliedVersion(Version v) { if (!impliedVersions.contains(v)) { impliedVersions.add(v); for (Version impliedVersion : v.getImpliedVersions()) { addImpliedVersion(impliedVersion); } } } @Override public boolean equals(Object o) { if (!(o instanceof Version)) { return false; } Version v = (Version) o; return isSameService(v) && major == v.major && minor == v.minor; } @Override public int hashCode() { int result = serviceClass.hashCode(); result = 37 * result + major; result = 37 * result + minor; return result; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(serviceClass.getName()); sb.append(':'); sb.append(getVersionString()); return sb.toString(); } }