added VersionComparator

This commit is contained in:
2020-12-21 07:01:12 +00:00
parent 45489e8ed4
commit 2cdc92effb
4 changed files with 462 additions and 17 deletions

View File

@@ -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
);
}
}

View File

@@ -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();
}
}
}

View 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);
}
}

View 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));
}
}