package co.codewizards.cloudstore.core.version; import java.io.Serializable; import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import co.codewizards.cloudstore.core.dto.jaxb.VersionXmlAdapter; /** * This class represents a <code>version</code> of a piece of software. * <p> * A version <code>String</code> has the following form: * <br/> * <code>major</code>{@value #DEFAULT_SEPARATOR}<code>minor</code>{@value #DEFAULT_SEPARATOR} * <code>release</code>[{@value #DEFAULT_SEPARATOR}<code>patchLevel</code>][{@value #SUFFIX_SEPARATOR}<code>suffix</code>] * <br/> * The suffix may consist of 'a-zA-Z0-9#$!_-'. Additionally the following version string is * also valid: '<pre>1.2.3.test</pre>'. * <p> * Examples of valid versions are: * <ul> * <li><pre>1.2.3</pre></li> * <li><pre>1.2.3.4</pre></li> * <li><pre>1.2.3.4-test</pre></li> * <li><pre>1.2.3-t_e!st</pre></li> * </ul> * <p> * Instances of this class are immutable! * <p> * {@link #compareTo(Version)} understands the {@link #SNAPSHOT_SUFFIX}. This means: * {@code 1.0.0-SNAPSHOT < 1.0.0} * <p> * * @author Marius Heinzmann marius[AT]NightLabs[DOT]de * @author Marco Schulze marco@nightlabs.de * @author Niklas Schiffler <nick@nightlabs.de> */ @XmlJavaTypeAdapter(type=Version.class, value=VersionXmlAdapter.class) public class Version implements Comparable<Version>, Serializable { public static final String SNAPSHOT_SUFFIX = "SNAPSHOT"; private static final long serialVersionUID = 1L; /** * The minimum Version that can exist. */ public static final Version MIN_VERSION = new Version(0,0,0,0); /** * The maximum Version possible. */ public static final Version MAX_VERSION = new Version(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); private int major; private int minor; private int release; private int patchLevel; private String suffix; public static final String DEFAULT_SEPARATOR = "."; public static final String SUFFIX_SEPARATOR = "-"; private static final String EMPTYSTRING = ""; /** * Creates a new instance from the given {@code versionStr}. * @param versionStr the version encoded as a String. * @throws MalformedVersionException */ public Version(String versionStr) throws MalformedVersionException { if (versionStr == null || versionStr.length() == 0) throw new IllegalArgumentException("versionStr must NEITHER be null NOR empty!"); parseVersion(versionStr); validate(); } /** * @param major * @param minor * @param release * @param patchLevel */ public Version(int major, int minor, int release, int patchLevel) { this(major, minor, release, patchLevel, null); } /** * @param major * @param minor * @param release * @param patchLevel * @param suffix */ public Version(int major, int minor, int release, int patchLevel, String suffix) { this.major = major; this.minor = minor; this.release = release; this.patchLevel = patchLevel; if (suffix == null) suffix = EMPTYSTRING; // TODO: should we limit the length of a suffix?? There is no need to, but it might become a // Problem when persisting in the DB. // else if (suffix.length() > MAX_STRING.length()) // throw new IllegalArgumentException("The given suffix is too long! It is allowed to have a " + // "maximum of "+MAX_STRING.length()+" characters!"); this.suffix = suffix; validate(); } /** * A regex that is used to check the suffix for valid characters. */ public static Pattern validityCheck = null; private void validate() { if (major < 0) throw new MalformedVersionException("Negative major versions are invalid!"); if (minor < 0) throw new MalformedVersionException("Negative minor versions are invalid!"); if (release < 0) throw new MalformedVersionException("Negative release versions are invalid!"); if (patchLevel < 0) throw new MalformedVersionException("Negative patchlevels are invalid!"); if (suffix == null || suffix.length() == 0) return; // This omits parsing problems of other classes which may as well be represented as strings // and have other separators which may be used inside this suffix. if (validityCheck == null) validityCheck = Pattern.compile("[\\w#\\$!\\-]*"); if (! validityCheck.matcher(suffix).matches()) throw new MalformedVersionException("The suffix contains illegal characters! Suffix='" +suffix+"'. Legal characters ='a-zA-Z0-9#$!_-'."); } /** * @return Returns the major. */ public int getMajor() { return major; } /** * @param major The major with which to create a new Version. * @return A copy of this Version except for the given new major. */ public Version changeMajor(int major) { return new Version(major, minor, release, patchLevel, suffix); } /** * @return Returns the minor. */ public int getMinor() { return minor; } /** * @param minor The minor with which to create a new Version. * @return A copy of this Version except for the given new minor. */ public Version changeMinor(int minor) { return new Version(major, minor, release, patchLevel, suffix); } /** * @return Returns the patchLevel. */ public int getPatchLevel() { return patchLevel; } /** * @param patchLevel The patchLevel with which to create a new Version. * @return A copy of this Version except for the given new patchLevel. */ public Version changePatchLevel(int patchLevel) { return new Version(major, minor, release, patchLevel, suffix); } /** * @return Returns the release. */ public int getRelease() { return release; } /** * @param release The <code>release</code> with which to create a new Version. * @return A copy of this Version except for the given new <code>release</code>. */ public Version changeRelease(int release) { return new Version(major, minor, release, patchLevel, suffix); } /** * @return Returns the suffix. */ public String getSuffix() { return suffix; } /** * @param suffix The <code>suffix</code> with which to create a new Version. * @return A copy of this Version except for the given new <code>suffix</code>. */ public Version changeSuffix(String suffix) { return new Version(major, minor, release, patchLevel, suffix); } /** * Returns the String representation of this version, which is defined as: * <code>major</code>{@value #DEFAULT_SEPARATOR}<code>minor</code>{@value #DEFAULT_SEPARATOR} * <code>release</code>[{@value #DEFAULT_SEPARATOR}<code>patchLevel</code> * [{@value #DEFAULT_SEPARATOR}<code>suffix</code>]] <br> * The suffix may consist of 'a-zA-Z0-9#$!_-'. Additionally the following version string is * also valid: '<pre>1.2.3.test</pre>'. * * @return the <code>String</code> representation of this version. */ @Override public String toString() { StringBuffer sb = new StringBuffer(); sb.append(major); sb.append(DEFAULT_SEPARATOR); sb.append(minor); sb.append(DEFAULT_SEPARATOR); sb.append(release); if (patchLevel != 0) { sb.append(DEFAULT_SEPARATOR); sb.append(patchLevel); } if (suffix != null && !EMPTYSTRING.equals(suffix)) { sb.append(SUFFIX_SEPARATOR); sb.append(suffix); } return sb.toString(); } /** * Creates a Version out of its String ({@link #toString()}) representation. The String should * conform to the following pattern: * <code>major</code>{@value #DEFAULT_SEPARATOR}<code>minor</code>{@value #DEFAULT_SEPARATOR} * <code>release</code>{@value #DEFAULT_SEPARATOR}<code>patchLevel</code> * {@value #DEFAULT_SEPARATOR}<code>suffix</code>. * * @param version the {@link #toString()} representation of a version. * @throws MalformedVersionException if the given <code>String</code> does not conform to the * pattern described. */ private void parseVersion(String version) throws MalformedVersionException { Pattern versionPattern = Pattern.compile( // The whole rexexp not escaped // (\d+)\.(\d+)\.(\d+)(?:[-\.](?:(\d+)|([\w#\$!\-]+)|(?:(\d+)[-\.]([\w#\$!\-]+))))?\s*\z "(\\d+)\\.(\\d+)\\.(\\d+)(?:[-\\.](?:(\\d+)|([\\w#\\$!\\-]+)|(?:(\\d+)[-\\.]([\\w#\\$!\\-]+))))?\\s*\\z"); // EOI = end of input; OPT(..) = optional; | = or // major . minor . release OPT( ./- (patchLevel | suffix | patchLevel ./- suffix)) whitespace* EOI // the endofinput prohibits the parsing of '1.7.0-9-asf.narf' which would otherwise be read as // '1.7.0-9-asf'. Matcher m = versionPattern.matcher(version); if(m.find()) { this.major = Integer.parseInt(m.group(1)); this.minor = Integer.parseInt(m.group(2)); this.release = Integer.parseInt(m.group(3)); // Assumption is that if the element in the 4th group is parseable as an int -> it is an int. // So given the string '1.2.3.02' the '02' is interpreted as an int, not a string! if (m.group(4) != null) { try { // assume we got a small JFire version without suffix. this.patchLevel = Integer.parseInt(m.group(4)); this.suffix = ""; } catch (NumberFormatException nfe) { // since the number is too big -> it must be a suffix consisting only of numbers this.patchLevel = 0; this.suffix = m.group(4); } } else if (m.group(5) != null) { // assume that we are given an osgi-version -> group(5) = suffix this.patchLevel = 0; this.suffix = m.group(5); } else if (m.group(6) != null && m.group(7) != null) { // assume we have a full JFire version try { this.patchLevel = Integer.parseInt(m.group(6)); this.suffix = m.group(7); } catch (NumberFormatException nfe2) { throw new MalformedVersionException("The patchlevel has to be a parseable number! " + "given patchlevel: '"+m.group(6)+"' given encoded version: '"+version+"'"); } } else { // assume we have only major.minor.release for (int i = 4; i < 8; i++) { if (m.group(i) != null) throw new MalformedVersionException("Malformed version string: " + version); } } } } @Override public boolean equals(Object other) { if (this == other) return true; if (!(other instanceof Version)) return false; final Version otherVersion = (Version) other; return major == otherVersion.major && minor == otherVersion.minor && release == otherVersion.release && patchLevel == otherVersion.patchLevel && equals(suffix, otherVersion.suffix); } private static boolean equals(Object obj0, Object obj1) { if (obj0 instanceof Object[] && obj1 instanceof Object[]) return obj0 == obj1 || Arrays.equals((Object[])obj0, (Object[])obj1); return obj0 == obj1 || (obj0 != null && obj0.equals(obj1)); } @Override public int hashCode() { final int PRIME = 31; int result = 1; result = result * PRIME + major; result = result * PRIME + minor; result = result * PRIME + release; result = result * PRIME + patchLevel; result = result * PRIME + (suffix == null ? 0 : suffix.hashCode()); return result; } /** * Compares this <code>Version</code> object to another one. * * <p> * A version is considered to be <b>less than </b> another version if its major component is less * than the other version's major component, or the major components are equal and its minor * component is less than the other version's minor component, or the major and minor components * are equal and its release component is less than the other version's release component. * The suffix is not relevant for the natural ordering of Versions, though for equality. * <p> * Note: Given two version <code>v1,v2</code> and <code>v1.compareTo(Version v2) = 0</code>, * this does not imply that <code>v1</code> and <code>v2</code> are equal according to equals! * </p> * * <p> * A version is considered to be <b>equal to</b> another version if the * major, minor and micro components are equal and the qualifier component * is equal (see {@link #equals(Object)}). * </p> * * @param other The <code>Version</code> object to be compared. * @return A negative integer, zero, or a positive integer if this object is * less than, equal to, or greater than the specified * <code>Version</code> object. */ @Override public int compareTo(Version other) { if (this == other) return 0; int difference = major - other.major; if (difference != 0) return difference; difference = minor - other.minor; if (difference != 0) return difference; difference = release - other.release; if (difference != 0) return difference; difference = patchLevel - other.patchLevel; if (difference != 0) return difference; // We currently take only the "SNAPSHOT" suffix into account. Every other // suffix is ignored when it comes to comparing. if (suffix == null) { if (other.suffix == null) return 0; if (other.suffix.equalsIgnoreCase(SNAPSHOT_SUFFIX)) return 1; // We currently ignore all suffix values except for the "SNAPSHOT". They are purely informational. return 0; } if (suffix.equalsIgnoreCase(other.suffix)) return 0; if (suffix.equalsIgnoreCase(SNAPSHOT_SUFFIX)) return -1; // We currently ignore all suffix values except for the "SNAPSHOT". They are purely informational. return 0; } }