From 2cdc92effbf9a2711a1698d757630bc81c472efc Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Mon, 21 Dec 2020 07:01:12 +0000 Subject: [PATCH] added VersionComparator --- .../net/woggioni/jwo/CollectionUtils.java | 80 ++++- src/main/java/net/woggioni/jwo/JWO.java | 70 ++++- .../net/woggioni/jwo/VersionComparator.java | 289 ++++++++++++++++++ .../woggioni/jwo/VersionComparatorTest.java | 40 +++ 4 files changed, 462 insertions(+), 17 deletions(-) create mode 100644 src/main/java/net/woggioni/jwo/VersionComparator.java create mode 100644 src/test/java/net/woggioni/jwo/VersionComparatorTest.java diff --git a/src/main/java/net/woggioni/jwo/CollectionUtils.java b/src/main/java/net/woggioni/jwo/CollectionUtils.java index 7eee038..57c45a4 100644 --- a/src/main/java/net/woggioni/jwo/CollectionUtils.java +++ b/src/main/java/net/woggioni/jwo/CollectionUtils.java @@ -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 Collector> toUnmodifiableTreeMap( + public static Collector> toUnmodifiableTreeMap( Function keyExtractor, Function valueExtractor) { - return toUnmodifiableMap(TreeMap::new, keyExtractor, valueExtractor); + return toUnmodifiableNavigableMap(TreeMap::new, keyExtractor, valueExtractor); + } + + public static Collector> toUnmodifiableTreeMap( + Function keyExtractor, + Function valueExtractor, + Comparator comparator) { + return toUnmodifiableNavigableMap(() -> new TreeMap<>(comparator), keyExtractor, valueExtractor); + } + + public static Collector> toTreeMap( + Function keyExtractor, + Function valueExtractor) { + return toNavigableMap(TreeMap::new, keyExtractor, valueExtractor); + } + + public static Collector> toTreeMap( + Function keyExtractor, + Function valueExtractor, + Comparator comparator) { + return toNavigableMap(() -> new TreeMap<>(comparator), keyExtractor, valueExtractor); + } + + public static Collector> toNavigableMap( + Supplier> constructor, + Function keyExtractor, + Function valueExtractor) { + BiConsumer, T> accumulator = (map, streamElement) -> { + map.merge(keyExtractor.apply(streamElement), valueExtractor.apply(streamElement), throwingMerger()); + }; + return Collector.of( + constructor, + accumulator, + mapMerger(throwingMerger()) + ); + } + + public static Collector> toMap( + Supplier> constructor, + Function keyExtractor, + Function valueExtractor) { + BiConsumer, T> accumulator = (map, streamElement) -> { + map.merge(keyExtractor.apply(streamElement), valueExtractor.apply(streamElement), throwingMerger()); + }; + return Collector.of( + constructor, + accumulator, + mapMerger(throwingMerger()) + ); } public static Collector> toUnmodifiableMap( @@ -152,4 +189,19 @@ public class CollectionUtils { Collections::unmodifiableMap ); } + + public static Collector> toUnmodifiableNavigableMap( + Supplier> constructor, + Function keyExtractor, + Function valueExtractor) { + BiConsumer, T> accumulator = (map, streamElement) -> { + map.merge(keyExtractor.apply(streamElement), valueExtractor.apply(streamElement), throwingMerger()); + }; + return Collector.of( + constructor, + accumulator, + mapMerger(throwingMerger()), + Collections::unmodifiableNavigableMap + ); + } } diff --git a/src/main/java/net/woggioni/jwo/JWO.java b/src/main/java/net/woggioni/jwo/JWO.java index 84dfdea..1560a15 100644 --- a/src/main/java/net/woggioni/jwo/JWO.java +++ b/src/main/java/net/woggioni/jwo/JWO.java @@ -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 source, String destinationFileName, @@ -462,7 +465,7 @@ public class JWO { zip.closeEntry(); } - public void writeZipEntry( + public static void writeZipEntry( ZipOutputStream zip, Supplier 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 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() { + @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 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(); + } + } } diff --git a/src/main/java/net/woggioni/jwo/VersionComparator.java b/src/main/java/net/woggioni/jwo/VersionComparator.java new file mode 100644 index 0000000..c764012 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/VersionComparator.java @@ -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 { + + 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 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); + } +} diff --git a/src/test/java/net/woggioni/jwo/VersionComparatorTest.java b/src/test/java/net/woggioni/jwo/VersionComparatorTest.java new file mode 100644 index 0000000..f26f01f --- /dev/null +++ b/src/test/java/net/woggioni/jwo/VersionComparatorTest.java @@ -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 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)); + } +}