blob: 344c3a2cb7243c24ee614560a201e12308403420 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011, 2013 Oracle and/or its affiliates. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
* which accompanies this distribution.
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* Contributors:
* Oracle - initial API and implementation
*
******************************************************************************/
package org.eclipse.persistence.tools.utility;
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. <code>"2.2.2"</code> vs. <code>"2.14.3"</code>).
* 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
*/
@SuppressWarnings("nls")
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.<strong>14</strong>" > "2.<strong>2</strong>"</code>
* is <code>true</code>.
*/
public static final Comparator<String> INTEGER_VERSION_COMPARATOR = new VersionComparator<Integer>(SegmentParser.IntegerSegmentParser.instance());
/**
* Use the specified segment parser.
* The default delimiter is <code>'.'</code>.
*/
public VersionComparator(SegmentParser<T> segmentParser) {
this(".", segmentParser);
}
/**
* Use the specified delimiters and segment parser.
*/
public VersionComparator(char delimiter, SegmentParser<T> segmentParser) {
this(new char[] {delimiter}, segmentParser);
}
/**
* Use the specified delimiters and segment parser.
*/
public VersionComparator(char[] delimiters, SegmentParser<T> segmentParser) {
this(new String(delimiters), segmentParser);
}
/**
* Use the specified delimiters and segment parser.
*/
public VersionComparator(String delimiters, SegmentParser<T> segmentParser) {
super();
if ((delimiters == null) || (segmentParser == null)) {
throw new NullPointerException();
}
this.delimiters = delimiters;
this.segmentParser = segmentParser;
}
/**
* <strong>NB:</strong> Callers must handle any runtime 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.
*/
@Override
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.segmentParser.parse(i++, stream.nextToken()));
}
return segments;
}
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
@Override
public Integer parse(int segmentIndex, String segment) {
return Integer.valueOf(segment);
}
@Override
public Integer getZero() {
return ZERO;
}
private static final Integer ZERO = Integer.valueOf(0);
@Override
public String toString() {
return ObjectTools.singletonToString(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
@Override
public S parse(int segmentIndex, String segment) {
throw new UnsupportedOperationException();
}
@Override
public S getZero() {
throw new UnsupportedOperationException();
}
@Override
public String toString() {
return ObjectTools.singletonToString(this);
}
private static final long serialVersionUID = 1L;
private Object readResolve() {
// replace this object with the singleton
return INSTANCE;
}
}
}
}