added VersionComparator
This commit is contained in:
@@ -1,17 +1,6 @@
|
||||
package net.woggioni.jwo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BinaryOperator;
|
||||
import java.util.function.Function;
|
||||
@@ -132,10 +121,58 @@ public class CollectionUtils {
|
||||
return toUnmodifiableMap(HashMap::new, keyExtractor, valueExtractor);
|
||||
}
|
||||
|
||||
public static <T, K, V> Collector<T, ?, Map<K, V>> toUnmodifiableTreeMap(
|
||||
public static <T, K, V> Collector<T, ?, NavigableMap<K, V>> toUnmodifiableTreeMap(
|
||||
Function<T, K> keyExtractor,
|
||||
Function<T, V> valueExtractor) {
|
||||
return toUnmodifiableMap(TreeMap::new, keyExtractor, valueExtractor);
|
||||
return toUnmodifiableNavigableMap(TreeMap::new, keyExtractor, valueExtractor);
|
||||
}
|
||||
|
||||
public static <T, K, V> Collector<T, ?, NavigableMap<K, V>> toUnmodifiableTreeMap(
|
||||
Function<T, K> keyExtractor,
|
||||
Function<T, V> valueExtractor,
|
||||
Comparator<K> comparator) {
|
||||
return toUnmodifiableNavigableMap(() -> new TreeMap<>(comparator), keyExtractor, valueExtractor);
|
||||
}
|
||||
|
||||
public static <T, K, V> Collector<T, ?, NavigableMap<K, V>> toTreeMap(
|
||||
Function<T, K> keyExtractor,
|
||||
Function<T, V> valueExtractor) {
|
||||
return toNavigableMap(TreeMap::new, keyExtractor, valueExtractor);
|
||||
}
|
||||
|
||||
public static <T, K, V> Collector<T, ?, NavigableMap<K, V>> toTreeMap(
|
||||
Function<T, K> keyExtractor,
|
||||
Function<T, V> valueExtractor,
|
||||
Comparator<K> comparator) {
|
||||
return toNavigableMap(() -> new TreeMap<>(comparator), keyExtractor, valueExtractor);
|
||||
}
|
||||
|
||||
public static <T, K, V> Collector<T, ?, NavigableMap<K, V>> toNavigableMap(
|
||||
Supplier<NavigableMap<K, V>> constructor,
|
||||
Function<T, K> keyExtractor,
|
||||
Function<T, V> valueExtractor) {
|
||||
BiConsumer<NavigableMap<K, V>, T> accumulator = (map, streamElement) -> {
|
||||
map.merge(keyExtractor.apply(streamElement), valueExtractor.apply(streamElement), throwingMerger());
|
||||
};
|
||||
return Collector.of(
|
||||
constructor,
|
||||
accumulator,
|
||||
mapMerger(throwingMerger())
|
||||
);
|
||||
}
|
||||
|
||||
public static <T, K, V> Collector<T, ?, Map<K, V>> toMap(
|
||||
Supplier<Map<K, V>> constructor,
|
||||
Function<T, K> keyExtractor,
|
||||
Function<T, V> valueExtractor) {
|
||||
BiConsumer<Map<K, V>, T> accumulator = (map, streamElement) -> {
|
||||
map.merge(keyExtractor.apply(streamElement), valueExtractor.apply(streamElement), throwingMerger());
|
||||
};
|
||||
return Collector.of(
|
||||
constructor,
|
||||
accumulator,
|
||||
mapMerger(throwingMerger())
|
||||
);
|
||||
}
|
||||
|
||||
public static <T, K, V> Collector<T, ?, Map<K, V>> toUnmodifiableMap(
|
||||
@@ -152,4 +189,19 @@ public class CollectionUtils {
|
||||
Collections::unmodifiableMap
|
||||
);
|
||||
}
|
||||
|
||||
public static <T, K, V> Collector<T, ?, NavigableMap<K, V>> toUnmodifiableNavigableMap(
|
||||
Supplier<NavigableMap<K, V>> constructor,
|
||||
Function<T, K> keyExtractor,
|
||||
Function<T, V> valueExtractor) {
|
||||
BiConsumer<NavigableMap<K, V>, T> accumulator = (map, streamElement) -> {
|
||||
map.merge(keyExtractor.apply(streamElement), valueExtractor.apply(streamElement), throwingMerger());
|
||||
};
|
||||
return Collector.of(
|
||||
constructor,
|
||||
accumulator,
|
||||
mapMerger(throwingMerger()),
|
||||
Collections::unmodifiableNavigableMap
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -10,8 +10,10 @@ import java.lang.reflect.Constructor;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
@@ -20,6 +22,7 @@ import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
@Slf4j
|
||||
@@ -434,7 +437,7 @@ public class JWO {
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public void writeZipEntry(
|
||||
public static void writeZipEntry(
|
||||
ZipOutputStream zip,
|
||||
Supplier<InputStream> source,
|
||||
String destinationFileName,
|
||||
@@ -462,7 +465,7 @@ public class JWO {
|
||||
zip.closeEntry();
|
||||
}
|
||||
|
||||
public void writeZipEntry(
|
||||
public static void writeZipEntry(
|
||||
ZipOutputStream zip,
|
||||
Supplier<InputStream> source,
|
||||
String destinationFileName,
|
||||
@@ -470,10 +473,71 @@ public class JWO {
|
||||
writeZipEntry(zip, source, destinationFileName, compressionMethod, new byte[0x10000]);
|
||||
}
|
||||
|
||||
public void writeZipEntry(
|
||||
public static void writeZipEntry(
|
||||
ZipOutputStream zip,
|
||||
Supplier<InputStream> source,
|
||||
String destinationFileName) {
|
||||
writeZipEntry(zip, source, destinationFileName, ZipEntry.DEFLATED);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static void extractZip(Path sourceArchive, Path destinationFolder) {
|
||||
byte[] buffer = new byte[0x10000];
|
||||
expandZip(sourceArchive, new BiConsumer<ZipInputStream, ZipEntry>() {
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void accept(ZipInputStream zipInputStream, ZipEntry zipEntry) {
|
||||
Path newFile = destinationFolder.resolve(zipEntry.getName());
|
||||
Files.createDirectories(newFile.getParent());
|
||||
try(OutputStream outputStream = Files.newOutputStream(newFile)) {
|
||||
while (true) {
|
||||
int read = zipInputStream.read(buffer);
|
||||
if (read < 0) break;
|
||||
outputStream.write(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static void expandZip(Path sourceArchive, BiConsumer<ZipInputStream, ZipEntry> consumer) {
|
||||
try(ZipInputStream zis = new ZipInputStream(Files.newInputStream(sourceArchive))) {
|
||||
ZipEntry zipEntry = zis.getNextEntry();
|
||||
while (zipEntry != null) {
|
||||
consumer.accept(zis, zipEntry);
|
||||
zipEntry = zis.getNextEntry();
|
||||
}
|
||||
zis.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static void installResource(String resourceName, Path destination, Class<?> cls) {
|
||||
Path outputFile;
|
||||
if (Files.isSymbolicLink(destination)) {
|
||||
destination = destination.toRealPath();
|
||||
}
|
||||
if(!Files.exists(destination)) {
|
||||
Files.createDirectories(destination.getParent());
|
||||
outputFile = destination;
|
||||
} else if(Files.isDirectory(destination)) {
|
||||
outputFile = destination.resolve(resourceName.substring(1 + resourceName.lastIndexOf('/')));
|
||||
} else if(Files.isRegularFile(destination)) {
|
||||
outputFile = destination;
|
||||
} else {
|
||||
throw newThrowable(IllegalStateException.class,
|
||||
"Path '%s' is neither a file nor a directory",
|
||||
destination
|
||||
);
|
||||
}
|
||||
InputStream is = cls.getResourceAsStream(resourceName);
|
||||
if(is == null) is = cls.getClassLoader().getResourceAsStream(resourceName);
|
||||
if(is == null) throw new FileNotFoundException(resourceName);
|
||||
try {
|
||||
Files.copy(is, outputFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
} finally {
|
||||
is.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
289
src/main/java/net/woggioni/jwo/VersionComparator.java
Normal file
289
src/main/java/net/woggioni/jwo/VersionComparator.java
Normal file
@@ -0,0 +1,289 @@
|
||||
package net.woggioni.jwo;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
|
||||
public class VersionComparator implements Comparator<String> {
|
||||
|
||||
private static int indexOf(char[] haystack, char needle, int start) {
|
||||
int result = -1;
|
||||
for (int i = start; i < haystack.length; ++i) {
|
||||
if (haystack[i] == needle) {
|
||||
result = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int indexOf(char[] haystack, char needle) {
|
||||
return indexOf(haystack, needle, 0);
|
||||
}
|
||||
|
||||
private static char[] cstring(String s) {
|
||||
char[] result = new char[s.length() + 1];
|
||||
for(int i=0; i<s.length(); i++) {
|
||||
result[i] = s.charAt(i);
|
||||
}
|
||||
result[s.length()] = '\0';
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean cstringEquals(char[] cstring1, int begin1, char[] cstring2, int begin2) {
|
||||
int i=0;
|
||||
while(i < cstring1.length + begin1 || i < cstring2.length + begin2) {
|
||||
char c1 = cstring1[begin1 + i];
|
||||
char c2 = cstring2[begin2 + i];
|
||||
if(c1 != c2) return false;
|
||||
else if(c1 == '\0') break;
|
||||
i++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int strlen(char[] cstring, int begin) {
|
||||
int i = begin;
|
||||
while(i < cstring.length) {
|
||||
if(cstring[i] == '\0') break;
|
||||
++i;
|
||||
}
|
||||
return i - begin;
|
||||
}
|
||||
|
||||
private static int strlen(char[] cstring) {
|
||||
return strlen(cstring, 0);
|
||||
}
|
||||
|
||||
private static int strcmp(char[] cstring1, int begin1, char[] cstring2, int begin2) {
|
||||
int i = 0;
|
||||
int lim = Math.min(strlen(cstring1, begin1), strlen(cstring2, begin2));
|
||||
while(i < lim) {
|
||||
char c1 = cstring1[begin1 + i];
|
||||
char c2 = cstring2[begin2 + i];
|
||||
if(c1 < c2) {
|
||||
return -1;
|
||||
} else if(c1 > c2) {
|
||||
return 1;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
return Integer.compare(cstring1.length, cstring2.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split EVR into epoch, version, and release components.
|
||||
* @param text [epoch:]version[-release] string
|
||||
* @param evr array that will contain starting indexes of epoch, version, and release
|
||||
*/
|
||||
private static void parseEVR(char[] text, int[] evr) {
|
||||
int epoch;
|
||||
int version;
|
||||
int release;
|
||||
|
||||
int s = 0;
|
||||
/* s points to epoch terminator */
|
||||
while (s < text.length && Character.isDigit(text[s])) s++;
|
||||
/* se points to version terminator */
|
||||
int se = indexOf(text, '-', s);
|
||||
|
||||
if(text[s] == ':') {
|
||||
epoch = 0;
|
||||
text[s++] = '\0';
|
||||
version = s;
|
||||
if(text[epoch] == '\0') {
|
||||
epoch = -1;
|
||||
}
|
||||
} else {
|
||||
/* different from RPM- always assume 0 epoch */
|
||||
epoch = -1;
|
||||
version = 0;
|
||||
}
|
||||
if(se != -1) {
|
||||
text[se++] = '\0';
|
||||
release = se;
|
||||
} else {
|
||||
release = -1;
|
||||
}
|
||||
evr[0] = epoch;
|
||||
evr[1] = version;
|
||||
evr[2] = release;
|
||||
}
|
||||
|
||||
|
||||
private static int rpmvercmp(char[] chars1, int start1, char[] chars2, int start2) {
|
||||
// easy comparison to see if versions are identical
|
||||
if(strcmp(chars1, start1, chars2, start2) == 0) return 0;
|
||||
char[] str1 = Arrays.copyOfRange(chars1, start1, start1 + strlen(chars1, start1) + 1);
|
||||
char[] str2 = Arrays.copyOfRange(chars2, start2, start2 + strlen(chars2, start2) + 1);
|
||||
int one = 0, two = 0;
|
||||
int ptr1 = 0, ptr2 = 0;
|
||||
boolean isNum;
|
||||
char oldch1, oldch2;
|
||||
|
||||
// loop through each version segment of str1 and str2 and compare them
|
||||
while(str1[one] != '\0' && str2[two] != '\0') {
|
||||
char c1;
|
||||
while(true) {
|
||||
c1 = str1[one];
|
||||
if(c1 == '\0' || Character.isAlphabetic(c1) || Character.isDigit(c1)) break;
|
||||
one++;
|
||||
}
|
||||
|
||||
char c2;
|
||||
while(true) {
|
||||
c2 = str2[two];
|
||||
if(c2 == '\0' || Character.isAlphabetic(c2) || Character.isDigit(c2)) break;
|
||||
two++;
|
||||
}
|
||||
|
||||
// If we ran to the end of either, we are finished with the loop
|
||||
if (c1 == '\0' || c2 == '\0') break;
|
||||
|
||||
// If the separator lengths were different, we are also finished
|
||||
if ((one - ptr1) != (two - ptr2)) {
|
||||
return (one - ptr1) < (two - ptr2) ? -1 : 1;
|
||||
}
|
||||
|
||||
ptr1 = one;
|
||||
ptr2 = two;
|
||||
|
||||
// grab first completely alpha or completely numeric segment
|
||||
// leave one and two pointing to the start of the alpha or numeric
|
||||
// segment and walk ptr1 and ptr2 to end of segment
|
||||
if (Character.isDigit(str1[ptr1])) {
|
||||
while(true) {
|
||||
c1 = str1[ptr1];
|
||||
if(c1 == '\0' || !Character.isDigit(c1)) break;
|
||||
ptr1++;
|
||||
}
|
||||
while(true) {
|
||||
c2 = str2[ptr2];
|
||||
if(c2 == '\0' || !Character.isDigit(c2)) break;
|
||||
ptr2++;
|
||||
}
|
||||
isNum = true;
|
||||
} else {
|
||||
while(true) {
|
||||
c1 = str1[ptr1];
|
||||
if(c1 == '\0' || !Character.isAlphabetic(c1)) break;
|
||||
ptr1++;
|
||||
}
|
||||
while(true) {
|
||||
c2 = str2[ptr2];
|
||||
if(c2 == '\0' || !Character.isAlphabetic(c2)) break;
|
||||
ptr2++;
|
||||
}
|
||||
isNum = false;
|
||||
}
|
||||
|
||||
// save character at the end of the alpha or numeric segment
|
||||
// so that they can be restored after the comparison
|
||||
oldch1 = str1[ptr1];
|
||||
str1[ptr1] = '\0';
|
||||
oldch2 = str2[ptr2];
|
||||
str2[ptr2] = '\0';
|
||||
|
||||
// this cannot happen, as we previously tested to make sure that
|
||||
// the first string has a non-null segment
|
||||
if (one == ptr1) {
|
||||
return -1; // arbitrary
|
||||
}
|
||||
|
||||
// take care of the case where the two version segments are
|
||||
// different types: one numeric, the other alpha (i.e. empty)
|
||||
// numeric segments are always newer than alpha segments
|
||||
// XXX See patch #60884 (and details) from bugzilla #50977.
|
||||
if (two == ptr2) {
|
||||
return isNum ? 1 : -1;
|
||||
}
|
||||
if (isNum) {
|
||||
/* this used to be done by converting the digit segments */
|
||||
/* to ints using atoi() - it's changed because long */
|
||||
/* digit segments can overflow an int - this should fix that. */
|
||||
|
||||
/* throw away any leading zeros - it's a number, right? */
|
||||
while (str1[one] == '0') one++;
|
||||
while (str2[two] == '0') two++;
|
||||
|
||||
/* whichever number has more digits wins */
|
||||
int len1 = strlen(str1, one);
|
||||
int len2 = strlen(str2, two);
|
||||
if (len1 > len2) {
|
||||
return 1;
|
||||
} else if (len2 > len1) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
// strcmp will return which one is greater - even if the two
|
||||
// segments are alpha or if they are numeric. don't return
|
||||
// if they are equal because there might be more segments to
|
||||
// compare
|
||||
int rc = strcmp(str1, one, str2, two);
|
||||
if (rc != 0) return rc;
|
||||
|
||||
// restore character that was replaced by null above
|
||||
str1[ptr1] = oldch1;
|
||||
one = ptr1;
|
||||
str2[ptr2] = oldch2;
|
||||
two = ptr2;
|
||||
}
|
||||
|
||||
// this catches the case where all numeric and alpha segments have
|
||||
// compared identically but the segment separating characters were
|
||||
// different
|
||||
if (str1[one] == '\0' && str2[two] == '\0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* the final showdown. we never want a remaining alpha string to
|
||||
* beat an empty string. the logic is a bit weird, but:
|
||||
* - if one is empty and two is not an alpha, two is newer.
|
||||
* - if one is an alpha, two is newer.
|
||||
* - otherwise one is newer.
|
||||
*/
|
||||
if ((str1[one] == '\0' && !Character.isAlphabetic(str2[two]))
|
||||
|| Character.isAlphabetic(str1[one])) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static char[] defaultEpoch = new char[] {'0'};
|
||||
|
||||
public static int cmp(String v1, String v2) {
|
||||
if(v1 == null && v2 == null) return 0;
|
||||
else if(v1 == null) return -1;
|
||||
else if(v2 == null) return 1;
|
||||
else if(Objects.equals(v1, v2)) return 0;
|
||||
|
||||
char[] chars1 = cstring(v1);
|
||||
char[] chars2 = cstring(v2);
|
||||
int[] evr = new int[3];
|
||||
parseEVR(chars1, evr);
|
||||
int epoch1 = evr[0];
|
||||
int version1 = evr[1];
|
||||
int release1 = evr[2];
|
||||
parseEVR(chars2, evr);
|
||||
int epoch2 = evr[0];
|
||||
int version2 = evr[1];
|
||||
int release2 = evr[2];
|
||||
|
||||
char[] seq1 = epoch1 == -1 ? defaultEpoch : chars1;
|
||||
char[] seq2 = epoch2 == -1 ? defaultEpoch : chars2;
|
||||
int ret = rpmvercmp(seq1, epoch1 == -1 ? 0 : epoch1, seq2, epoch2 == -1 ? 0 : epoch2);
|
||||
if(ret == 0) {
|
||||
ret = rpmvercmp(chars1, version1, chars2, version2);
|
||||
if(ret == 0 && release1 != -1 && release2 != -1) {
|
||||
ret = rpmvercmp(chars1, release1, chars2, release2);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(String v1, String v2) {
|
||||
return cmp(v1, v2);
|
||||
}
|
||||
}
|
40
src/test/java/net/woggioni/jwo/VersionComparatorTest.java
Normal file
40
src/test/java/net/woggioni/jwo/VersionComparatorTest.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package net.woggioni.jwo;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.ArgumentsProvider;
|
||||
import org.junit.jupiter.params.provider.ArgumentsSource;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class VersionComparatorTest {
|
||||
|
||||
private static class TestCaseProvider implements ArgumentsProvider {
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
|
||||
return Stream.of(
|
||||
Arguments.of("", "", 0),
|
||||
Arguments.of("asdfg-2019", "asdfg-2019", 0),
|
||||
Arguments.of("1.2", "1", 1),
|
||||
Arguments.of("1.2", "1.0", 1),
|
||||
Arguments.of("1.2", "1.10", -1),
|
||||
Arguments.of("5.9.12.arch1-1", "5.8.0.arch1-1", 1),
|
||||
Arguments.of("5.9.12.arch1-1", "5.10.0.arch1-1", -1),
|
||||
Arguments.of("5.10.0.arch1-1", "5.10.0.arch1-3", -1),
|
||||
Arguments.of("5.10.0.arch1-10", "5.10.0.arch1-3", 1),
|
||||
Arguments.of("5.9.0.arch1-10", "5.10.0.arch1-3", -1),
|
||||
Arguments.of("20191220.6871bff-1", "20201120.bc9cd0b-1", -1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest(name="version1: \"{0}\", version2: \"{1}\", expected outcome: {2}")
|
||||
@ArgumentsSource(TestCaseProvider.class)
|
||||
public void test(String version1, String version2, int expectedOutcome) {
|
||||
Assertions.assertEquals(expectedOutcome, VersionComparator.cmp(version1, version2));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user