diff --git a/src/main/java/com/eekboom/utils/Strings.java b/src/main/java/com/eekboom/utils/Strings.java new file mode 100644 index 0000000..a74df43 --- /dev/null +++ b/src/main/java/com/eekboom/utils/Strings.java @@ -0,0 +1,353 @@ +/* + * Copyright (c) 2006, Stephen Kelvin Friedrich, All rights reserved. + * + * This a BSD license. If you use or enhance the code, I'd be pleased if you sent a mail to s.friedrich@eekboom.com + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the + * following conditions are met: + * * Redistributions of source code must retain the above copyright notice, this list of conditions and the + * following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other materials provided with the distribution. + * * Neither the name of the "Stephen Kelvin Friedrich" nor the names of its contributors may be used to endorse + * or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.eekboom.utils; + +import java.text.Collator; +import java.util.Comparator; + +/** + * Utility class for common String operations + */ +public final class Strings { + /** + *
A string comparator that does case sensitive comparisons and handles embedded numbers correctly.
+ *Do not use if your app might ever run on any locale that uses more than 7-bit ascii characters.
+ */ + private static final ComparatorA string comparator that does case insensitive comparisons and handles embedded numbers correctly.
+ *Do not use if your app might ever run on any locale that uses more than 7-bit ascii characters.
+ */ + private static final ComparatorFor example in German locale this will be a comparator that handles umlauts correctly and ignores + * upper/lower case differences.
+ * + * @returnA string comparator that uses the current locale's order rules and handles embedded numbers + * correctly.
+ * @see #getNaturalComparator(java.text.Collator) + */ + public static ComparatorA string comparator that uses the given Collator to compare subwords and handles embedded numbers + * correctly.
+ * @see #getNaturalComparator() + */ + public static Comparatora string comparator that does case sensitive comparisons on pure ascii strings and handles embedded + * numbers correctly.
+ * Do not use if your app might ever run on any locale that uses more than 7-bit ascii characters. + * @see #getNaturalComparator() + * @see #getNaturalComparator(java.text.Collator) + */ + public static Comparatora string comparator that does case insensitive comparisons on pure ascii strings and handles embedded + * numbers correctly.
+ * @see #getNaturalComparator() + * @see #getNaturalComparator(java.text.Collator) + */ + public static ComparatorCompares two strings using the current locale's rules and comparing contained numbers based on their numeric + * values.
+ *This is probably the best default comparison to use.
+ *If you know that the texts to be compared are in a certain language that differs from the default locale's + * langage, then get a collator for the desired locale ({@link java.text.Collator#getInstance(java.util.Locale)}) + * and pass it to {@link #compareNatural(java.text.Collator, String, String)}
+ * + * @param s first string + * @param t second string + * @return zero iffs and t are equal,
+ * a value less than zero iff s lexicographically precedes t
+ * and a value larger than zero iff s lexicographically follows t
+ */
+ public static int compareNatural(String s, String t) {
+ return compareNatural(s, t, false, Collator.getInstance());
+ }
+
+ /**
+ * Compares two strings using the given collator and comparing contained numbers based on their numeric + * values.
+ * + * @param s first string + * @param t second string + * @return zero iffs and t are equal,
+ * a value less than zero iff s lexicographically precedes t
+ * and a value larger than zero iff s lexicographically follows t
+ */
+ public static int compareNatural(Collator collator, String s, String t) {
+ return compareNatural(s, t, true, collator);
+ }
+
+ /**
+ * Compares two strings using each character's Unicode value for non-digit characters and the numeric values off + * any contained numbers.
+ *(This will probably make sense only for strings containing 7-bit ascii characters only.)
+ * + * @return zero iffs and t are equal,
+ * a value less than zero iff s lexicographically precedes t
+ * and a value larger than zero iff s lexicographically follows t
+ */
+ public static int compareNaturalAscii(String s, String t) {
+ return compareNatural(s, t, true, null);
+ }
+
+ /**
+ * Compares two strings using each character's Unicode value - ignoring upper/lower case - for non-digit + * characters and the numeric values of any contained numbers.
+ *(This will probably make sense only for strings containing 7-bit ascii characters only.)
+ * + * @return zero iffs and t are equal,
+ * a value less than zero iff s lexicographically precedes t
+ * and a value larger than zero iff s lexicographically follows t
+ */
+ public static int compareNaturalIgnoreCaseAscii(String s, String t) {
+ return compareNatural(s, t, false, null);
+ }
+
+ /**
+ * @param s first string
+ * @param t second string
+ * @param caseSensitive treat characters differing in case only as equal - will be ignored if a collator is given
+ * @param collator used to compare subwords that aren't numbers - if null, characters will be compared
+ * individually based on their Unicode value
+ * @return zero iff s and t are equal,
+ * a value less than zero iff s lexicographically precedes t
+ * and a value larger than zero iff s lexicographically follows t
+ */
+ private static int compareNatural(String s, String t, boolean caseSensitive, Collator collator) {
+ int sIndex = 0;
+ int tIndex = 0;
+
+ int sLength = s.length();
+ int tLength = t.length();
+
+ while(true) {
+ // both character indices are after a subword (or at zero)
+
+ // Check if one string is at end
+ if(sIndex == sLength && tIndex == tLength) {
+ return 0;
+ }
+ if(sIndex == sLength) {
+ return -1;
+ }
+ if(tIndex == tLength) {
+ return 1;
+ }
+
+ // Compare sub word
+ char sChar = s.charAt(sIndex);
+ char tChar = t.charAt(tIndex);
+
+ boolean sCharIsDigit = Character.isDigit(sChar);
+ boolean tCharIsDigit = Character.isDigit(tChar);
+
+ if(sCharIsDigit && tCharIsDigit) {
+ // Compare numbers
+
+ // skip leading 0s
+ int sLeadingZeroCount = 0;
+ while(sChar == '0') {
+ ++sLeadingZeroCount;
+ ++sIndex;
+ if(sIndex == sLength) {
+ break;
+ }
+ sChar = s.charAt(sIndex);
+ }
+ int tLeadingZeroCount = 0;
+ while(tChar == '0') {
+ ++tLeadingZeroCount;
+ ++tIndex;
+ if(tIndex == tLength) {
+ break;
+ }
+ tChar = t.charAt(tIndex);
+ }
+ boolean sAllZero = sIndex == sLength || !Character.isDigit(sChar);
+ boolean tAllZero = tIndex == tLength || !Character.isDigit(tChar);
+ if(sAllZero && tAllZero) {
+ continue;
+ }
+ if(sAllZero && !tAllZero) {
+ return -1;
+ }
+ if(tAllZero) {
+ return 1;
+ }
+
+ int diff = 0;
+ do {
+ if(diff == 0) {
+ diff = sChar - tChar;
+ }
+ ++sIndex;
+ ++tIndex;
+ if(sIndex == sLength && tIndex == tLength) {
+ return diff != 0 ? diff : sLeadingZeroCount - tLeadingZeroCount;
+ }
+ if(sIndex == sLength) {
+ if(diff == 0) {
+ return -1;
+ }
+ return Character.isDigit(t.charAt(tIndex)) ? -1 : diff;
+ }
+ if(tIndex == tLength) {
+ if(diff == 0) {
+ return 1;
+ }
+ return Character.isDigit(s.charAt(sIndex)) ? 1 : diff;
+ }
+ sChar = s.charAt(sIndex);
+ tChar = t.charAt(tIndex);
+ sCharIsDigit = Character.isDigit(sChar);
+ tCharIsDigit = Character.isDigit(tChar);
+ if(!sCharIsDigit && !tCharIsDigit) {
+ // both number sub words have the same length
+ if(diff != 0) {
+ return diff;
+ }
+ break;
+ }
+ if(!sCharIsDigit) {
+ return -1;
+ }
+ if(!tCharIsDigit) {
+ return 1;
+ }
+ } while(true);
+ }
+ else {
+ // Compare words
+ if(collator != null) {
+ // To use the collator the whole subwords have to be compared - character-by-character comparision
+ // is not possible. So find the two subwords first
+ int aw = sIndex;
+ int bw = tIndex;
+ do {
+ ++sIndex;
+ } while(sIndex < sLength && !Character.isDigit(s.charAt(sIndex)));
+ do {
+ ++tIndex;
+ } while(tIndex < tLength && !Character.isDigit(t.charAt(tIndex)));
+
+ String as = s.substring(aw, sIndex);
+ String bs = t.substring(bw, tIndex);
+ int subwordResult = collator.compare(as, bs);
+ if(subwordResult != 0) {
+ return subwordResult;
+ }
+ }
+ else {
+ // No collator specified. All characters should be ascii only. Compare character-by-character.
+ do {
+ if(sChar != tChar) {
+ if(caseSensitive) {
+ return sChar - tChar;
+ }
+ sChar = Character.toUpperCase(sChar);
+ tChar = Character.toUpperCase(tChar);
+ if(sChar != tChar) {
+ sChar = Character.toLowerCase(sChar);
+ tChar = Character.toLowerCase(tChar);
+ if(sChar != tChar) {
+ return sChar - tChar;
+ }
+ }
+ }
+ ++sIndex;
+ ++tIndex;
+ if(sIndex == sLength && tIndex == tLength) {
+ return 0;
+ }
+ if(sIndex == sLength) {
+ return -1;
+ }
+ if(tIndex == tLength) {
+ return 1;
+ }
+ sChar = s.charAt(sIndex);
+ tChar = t.charAt(tIndex);
+ sCharIsDigit = Character.isDigit(sChar);
+ tCharIsDigit = Character.isDigit(tChar);
+ } while(!sCharIsDigit && !tCharIsDigit);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/jenkinsci/plugins/repoclient/client/Version.java b/src/main/java/org/jenkinsci/plugins/repoclient/client/Version.java
index 7f4243d..f636e92 100644
--- a/src/main/java/org/jenkinsci/plugins/repoclient/client/Version.java
+++ b/src/main/java/org/jenkinsci/plugins/repoclient/client/Version.java
@@ -1,7 +1,8 @@
package org.jenkinsci.plugins.repoclient.client;
+import com.eekboom.utils.Strings;
+
import java.io.Serializable;
-import java.text.Collator;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
@@ -102,9 +103,11 @@ public Version(String ver) {
int idx = -1;
char[] chars = ver.toCharArray();
int j = 0;
+ char delimiter = '.';
for (char c : chars) {
if ((c < '0' || '9' < c)) {
idx = j;
+ delimiter = c;
break;
}
j++;
@@ -115,7 +118,7 @@ public Version(String ver) {
String fragment = ver.substring(0, idx);
try {
Integer.valueOf(fragment);
- if (idx < ver.length()) {
+ if (idx < ver.length() && delimiter == '.') {
String check = stripVersion(ver.substring(idx + 1));
if (check != null) {
version = fragment + "." + check;
@@ -269,9 +272,8 @@ public int compareTo(Version ver) {
}
}
if (result == 0) {
- result = Collator.getInstance().compare(qualifier,
- ver.getQualifier());
- }
+ result = Strings.compareNaturalAscii(qualifier, ver.getQualifier());
+ }
return result;
}
}
diff --git a/src/test/java/com/eekboom/utils/StringsTest.java b/src/test/java/com/eekboom/utils/StringsTest.java
new file mode 100644
index 0000000..3dd41a7
--- /dev/null
+++ b/src/test/java/com/eekboom/utils/StringsTest.java
@@ -0,0 +1,174 @@
+package com.eekboom.utils;
+import junit.framework.TestCase;
+
+import java.text.Collator;
+import java.util.*;
+
+public class StringsTest extends TestCase {
+ public void testCompareNatural() {
+ assertEquals(0, c("", ""));
+ assertEquals(1, c("1-02", "1-2"));
+ assertEquals(-1, c("1-2", "1-02"));
+ assertEquals(-1, c("catch 22", "catch 022"));
+ assertEquals(0, c("a", "a"));
+ assertEquals(-1, c("2a", "2a2"));
+ assertEquals(1, c("b", "a"));
+ assertEquals(-1, c("a", "b"));
+ assertEquals(-1, c("002", "11"));
+ assertEquals(-1, c("2", "11"));
+ assertEquals(1, c("22", "11"));
+ assertEquals(1, c("222", "99"));
+ assertEquals(-1, c("a 2", "a 11"));
+ assertEquals(-1, c("c23", "c111"));
+ assertEquals(-1, c("a2", "aa2"));
+ assertEquals(1, c("a 22", "a 2"));
+ assertEquals(1, c("a", "A"));
+ assertEquals(-1, c("a 2 h", "a 2 h 2"));
+ assertEquals(-1, c("abcd 234 huj", "abcd 234 huj 2"));
+ assertEquals(0, c("abcd 234 huj", "abcd 234 huj"));
+ assertEquals(1, c("abcd 234 huj 33", "abcd 234 huj 9"));
+ assertEquals(-1, c("1.9.2-r9abc", "1.10.1-r9abc"));
+ assertEquals(1, c("1.9.2-r10abc", "1.9.2-r9abc"));
+ }
+
+ public void testNaturalCompareWhitespace() {
+ String[] strings = { "p4", "p 3" };
+ List