blob: 9aa9603a9d4c87f45eb8190b7928a89eba90cf0f [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011 Oracle. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0, which accompanies this distribution
* and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Contributors:
* Oracle - initial API and implementation
******************************************************************************/
package org.eclipse.jpt.common.utility.internal;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.StringTokenizer;
/**
* This comparator can be used to compare version strings (e.g. "2.2.2" vs.
* "2.14.3"). Clients can specify the delimiter(s) that separates a version's
* <em>segments</em> as well as a parser to be used for parsing each
* <em>segment</em>.
*
* @see #INTEGER_VERSION_COMPARATOR
*/
public class VersionComparator<T extends Comparable<T>>
implements Comparator<String>
{
private final String delimiters;
private final SegmentParser<T> segmentParser;
/**
* Static implementation of the version comparator interface that converts
* each version into a series of integers and compares them.
* <p>
* <strong>NB:</strong> With this comparator <code>"2.14" > "2.2"</code>
*/
public static final Comparator<String> INTEGER_VERSION_COMPARATOR = new VersionComparator<Integer>(SegmentParser.IntegerSegmentParser.instance());
/**
* The default delimiter is <code>'.'</code>.
* The default segment parser is disabled.
* <p>
* <strong>NB:</strong> Subclass must override:<ul>
* <li>{@link #parseSegment(int, String)}
* <li>{@link #getZero()}
* </ul>
*/
protected VersionComparator() {
this("."); //$NON-NLS-1$
}
/**
* The default segment parser is disabled.
* <p>
* <strong>NB:</strong> Subclass must override:<ul>
* <li>{@link #parseSegment(int, String)}
* <li>{@link #getZero()}
* </ul>
*/
protected VersionComparator(String delimiters) {
this(delimiters, SegmentParser.Disabled.<T>instance());
}
/**
* Use the specified segment parser.
* The default delimiter is <code>'.'</code>.
*/
public VersionComparator(SegmentParser<T> segmentParser) {
this(".", segmentParser); //$NON-NLS-1$
}
/**
* Use the specified delimiters and segment parser.
*/
public VersionComparator(String delimiters, SegmentParser<T> segmentParser) {
super();
this.delimiters = delimiters;
this.segmentParser = segmentParser;
}
/**
* <strong>NB:</strong> Callers must handle any exceptions thrown by the
* segment parser supplied to the comparator. In particular, the pre-built
* integer segment parser {@link #INTEGER_VERSION_COMPARATOR} can throw a
* {@link NumberFormatException} if any segement string contains non-numeric
* characters.
*/
public int compare(String version1, String version2) {
ArrayList<T> segments1 = this.parseVersion(version1);
ArrayList<T> segments2 = this.parseVersion(version2);
int size1 = segments1.size();
int size2 = segments2.size();
int min = Math.min(size1, size2);
for (int i = 0; i < min; i++) {
int segmentCompare = segments1.get(i).compareTo(segments2.get(i));
if (segmentCompare != 0) {
return segmentCompare;
}
}
if (size1 == size2) {
return 0;
}
int max = Math.max(size1, size2);
T zero = this.getZero();
if (size1 < size2) {
for (int i = min; i < max; i++) {
int segmentCompare = zero.compareTo(segments2.get(i));
if (segmentCompare != 0) {
return segmentCompare;
}
}
} else {
for (int i = min; i < max; i++) {
int segmentCompare = segments1.get(i).compareTo(zero);
if (segmentCompare != 0) {
return segmentCompare;
}
}
}
return 0;
}
/**
* Parse the specified version into a list of segments that can be
* compared individually.
*/
protected ArrayList<T> parseVersion(String s) {
ArrayList<T> segments = new ArrayList<T>();
int i = 0;
for (StringTokenizer stream = new StringTokenizer(s, this.delimiters); stream.hasMoreTokens(); ) {
segments.add(this.parseSegment(i++, stream.nextToken()));
}
return segments;
}
/**
* Parse the specified segment into the appropriate comparable.
* Subclasses must override this method if a segment parser is not passed
* to the version comparator's constructor.
*/
protected T parseSegment(int index, String s) {
return this.segmentParser.parse(index, s);
}
protected T getZero() {
return this.segmentParser.getZero();
}
/**
* A segment parser is used by a version comparator to convert each
* <em>segment</em> of a version into something that can be compared to the
* corresponding <em>segment</em> in another version.
*/
public interface SegmentParser<T extends Comparable<T>> {
/**
* Convert the specified version <em>segment</em> into something that
* can be compared to the corresponding <em>segment</em> in another
* version.
*/
T parse(int segmentIndex, String segment);
/**
* Return a "zero" <em>segment</em> value that can be compared to
* trailing segments when two version have differing numbers of
* <em>segments</em>.
*/
T getZero();
/**
* Singleton implementation of the segment parser interface that converts
* each segment into an integer, irrespective of position.
* <p>
* <strong>NB:</strong> With this parser <code>"2.14" > "2.2"</code>
*/
final class IntegerSegmentParser
implements SegmentParser<Integer>, Serializable
{
public static final SegmentParser<Integer> INSTANCE = new IntegerSegmentParser();
public static SegmentParser<Integer> instance() {
return INSTANCE;
}
// ensure single instance
private IntegerSegmentParser() {
super();
}
// simply parse the segment as an integer
public Integer parse(int segmentIndex, String segment) {
return Integer.valueOf(segment);
}
public Integer getZero() {
return ZERO;
}
private static final Integer ZERO = Integer.valueOf(0);
@Override
public String toString() {
return StringTools.buildSingletonToString(this);
}
private static final long serialVersionUID = 1L;
private Object readResolve() {
// replace this object with the singleton
return INSTANCE;
}
}
/**
* Singleton implementation of the segment parser interface that throws
* an exception if called.
*/
final class Disabled<S extends Comparable<S>>
implements SegmentParser<S>, Serializable
{
@SuppressWarnings("rawtypes")
public static final SegmentParser INSTANCE = new Disabled();
@SuppressWarnings("unchecked")
public static <R extends Comparable<R>> SegmentParser<R> instance() {
return INSTANCE;
}
// ensure single instance
private Disabled() {
super();
}
// throw an exception
public S parse(int segmentIndex, String segment) {
throw new UnsupportedOperationException();
}
public S getZero() {
throw new UnsupportedOperationException();
}
@Override
public String toString() {
return StringTools.buildSingletonToString(this);
}
private static final long serialVersionUID = 1L;
private Object readResolve() {
// replace this object with the singleton
return INSTANCE;
}
}
}
}