001package co.codewizards.cloudstore.core.version;
002
003import java.io.Serializable;
004import java.util.Arrays;
005import java.util.regex.Matcher;
006import java.util.regex.Pattern;
007
008import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
009
010import co.codewizards.cloudstore.core.dto.jaxb.VersionXmlAdapter;
011
012/**
013 * This class represents a <code>version</code> of a piece of software.
014 * <p>
015 * A version <code>String</code> has the following form:
016 * <br/>
017 * <code>major</code>{@value #DEFAULT_SEPARATOR}<code>minor</code>{@value #DEFAULT_SEPARATOR}
018 * <code>release</code>[{@value #DEFAULT_SEPARATOR}<code>patchLevel</code>][{@value #SUFFIX_SEPARATOR}<code>suffix</code>]
019 * <br/>
020 * The suffix may consist of 'a-zA-Z0-9#$!_-'. Additionally the following version string is
021 * also valid: '<pre>1.2.3.test</pre>'.
022 * <p>
023 * Examples of valid versions are:
024 * <ul>
025 *      <li><pre>1.2.3</pre></li>
026 *      <li><pre>1.2.3.4</pre></li>
027 *      <li><pre>1.2.3.4-test</pre></li>
028 *      <li><pre>1.2.3-t_e!st</pre></li>
029 * </ul>
030 * <p>
031 * Instances of this class are immutable!
032 * <p>
033 * {@link #compareTo(Version)} understands the {@link #SNAPSHOT_SUFFIX}. This means:
034 * {@code 1.0.0-SNAPSHOT < 1.0.0}
035 * <p>
036 *
037 * @author Marius Heinzmann marius[AT]NightLabs[DOT]de
038 * @author Marco Schulze marco@nightlabs.de
039 * @author Niklas Schiffler <nick@nightlabs.de>
040 */
041@XmlJavaTypeAdapter(type=Version.class, value=VersionXmlAdapter.class)
042public class Version implements Comparable<Version>, Serializable
043{
044        public static final String SNAPSHOT_SUFFIX = "SNAPSHOT";
045
046        private static final long serialVersionUID = 1L;
047
048        /**
049         * The minimum Version that can exist.
050         */
051        public static final Version MIN_VERSION = new Version(0,0,0,0);
052
053        /**
054         * The maximum Version possible.
055         */
056        public static final Version MAX_VERSION = new Version(Integer.MAX_VALUE, Integer.MAX_VALUE,
057                        Integer.MAX_VALUE, Integer.MAX_VALUE);
058
059        private int major;
060        private int minor;
061        private int release;
062        private int patchLevel;
063        private String suffix;
064
065        public static final String DEFAULT_SEPARATOR = ".";
066        public static final String SUFFIX_SEPARATOR = "-";
067        private static final String EMPTYSTRING = "";
068
069        /**
070         * Creates a new instance from the given {@code versionStr}.
071         * @param versionStr the version encoded as a String.
072         * @throws MalformedVersionException
073         */
074        public Version(String versionStr)
075        throws MalformedVersionException
076        {
077                if (versionStr == null || versionStr.length() == 0)
078                        throw new IllegalArgumentException("versionStr must NEITHER be null NOR empty!");
079
080                parseVersion(versionStr);
081                validate();
082        }
083
084        /**
085         * @param major
086         * @param minor
087         * @param release
088         * @param patchLevel
089         */
090        public Version(int major, int minor, int release, int patchLevel) {
091                this(major, minor, release, patchLevel, null);
092        }
093
094        /**
095         * @param major
096         * @param minor
097         * @param release
098         * @param patchLevel
099         * @param suffix
100         */
101        public Version(int major, int minor, int release, int patchLevel, String suffix) {
102                this.major = major;
103                this.minor = minor;
104                this.release = release;
105                this.patchLevel = patchLevel;
106                if (suffix == null)
107                        suffix = EMPTYSTRING;
108//              TODO: should we limit the length of a suffix?? There is no need to, but it might become a
109//                                      Problem when persisting in the DB.
110//              else if (suffix.length() > MAX_STRING.length())
111//                      throw new IllegalArgumentException("The given suffix is too long! It is allowed to have a " +
112//                                      "maximum of "+MAX_STRING.length()+" characters!");
113
114                this.suffix = suffix;
115                validate();
116        }
117
118        /**
119         * A regex that is used to check the suffix for valid characters.
120         */
121        public static Pattern validityCheck = null;
122
123        private void validate() {
124                if (major < 0)
125                        throw new MalformedVersionException("Negative major versions are invalid!");
126                if (minor < 0)
127                        throw new MalformedVersionException("Negative minor versions are invalid!");
128                if (release < 0)
129                        throw new MalformedVersionException("Negative release versions are invalid!");
130                if (patchLevel < 0)
131                        throw new MalformedVersionException("Negative patchlevels are invalid!");
132
133                if (suffix == null || suffix.length() == 0)
134                        return;
135
136                // This omits parsing problems of other classes which may as well be represented as strings
137                // and have other separators which may be used inside this suffix.
138                if (validityCheck == null)
139                        validityCheck = Pattern.compile("[\\w#\\$!\\-]*");
140
141                 if (! validityCheck.matcher(suffix).matches())
142                         throw new MalformedVersionException("The suffix contains illegal characters! Suffix='"
143                                         +suffix+"'. Legal characters ='a-zA-Z0-9#$!_-'.");
144        }
145
146        /**
147         * @return Returns the major.
148         */
149        public int getMajor() {
150                return major;
151        }
152
153        /**
154         * @param major The major with which to create a new Version.
155         * @return A copy of this Version except for the given new major.
156         */
157        public Version changeMajor(int major) {
158                return new Version(major, minor, release, patchLevel, suffix);
159        }
160
161        /**
162         * @return Returns the minor.
163         */
164        public int getMinor() {
165                return minor;
166        }
167
168        /**
169         * @param minor The minor with which to create a new Version.
170         * @return A copy of this Version except for the given new minor.
171         */
172        public Version changeMinor(int minor) {
173                return new Version(major, minor, release, patchLevel, suffix);
174        }
175
176        /**
177         * @return Returns the patchLevel.
178         */
179        public int getPatchLevel() {
180                return patchLevel;
181        }
182
183        /**
184         * @param patchLevel The patchLevel with which to create a new Version.
185         * @return A copy of this Version except for the given new patchLevel.
186         */
187        public Version changePatchLevel(int patchLevel) {
188                return new Version(major, minor, release, patchLevel, suffix);
189        }
190
191        /**
192         * @return Returns the release.
193         */
194        public int getRelease() {
195                return release;
196        }
197
198        /**
199         * @param release The <code>release</code> with which to create a new Version.
200         * @return A copy of this Version except for the given new <code>release</code>.
201         */
202        public Version changeRelease(int release) {
203                return new Version(major, minor, release, patchLevel, suffix);
204        }
205
206        /**
207         * @return Returns the suffix.
208         */
209        public String getSuffix() {
210                return suffix;
211        }
212
213        /**
214         * @param suffix The <code>suffix</code> with which to create a new Version.
215         * @return A copy of this Version except for the given new <code>suffix</code>.
216         */
217        public Version changeSuffix(String suffix) {
218                return new Version(major, minor, release, patchLevel, suffix);
219        }
220
221        /**
222         * Returns the String representation of this version, which is defined as:
223         * <code>major</code>{@value #DEFAULT_SEPARATOR}<code>minor</code>{@value #DEFAULT_SEPARATOR}
224         * <code>release</code>[{@value #DEFAULT_SEPARATOR}<code>patchLevel</code>
225         * [{@value #DEFAULT_SEPARATOR}<code>suffix</code>]] <br>
226         * The suffix may consist of 'a-zA-Z0-9#$!_-'. Additionally the following version string is
227         * also valid: '<pre>1.2.3.test</pre>'.
228         *
229         * @return the <code>String</code> representation of this version.
230         */
231        @Override
232        public String toString() {
233                StringBuffer sb = new StringBuffer();
234                sb.append(major);
235                sb.append(DEFAULT_SEPARATOR);
236                sb.append(minor);
237                sb.append(DEFAULT_SEPARATOR);
238                sb.append(release);
239                if (patchLevel != 0) {
240                        sb.append(DEFAULT_SEPARATOR);
241                        sb.append(patchLevel);
242                }
243                if (suffix != null && !EMPTYSTRING.equals(suffix)) {
244                        sb.append(SUFFIX_SEPARATOR);
245                        sb.append(suffix);
246                }
247                return sb.toString();
248        }
249
250        /**
251         * Creates a Version out of its String ({@link #toString()}) representation. The String should
252         * conform to the following pattern:
253         * <code>major</code>{@value #DEFAULT_SEPARATOR}<code>minor</code>{@value #DEFAULT_SEPARATOR}
254         * <code>release</code>{@value #DEFAULT_SEPARATOR}<code>patchLevel</code>
255         * {@value #DEFAULT_SEPARATOR}<code>suffix</code>.
256         *
257         * @param version the {@link #toString()} representation of a version.
258         * @throws MalformedVersionException if the given <code>String</code> does not conform to the
259         *      pattern described.
260         */
261        private void parseVersion(String version)
262                throws MalformedVersionException
263        {
264                Pattern versionPattern = Pattern.compile(
265//              The whole rexexp not escaped
266//                      (\d+)\.(\d+)\.(\d+)(?:[-\.](?:(\d+)|([\w#\$!\-]+)|(?:(\d+)[-\.]([\w#\$!\-]+))))?\s*\z
267                                "(\\d+)\\.(\\d+)\\.(\\d+)(?:[-\\.](?:(\\d+)|([\\w#\\$!\\-]+)|(?:(\\d+)[-\\.]([\\w#\\$!\\-]+))))?\\s*\\z");
268//              EOI = end of input; OPT(..) = optional; | = or
269//        major  . minor  . release OPT( ./- (patchLevel  | suffix | patchLevel ./- suffix)) whitespace* EOI
270//              the endofinput prohibits the parsing of '1.7.0-9-asf.narf' which would otherwise be read as
271//              '1.7.0-9-asf'.
272                Matcher m = versionPattern.matcher(version);
273                if(m.find())
274                {
275                        this.major = Integer.parseInt(m.group(1));
276                        this.minor = Integer.parseInt(m.group(2));
277                        this.release = Integer.parseInt(m.group(3));
278
279                        // Assumption is that if the element in the 4th group is parseable as an int -> it is an int.
280                        // So given the string '1.2.3.02' the '02' is interpreted as an int, not a string!
281                        if (m.group(4) != null)
282                        {
283                                try
284                                {
285//                              assume we got a small JFire version without suffix.
286                                        this.patchLevel = Integer.parseInt(m.group(4));
287                                        this.suffix = "";
288                                }
289                                catch (NumberFormatException nfe)
290                                {
291//                              since the number is too big -> it must be a suffix consisting only of numbers
292                                        this.patchLevel = 0;
293                                        this.suffix = m.group(4);
294                                }
295                        }
296                        else if (m.group(5) != null)
297                        {
298//                      assume that we are given an osgi-version -> group(5) = suffix
299                                this.patchLevel = 0;
300                                this.suffix = m.group(5);
301                        }
302                        else if (m.group(6) != null && m.group(7) != null)
303                        {
304//                              assume we have a full JFire version
305                                try {
306                                        this.patchLevel = Integer.parseInt(m.group(6));
307                                        this.suffix = m.group(7);
308                                } catch (NumberFormatException nfe2) {
309                                        throw new MalformedVersionException("The patchlevel has to be a parseable number! " +
310                                                        "given patchlevel: '"+m.group(6)+"' given encoded version: '"+version+"'");
311                                }
312                        }
313                        else
314                        {
315                                // assume we have only major.minor.release
316                                for (int i = 4; i < 8; i++)
317                                {
318                                        if (m.group(i) != null)
319                                                throw new MalformedVersionException("Malformed version string: " + version);
320                                }
321                        }
322                }
323        }
324
325        @Override
326        public boolean equals(Object other) {
327                if (this == other) return true;
328                if (!(other instanceof Version)) return false;
329
330                final Version otherVersion = (Version) other;
331
332                return major == otherVersion.major
333                                && minor == otherVersion.minor
334                                && release == otherVersion.release
335                                && patchLevel == otherVersion.patchLevel
336                                && equals(suffix, otherVersion.suffix);
337        }
338
339        private static boolean equals(Object obj0, Object obj1)
340        {
341                if (obj0 instanceof Object[] && obj1 instanceof Object[])
342                        return obj0 == obj1 || Arrays.equals((Object[])obj0, (Object[])obj1);
343                return obj0 == obj1 || (obj0 != null && obj0.equals(obj1));
344        }
345
346        @Override
347        public int hashCode() {
348                final int PRIME = 31;
349                int result = 1;
350                result = result * PRIME + major;
351                result = result * PRIME + minor;
352                result = result * PRIME + release;
353                result = result * PRIME + patchLevel;
354                result = result * PRIME + (suffix == null ? 0 : suffix.hashCode());
355                return result;
356        }
357
358        /**
359         * Compares this <code>Version</code> object to another one.
360         *
361         * <p>
362         * A version is considered to be <b>less than </b> another version if its major component is less
363         * than the other version's major component, or the major components are equal and its minor
364         * component is less than the other version's minor component, or the major and minor components
365         * are equal and its release component is less than the other version's release component.
366         * The suffix is not relevant for the natural ordering of Versions, though for equality.
367         * <p>
368         * Note: Given two version <code>v1,v2</code> and <code>v1.compareTo(Version v2) = 0</code>,
369         * this does not imply that <code>v1</code> and <code>v2</code> are equal according to equals!
370         * </p>
371         *
372         * <p>
373         * A version is considered to be <b>equal to</b> another version if the
374         * major, minor and micro components are equal and the qualifier component
375         * is equal (see {@link #equals(Object)}).
376         * </p>
377         *
378         * @param other The <code>Version</code> object to be compared.
379         * @return A negative integer, zero, or a positive integer if this object is
380         *         less than, equal to, or greater than the specified
381         *         <code>Version</code> object.
382         */
383        @Override
384        public int compareTo(Version other) {
385                if (this == other)
386                        return 0;
387
388                int difference = major - other.major;
389                if (difference != 0)
390                        return difference;
391
392                difference = minor - other.minor;
393                if (difference != 0)
394                        return difference;
395
396                difference = release - other.release;
397                if (difference != 0)
398                        return difference;
399
400                difference = patchLevel - other.patchLevel;
401                if (difference != 0)
402                        return difference;
403
404                // We currently take only the "SNAPSHOT" suffix into account. Every other
405                // suffix is ignored when it comes to comparing.
406                if (suffix == null) {
407                        if (other.suffix == null)
408                                return 0;
409
410                        if (other.suffix.equalsIgnoreCase(SNAPSHOT_SUFFIX))
411                                return 1;
412
413                        // We currently ignore all suffix values except for the "SNAPSHOT". They are purely informational.
414                        return 0;
415                }
416
417                if (suffix.equalsIgnoreCase(other.suffix))
418                        return 0;
419
420                if (suffix.equalsIgnoreCase(SNAPSHOT_SUFFIX))
421                        return -1;
422
423                // We currently ignore all suffix values except for the "SNAPSHOT". They are purely informational.
424                return 0;
425        }
426
427}