diff --git a/build.gradle b/build.gradle index de90546..583a822 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ plugins { id 'java-library' id 'maven-publish' + id 'jacoco' alias(catalog.plugins.multi.release.jar) alias(catalog.plugins.lombok) alias(catalog.plugins.sambal) @@ -20,7 +21,6 @@ allprojects { pluginManager.withPlugin('java-library') { java { - toolchain { languageVersion = JavaLanguageVersion.of(17) vendor = JvmVendorSpec.GRAAL_VM @@ -35,6 +35,21 @@ allprojects { test { useJUnitPlatform() + + systemProperties([ + 'junit.jupiter.execution.parallel.enabled' : true, + 'junit.jupiter.execution.parallel.mode.default' : 'concurrent', + 'junit.jupiter.execution.parallel.mode.classes.default' : 'concurrent' + ]) + } + + pluginManager.withPlugin('jacoco') { + test { + finalizedBy jacocoTestReport + } + jacocoTestReport { + dependsOn test + } } } @@ -75,11 +90,18 @@ ext { configurations { pathClassloaderTest + zipTestBundle { + transitive = false + canBeConsumed = false + visible = false + canBeResolved = true + } } dependencies { implementation catalog.slf4j.api pathClassloaderTest group: 'com.google.inject', name: 'guice', version: getProperty('guice.version') + zipTestBundle group: 'com.google.inject', name: 'guice', version: getProperty('guice.version') } compileJava { @@ -104,7 +126,8 @@ test { }.first() systemProperties([ 'junit.jupiter.engine.jar' : junitJupiterEngineJar.toString(), - 'path.classloader.test.bundle' : pathClassLoaderTestBundleTask.get().outputs.files.singleFile + 'path.classloader.test.bundle' : pathClassLoaderTestBundleTask.get().outputs.files.singleFile, + 'zip.test.bundle' : configurations.zipTestBundle.singleFile ]) jvmArgs(['--add-opens', 'java.base/sun.nio.fs=ALL-UNNAMED']) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c1962a7..7f93135 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c30b486..3fa8f86 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index aeb74cb..0adc8e1 100755 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -130,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. diff --git a/jmath/build.gradle b/jmath/build.gradle index 6c8317f..28beb22 100644 --- a/jmath/build.gradle +++ b/jmath/build.gradle @@ -1,5 +1,6 @@ plugins { id 'java-library' + id 'jacoco' alias(catalog.plugins.lombok) id 'maven-publish' } diff --git a/src/main/java/net/woggioni/jwo/Application.java b/src/main/java/net/woggioni/jwo/Application.java index 5d7f58a..c0ef7f3 100644 --- a/src/main/java/net/woggioni/jwo/Application.java +++ b/src/main/java/net/woggioni/jwo/Application.java @@ -35,39 +35,6 @@ public class Application { return new Builder().name(name); } - private static boolean validateConfigurationDirectory(Path candidate) { - try { - if (!Files.exists(candidate)) { - Files.createDirectories(candidate); - return true; - } else if (!Files.isDirectory(candidate)) { - log.debug("Configuration directory '{}' discarded because it is not a directory", candidate); - return false; - } else if (!Files.isWritable(candidate)) { - log.debug("Configuration directory '{}' discarded because it is not writable", candidate); - return false; - } else { - log.debug("Using configuration directory '{}'", candidate); - return true; - } - } catch (Exception ioe) { - log.debug( - String.format("configuration directory '%s' discarded: %s", candidate.toString(), ioe.getMessage()), - ioe - ); - return false; - } - } - - @SneakyThrows - private static Path selectCandidate(Stream candidates, String successMessage, String errorMessage) { - return candidates - .filter(Application::validateConfigurationDirectory) - .peek(p -> log.debug(successMessage, p)) - .findFirst() - .orElseThrow((Sup) () -> new FileNotFoundException(errorMessage)); - } - private static boolean validateWritableDirectory(Path candidate) { try { if (!Files.exists(candidate)) { diff --git a/src/main/java/net/woggioni/jwo/CollectionUtils.java b/src/main/java/net/woggioni/jwo/CollectionUtils.java index 9ab8ec1..b759c4a 100644 --- a/src/main/java/net/woggioni/jwo/CollectionUtils.java +++ b/src/main/java/net/woggioni/jwo/CollectionUtils.java @@ -9,6 +9,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.ListIterator; import java.util.Map; import java.util.NavigableMap; import java.util.NavigableSet; @@ -124,10 +125,16 @@ public class CollectionUtils { }; } - private static BinaryOperator throwingMerger() { - return (v1, v2) -> { - throw new IllegalStateException(String.format("Duplicate key %s", v1)); - }; + public static T throwingMerger(T v1, T v2) { + throw new IllegalStateException(String.format("Duplicate key %s", v1)); + } + + public static T oldValueMerger(T oldValue, T newValue) { + return oldValue; + } + + public static T newValueMerger(T oldValue, T newValue) { + return newValue; } public static Collector> toUnmodifiableHashMap( @@ -162,62 +169,109 @@ public class CollectionUtils { return toNavigableMap(() -> new TreeMap<>(comparator), keyExtractor, valueExtractor); } + public static > Collector toNavigableMap( + Supplier constructor, + Function keyExtractor, + Function valueExtractor) { + return toNavigableMap( + constructor, + keyExtractor, + valueExtractor, + CollectionUtils::throwingMerger + ); + } + public static > Collector toNavigableMap( Supplier constructor, Function keyExtractor, - Function valueExtractor) { + Function valueExtractor, + BinaryOperator valueMerger) { BiConsumer accumulator = (map, streamElement) -> { - map.merge(keyExtractor.apply(streamElement), valueExtractor.apply(streamElement), throwingMerger()); + map.merge(keyExtractor.apply(streamElement), valueExtractor.apply(streamElement), + valueMerger + ); }; return Collector.of( constructor, accumulator, - mapMerger(throwingMerger()) + mapMerger(valueMerger) ); } + public static Collector> toMap( + Supplier> constructor, + Function keyExtractor, + Function valueExtractor) { + return toMap(constructor, keyExtractor, valueExtractor, CollectionUtils::throwingMerger); + } + public static Collector> toMap( Supplier> constructor, Function keyExtractor, - Function valueExtractor) { + Function valueExtractor, + BinaryOperator valueMerger) { BiConsumer, T> accumulator = (map, streamElement) -> { - map.merge(keyExtractor.apply(streamElement), valueExtractor.apply(streamElement), throwingMerger()); + map.merge(keyExtractor.apply(streamElement), valueExtractor.apply(streamElement), + valueMerger + ); }; return Collector.of( constructor, accumulator, - mapMerger(throwingMerger()) + mapMerger(valueMerger) ); } + public static Collector> toUnmodifiableMap( + Supplier> constructor, + Function keyExtractor, + Function valueExtractor) { + return toUnmodifiableMap(constructor, keyExtractor, valueExtractor, CollectionUtils::throwingMerger); + } public static Collector> toUnmodifiableMap( Supplier> constructor, Function keyExtractor, - Function valueExtractor) { + Function valueExtractor, + BinaryOperator valueMerger) { BiConsumer, T> accumulator = (map, streamElement) -> { - map.merge(keyExtractor.apply(streamElement), valueExtractor.apply(streamElement), throwingMerger()); + map.merge(keyExtractor.apply(streamElement), + valueExtractor.apply(streamElement), + valueMerger + ); }; return Collector.of( constructor, accumulator, - mapMerger(throwingMerger()), + mapMerger(valueMerger), Collections::unmodifiableMap ); } + public static Collector> toUnmodifiableNavigableMap( + Supplier> constructor, + Function keyExtractor, + Function valueExtractor, + BinaryOperator valueMerger + ) { + BiConsumer, T> accumulator = (map, streamElement) -> { + map.merge( + keyExtractor.apply(streamElement), + valueExtractor.apply(streamElement), + valueMerger + ); + }; + return Collector.of( + constructor, + accumulator, + mapMerger(valueMerger), + Collections::unmodifiableNavigableMap + ); + } 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 - ); + return toUnmodifiableNavigableMap(constructor, keyExtractor, valueExtractor, CollectionUtils::throwingMerger); } public static Stream> mapValues(Map map, Fun xform) { return map @@ -225,4 +279,26 @@ public class CollectionUtils { .stream() .map(entry -> new AbstractMap.SimpleEntry<>(entry.getKey(), xform.apply(entry.getValue()))); } + + public static Iterator reverseIterator(List list) { + return new Iterator() { + private final ListIterator it = list.listIterator(list.size()); + @Override + public boolean hasNext() { + return it.hasPrevious(); + } + + @Override + public T next() { + return it.previous(); + } + }; + } + + public static Iterator reverseIterator(NavigableSet set) { + return set.descendingIterator(); + } + public static Iterator> reverseIterator(NavigableMap map) { + return map.descendingMap().entrySet().iterator(); + } } diff --git a/src/main/java/net/woggioni/jwo/DelegatingMap.java b/src/main/java/net/woggioni/jwo/DelegatingMap.java index d0203f4..b32db3e 100644 --- a/src/main/java/net/woggioni/jwo/DelegatingMap.java +++ b/src/main/java/net/woggioni/jwo/DelegatingMap.java @@ -1,20 +1,17 @@ package net.woggioni.jwo; -import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Supplier; -public class DelegatingMap implements Map { - private final Supplier> mapFactory; - private final List> delegates; - private final Map thisMap; +import static net.woggioni.jwo.JWO.streamCat; +public class DelegatingMap extends UnmodifiableDelegatingMap { + private final Map thisMap; public DelegatingMap(Supplier> mapFactory, List> delegates) { - this.mapFactory = mapFactory; - this.delegates = delegates; + super(mapFactory, delegates); thisMap = mapFactory.get(); } @@ -26,67 +23,36 @@ public class DelegatingMap implements Map { @Override public boolean isEmpty() { if(!thisMap.isEmpty()) return false; - for(Map delegate : delegates) { - if(!delegate.isEmpty()) { - return false; - } - } - return true; + return super.isEmpty(); } @Override public boolean containsKey(Object key) { if(thisMap.containsKey(key)) return true; - for(Map delegate : delegates) { - if(!delegate.containsKey(key)) { - return true; - } - } - return false; + return super.containsKey(key); } @Override public boolean containsValue(Object value) { if(thisMap.containsValue(value)) return true; - for(Map delegate : delegates) { - if(!delegate.containsValue(value)) { - return true; - } - } - return false; + return super.containsValue(value); } @Override public V get(Object key) { - V result = thisMap.get(key); - if(result != null) return result; - for(Map delegate : delegates) { - result = delegate.get(key); - if(result != null) break; - } - return result; + return Optional.ofNullable(thisMap.get(key)).orElseGet( + () -> super.get(key) + ); } @Override public V put(K key, V value) { - V result = thisMap.put(key, value); - if(result != null) return result; - for(Map delegate : delegates) { - result = delegate.put(key, value); - if(result != null) break; - } - return result; + return thisMap.put(key, value); } @Override public V remove(Object key) { - V result = thisMap.remove(key); - if(result != null) return result; - for(Map delegate : delegates) { - result = delegate.remove(key); - if(result != null) break; - } - return result; + return thisMap.remove(key); } @Override @@ -104,24 +70,8 @@ public class DelegatingMap implements Map { return flatten().keySet(); } - private Map flatten() { - Map result = mapFactory.get(); - int i = delegates.size(); - while(i-->0) { - Map delegate = delegates.get(i); - result.putAll(delegate); - } - result.putAll(thisMap); - return Collections.unmodifiableMap(result); - } - - @Override - public Collection values() { - return flatten().values(); - } - - @Override - public Set> entrySet() { - return flatten().entrySet(); + protected Map flatten() { + return streamCat(super.stream(), thisMap.entrySet().stream()) + .collect(CollectionUtils.toUnmodifiableMap(this.mapFactory, Map.Entry::getKey, Map.Entry::getValue)); } } diff --git a/src/main/java/net/woggioni/jwo/Hash.java b/src/main/java/net/woggioni/jwo/Hash.java index 746d377..57329ad 100644 --- a/src/main/java/net/woggioni/jwo/Hash.java +++ b/src/main/java/net/woggioni/jwo/Hash.java @@ -6,6 +6,9 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import java.io.InputStream; +import java.io.OutputStream; +import java.security.DigestInputStream; +import java.security.DigestOutputStream; import java.security.MessageDigest; @EqualsAndHashCode @@ -27,6 +30,14 @@ public class Hash { public MessageDigest newMessageDigest() { return MessageDigest.getInstance(key); } + @SneakyThrows + public DigestOutputStream newOutputStream(OutputStream delegate) { + return new DigestOutputStream(delegate, MessageDigest.getInstance(key)); + } + @SneakyThrows + public DigestInputStream newInputStream(InputStream delegate) { + return new DigestInputStream(delegate, MessageDigest.getInstance(key)); + } } @Getter diff --git a/src/main/java/net/woggioni/jwo/JWO.java b/src/main/java/net/woggioni/jwo/JWO.java index c48df8e..141d551 100644 --- a/src/main/java/net/woggioni/jwo/JWO.java +++ b/src/main/java/net/woggioni/jwo/JWO.java @@ -22,6 +22,7 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; +import java.io.StringWriter; import java.io.Writer; import java.lang.reflect.Constructor; import java.nio.channels.Channels; @@ -104,23 +105,19 @@ public class JWO { @SneakyThrows public static void writeBytes2File(Path file, byte[] bytes) { - try (OutputStream os = new FileOutputStream(file.toString())) { + try (OutputStream os = Files.newOutputStream(file)) { os.write(bytes); } } @SneakyThrows public static String readFile2String(File file) { - StringBuilder builder = new StringBuilder(); - try (Reader reader = new InputStreamReader(new BufferedInputStream(new FileInputStream(file.getPath())))) { + StringWriter writer = new StringWriter(); + try (Reader reader = Files.newBufferedReader(file.toPath())) { char[] buffer = new char[1024]; - while (true) { - int read = reader.read(buffer); - builder.append(buffer, 0, read); - if (read < buffer.length) break; - } + JWO.copy(reader, writer, buffer); } - return builder.toString(); + return writer.toString(); } @SneakyThrows @@ -137,6 +134,13 @@ public class JWO { return builder.toString(); } + @SneakyThrows + public static T newThrowable(Class cls) { + Constructor constructor = cls.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } + @SneakyThrows public static T newThrowable(Class cls, String format, Object... args) { Constructor constructor = cls.getDeclaredConstructor(String.class); @@ -150,6 +154,11 @@ public class JWO { return constructor.newInstance(String.format(format, args), throwable); } + @SneakyThrows + public static T raise(Class cls) { + throw newThrowable(cls); + } + @SneakyThrows public static void raise(Class cls, Throwable throwable, String format, Object... args) { throw newThrowable(cls, throwable, format, args); @@ -160,7 +169,6 @@ public class JWO { throw newThrowable(cls, format, args); } - private static SSLSocketFactory defaultSSLSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); private static HostnameVerifier defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); @@ -563,13 +571,13 @@ public class JWO { byte[] buffer = new byte[0x10000]; expandZip(sourceArchive, (BiCon) (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); + Path entryPath = destinationFolder.resolve(zipEntry.getName()); + if(zipEntry.isDirectory()) { + Files.createDirectories(entryPath); + } else { + Files.createDirectories(entryPath.getParent()); + try(OutputStream outputStream = Files.newOutputStream(entryPath)) { + copy(zipInputStream, outputStream, buffer); } } }); @@ -815,7 +823,7 @@ public class JWO { } public static void replaceFileIfDifferent(InputStream inputStream, Path destination, FileAttribute... attrs) { - replaceFileIfDifferent(() -> inputStream, destination, attrs); + replaceFileIfDifferent(() -> inputStream, destination, null, attrs); } @SneakyThrows @@ -874,6 +882,18 @@ public class JWO { } } + public static Runnable curry(Consumer original, Supplier sup) { + return () -> original.accept(sup.get()); + } + + public static Runnable curry(Consumer original, T arg) { + return () -> original.accept(arg); + } + + public static Supplier curry(Fun original, T arg) { + return () -> original.apply(arg); + } + public static Fun curry1(BiFun original, T arg) { return u -> original.apply(arg, u); } @@ -894,7 +914,7 @@ public class JWO { return Stream.generate(valueSupplier).limit(1); } - public static Supplier compose(Supplier sup, Function fun) { + public static Supplier compose(Supplier sup, Function fun) { return () -> fun.apply(sup.get()); } diff --git a/src/main/java/net/woggioni/jwo/JavaVersion.java b/src/main/java/net/woggioni/jwo/JavaVersion.java index d177020..4e39ad6 100644 --- a/src/main/java/net/woggioni/jwo/JavaVersion.java +++ b/src/main/java/net/woggioni/jwo/JavaVersion.java @@ -72,6 +72,11 @@ public enum JavaVersion { */ VERSION_20, + /** + * Java 20 major version. + */ + VERSION_21, + /** * Higher version of Java. */ @@ -79,7 +84,10 @@ public enum JavaVersion { // Since Java 9, version should be X instead of 1.X // However, to keep backward compatibility, we change from 11 private static final int FIRST_MAJOR_VERSION_ORDINAL = 10; - private static JavaVersion currentJavaVersion; + private static LazyValue currentJavaVersion = LazyValue.of( + () -> toVersion(System.getProperty("java.version")), + LazyValue.ThreadSafetyMode.SYNCHRONIZED + ); private final String versionName; JavaVersion() { @@ -125,10 +133,7 @@ public enum JavaVersion { * @return The version of the current JVM. */ public static JavaVersion current() { - if (currentJavaVersion == null) { - currentJavaVersion = toVersion(System.getProperty("java.version")); - } - return currentJavaVersion; + return currentJavaVersion.get(); } static void resetCurrent() { @@ -146,90 +151,6 @@ public enum JavaVersion { return forClassVersion(classData[7] & 0xFF); } - public boolean isJava5() { - return this == VERSION_1_5; - } - - public boolean isJava6() { - return this == VERSION_1_6; - } - - public boolean isJava7() { - return this == VERSION_1_7; - } - - public boolean isJava8() { - return this == VERSION_1_8; - } - - public boolean isJava9() { - return this == VERSION_1_9; - } - - public boolean isJava10() { - return this == VERSION_1_10; - } - - /** - * Returns if the version is Java 11. - * - * @since 4.7 - */ - public boolean isJava11() { - return this == VERSION_11; - } - - /** - * Returns if the version is Java 12. - * - * @since 5.0 - */ - public boolean isJava12() { - return this == VERSION_12; - } - - public boolean isJava5Compatible() { - return isCompatibleWith(VERSION_1_5); - } - - public boolean isJava6Compatible() { - return isCompatibleWith(VERSION_1_6); - } - - public boolean isJava7Compatible() { - return isCompatibleWith(VERSION_1_7); - } - - public boolean isJava8Compatible() { - return isCompatibleWith(VERSION_1_8); - } - - public boolean isJava9Compatible() { - return isCompatibleWith(VERSION_1_9); - } - - public boolean isJava10Compatible() { - return isCompatibleWith(VERSION_1_10); - } - - /** - * Returns if the version is Java 11 compatible. - * - * @since 4.7 - */ - public boolean isJava11Compatible() { - return isCompatibleWith(VERSION_11); - } - - /** - * Returns if the version is Java 12 compatible. - * - * @since 5.0 - */ - public boolean isJava12Compatible() { - return isCompatibleWith(VERSION_12); - } - /** * Returns if this version is compatible with the given version * diff --git a/src/main/java/net/woggioni/jwo/LRUCache.java b/src/main/java/net/woggioni/jwo/LRUCache.java new file mode 100644 index 0000000..14d6446 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/LRUCache.java @@ -0,0 +1,110 @@ +package net.woggioni.jwo; + +import lombok.RequiredArgsConstructor; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +@RequiredArgsConstructor +public class LRUCache implements Map { + private final Map delegate; + + private LRUCache(final long maxSize, final Function loader, Class cls) { + delegate = new LinkedHashMap() { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() >= maxSize; + } + + @Override + public V get(Object key) { + if(cls.isInstance(key)) { + return computeIfAbsent((K) key, loader); + } else { + return null; + } + } + }; + } + + @Override + public V get(Object key) { + return delegate.get(key); + } + + @Override + public boolean containsKey(Object key) { + return delegate.containsKey(key); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public V put(K key, V value) { + return null; + } + + @Override + public boolean containsValue(Object value) { + return delegate.containsValue(value); + } + + @Override + public V remove(Object key) { + return delegate.remove(key); + } + + @Override + public void putAll(Map m) { + delegate.putAll(m); + } + + @Override + public void clear() { + delegate.clear(); + } + + @Override + public Set keySet() { + return delegate.keySet(); + } + + @Override + public Collection values() { + return delegate.values(); + } + + @Override + public Set> entrySet() { + return delegate.entrySet(); + } + + @Override + public boolean equals(Object o) { + if(o instanceof LRUCache) { + return delegate.equals(o); + } else { + return false; + } + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + public static LRUCache of(final long maxSize, final Function loader, Class cls) { + return new LRUCache<>(maxSize, loader, cls); + } +} diff --git a/src/main/java/net/woggioni/jwo/NullReader.java b/src/main/java/net/woggioni/jwo/NullReader.java new file mode 100644 index 0000000..32ec3ea --- /dev/null +++ b/src/main/java/net/woggioni/jwo/NullReader.java @@ -0,0 +1,14 @@ +package net.woggioni.jwo; + +import java.io.IOException; +import java.io.Reader; + +public class NullReader extends Reader { + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + return -1; + } + + @Override + public void close() {} +} diff --git a/src/main/java/net/woggioni/jwo/NullWriter.java b/src/main/java/net/woggioni/jwo/NullWriter.java new file mode 100644 index 0000000..e730232 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/NullWriter.java @@ -0,0 +1,18 @@ +package net.woggioni.jwo; + +import java.io.IOException; +import java.io.Writer; + +public class NullWriter extends Writer { + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + } + + @Override + public void flush() { + } + + @Override + public void close() { + } +} diff --git a/src/main/java/net/woggioni/jwo/RetryExecutor.java b/src/main/java/net/woggioni/jwo/RetryExecutor.java index 3303e99..f5c0635 100644 --- a/src/main/java/net/woggioni/jwo/RetryExecutor.java +++ b/src/main/java/net/woggioni/jwo/RetryExecutor.java @@ -30,7 +30,7 @@ public class RetryExecutor { private final ExceptionHandler exceptionHandler = err -> ExceptionHandlerOutcome.CONTINUE; @Builder.Default - private final ExecutorService executorService = ForkJoinPool.commonPool(); + private final Executor executor = ForkJoinPool.commonPool(); public enum ExceptionHandlerOutcome { THROW, CONTINUE @@ -41,7 +41,7 @@ public class RetryExecutor { } public CompletableFuture submit(Runnable cb) { - return submit(cb, maxAttempts, initialDelay, exp, exceptionHandler, executorService); + return submit(cb, maxAttempts, initialDelay, exp, exceptionHandler, executor); } public CompletableFuture submit( @@ -79,36 +79,36 @@ public class RetryExecutor { Duration initialDelay, Double exp, ExceptionHandler exceptionHandler, - ExecutorService executorService) { + Executor executor) { return submit(() -> { cb.run(); return null; - }, maxAttempts, initialDelay, exp, exceptionHandler, executorService); + }, maxAttempts, initialDelay, exp, exceptionHandler, executor); } public CompletableFuture submit(Callable cb) { - return submit(cb, maxAttempts, initialDelay, exp, exceptionHandler, executorService); + return submit(cb, maxAttempts, initialDelay, exp, exceptionHandler, executor); } public CompletableFuture submit( Callable cb, - ExecutorService executorService) { - return submit(cb, maxAttempts, initialDelay, exp, exceptionHandler, executorService); + Executor executor) { + return submit(cb, maxAttempts, initialDelay, exp, exceptionHandler, executor); } public CompletableFuture submit( Callable cb, ExceptionHandler exceptionHandler, - ExecutorService executorService) { - return submit(cb, maxAttempts, initialDelay, exp, exceptionHandler, executorService); + Executor executor) { + return submit(cb, maxAttempts, initialDelay, exp, exceptionHandler, executor); } public CompletableFuture submit( Callable cb, Double exp, ExceptionHandler exceptionHandler, - ExecutorService executorService) { - return submit(cb, maxAttempts, initialDelay, exp, exceptionHandler, executorService); + ExecutorService executor) { + return submit(cb, maxAttempts, initialDelay, exp, exceptionHandler, executor); } public CompletableFuture submit( @@ -116,8 +116,8 @@ public class RetryExecutor { Duration initialDelay, Double exp, ExceptionHandler exceptionHandler, - ExecutorService executorService) { - return submit(cb, maxAttempts, initialDelay, exp, exceptionHandler, executorService); + ExecutorService executor) { + return submit(cb, maxAttempts, initialDelay, exp, exceptionHandler, executor); } public static CompletableFuture submit( @@ -126,8 +126,8 @@ public class RetryExecutor { Duration initialDelay, Double exp, ExceptionHandler exceptionHandler, - ExecutorService executorService) { - CompletableFuture result = CompletableFuture.supplyAsync((Sup) cb::call, executorService); + Executor executor) { + CompletableFuture result = CompletableFuture.supplyAsync((Sup) cb::call, executor); double delay = initialDelay.toMillis(); for(int i = 1; i <= maxAttempts; i++) { int attempt = i; @@ -148,7 +148,7 @@ public class RetryExecutor { Executor delayedExecutor = delayedExecutor( (long) thisAttemptDelay, TimeUnit.MILLISECONDS, - executorService + executor ); return CompletableFuture.supplyAsync((Sup) cb::call, delayedExecutor); default: @@ -159,9 +159,9 @@ public class RetryExecutor { ); } } - }, executorService).thenComposeAsync(Function.identity(), executorService); + }, executor).thenComposeAsync(Function.identity(), executor); delay *= exp; } return result; } -} +} \ No newline at end of file diff --git a/src/main/java/net/woggioni/jwo/UnmodifiableDelegatingMap.java b/src/main/java/net/woggioni/jwo/UnmodifiableDelegatingMap.java new file mode 100644 index 0000000..d2a9c92 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/UnmodifiableDelegatingMap.java @@ -0,0 +1,123 @@ +package net.woggioni.jwo; + +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static net.woggioni.jwo.JWO.newThrowable; + +@RequiredArgsConstructor +public class UnmodifiableDelegatingMap implements Map { + protected final Supplier> mapFactory; + private final List> delegates; + + public static UnmodifiableDelegatingMap of( + Supplier> mapFactory, + Map... delegates + ) { + return new UnmodifiableDelegatingMap<>(mapFactory, Arrays.asList(delegates)); + } + + @Override + public int size() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isEmpty() { + for (Map delegate : delegates) { + if (!delegate.isEmpty()) { + return false; + } + } + return true; + } + + @Override + public boolean containsKey(Object key) { + for (Map delegate : delegates) { + if (delegate.containsKey(key)) { + return true; + } + } + return false; + } + + @Override + public boolean containsValue(Object value) { + for (Map delegate : delegates) { + if (delegate.containsValue(value)) { + return true; + } + } + return false; + } + + @Override + public V get(Object key) { + V result = null; + for (Map delegate : delegates) { + result = delegate.get(key); + if (result != null) break; + } + return result; + } + + @Override + public V put(K key, V value) { + throw newThrowable(UnsupportedOperationException.class); + } + + @Override + public V remove(Object key) { + throw newThrowable(UnsupportedOperationException.class); + } + + @Override + public void putAll(Map m) { + throw newThrowable(UnsupportedOperationException.class); + } + + @Override + public void clear() { + throw newThrowable(UnsupportedOperationException.class); + } + + @Override + public Set keySet() { + return flatten().keySet(); + } + + private Map flatten() { + return stream().collect( + CollectionUtils.toUnmodifiableMap( + mapFactory, + Map.Entry::getKey, + Map.Entry::getValue, + CollectionUtils::newValueMerger + ) + ); + } + + protected Stream> stream() { + return JWO.iterator2Stream(CollectionUtils.reverseIterator(delegates)).flatMap( + it -> it.entrySet().stream() + ); + } + + @Override + public Collection values() { + return flatten().values(); + } + + @Override + public Set> entrySet() { + return flatten().entrySet(); + } +} diff --git a/src/main/java/net/woggioni/jwo/internal/MutableTuple2Impl.java b/src/main/java/net/woggioni/jwo/internal/MutableTuple2Impl.java index a63cbb6..5b24475 100644 --- a/src/main/java/net/woggioni/jwo/internal/MutableTuple2Impl.java +++ b/src/main/java/net/woggioni/jwo/internal/MutableTuple2Impl.java @@ -1,23 +1,12 @@ package net.woggioni.jwo.internal; import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.Data; import net.woggioni.jwo.MutableTuple2; -@EqualsAndHashCode -@NoArgsConstructor +@Data @AllArgsConstructor public class MutableTuple2Impl implements MutableTuple2 { - @Getter - @Setter private T _1; - - - - @Getter - @Setter private U _2; } \ No newline at end of file diff --git a/src/main/java/net/woggioni/jwo/internal/MutableTuple3Impl.java b/src/main/java/net/woggioni/jwo/internal/MutableTuple3Impl.java index 29b11fc..c385c67 100644 --- a/src/main/java/net/woggioni/jwo/internal/MutableTuple3Impl.java +++ b/src/main/java/net/woggioni/jwo/internal/MutableTuple3Impl.java @@ -1,25 +1,15 @@ package net.woggioni.jwo.internal; import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.Data; import net.woggioni.jwo.MutableTuple3; -@EqualsAndHashCode -@NoArgsConstructor +@Data @AllArgsConstructor public class MutableTuple3Impl implements MutableTuple3 { - @Getter - @Setter private T _1; - @Getter - @Setter private U _2; - @Getter - @Setter private V _3; } \ No newline at end of file diff --git a/src/main/java/net/woggioni/jwo/internal/Tuple2Impl.java b/src/main/java/net/woggioni/jwo/internal/Tuple2Impl.java index 778c7f1..ad5769c 100644 --- a/src/main/java/net/woggioni/jwo/internal/Tuple2Impl.java +++ b/src/main/java/net/woggioni/jwo/internal/Tuple2Impl.java @@ -1,17 +1,12 @@ package net.woggioni.jwo.internal; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.RequiredArgsConstructor; +import lombok.Data; import net.woggioni.jwo.Tuple2; -@EqualsAndHashCode -@RequiredArgsConstructor +@Data public class Tuple2Impl implements Tuple2 { - @Getter private final T _1; - @Getter private final U _2; } diff --git a/src/test/java/net/woggioni/jwo/ApplicationTest.java b/src/test/java/net/woggioni/jwo/ApplicationTest.java new file mode 100644 index 0000000..84c7943 --- /dev/null +++ b/src/test/java/net/woggioni/jwo/ApplicationTest.java @@ -0,0 +1,21 @@ +package net.woggioni.jwo; + +import org.junit.jupiter.api.Test; + +public class ApplicationTest { + + @Test + void builderTest() { + final var app = Application.builder("app") + .configurationDirectoryPropertyKey("app.conf.dir") + .dataDirectoryPropertyKey("app.data.dir") + .cacheDirectoryPropertyKey("app.cache.dir") + .cacheDirectoryEnvVar("APP_CACHE_DIR") + .dataDirectoryEnvVar("APP_DATA_DIR") + .configurationDirectoryEnvVar("APP_CONF_DIR") + .build(); + final var confDir = app.computeConfigurationDirectory(); + final var cacheDir = app.computeCacheDirectory(); + final var dataDir = app.computeDataDirectory(); + } +} diff --git a/src/test/java/net/woggioni/jwo/JWOTest.java b/src/test/java/net/woggioni/jwo/JWOTest.java index b216e18..5449e14 100644 --- a/src/test/java/net/woggioni/jwo/JWOTest.java +++ b/src/test/java/net/woggioni/jwo/JWOTest.java @@ -3,33 +3,65 @@ package net.woggioni.jwo; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.io.Reader; +import java.io.Writer; import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileTime; import java.nio.file.attribute.PosixFileAttributes; import java.nio.file.attribute.UserPrincipal; +import java.security.DigestInputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.TreeMap; +import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; import static net.woggioni.jwo.CollectionUtils.immutableList; import static net.woggioni.jwo.CollectionUtils.newArrayList; +import static net.woggioni.jwo.Misc.CRACKLIB_RESOURCE; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.assertTrue; public class JWOTest { @@ -40,38 +72,57 @@ public class JWOTest { if (n > 3) return Optional.of(n); else return Optional.empty(); }).collect(Collectors.toList()); - Assertions.assertEquals(Collections.singletonList(4), l); + assertEquals(Collections.singletonList(4), l); } @Test public void optional2StreamTest() { Integer integer = 3; Optional s = Optional.of(integer); - JWO.optional2Stream(s).forEach(n -> Assertions.assertEquals(integer, n)); + JWO.optional2Stream(s).forEach(n -> assertEquals(integer, n)); s = Optional.empty(); JWO.optional2Stream(s).forEach(n -> Assertions.fail( "Stream should have been empty and this piece of code never executed") ); } + @Test + public void optional2StreamTest2() { + Integer integer = 3; + Optional o1 = Optional.of(integer); + Integer integer2 = 3; + Optional o2 = Optional.of(integer2); + final var values = JWO.optional2Stream(o1, Optional.empty(), o2) + .collect(Collectors.toList()); + assertEquals(Arrays.asList(integer, integer2), values); + } + + @Test + public void optionalOrTest() { + Integer integer = 3; + Optional o1 = Optional.of(integer); + Optional o2 = Optional.of(integer); + assertEquals(o2, JWO.or(Optional.empty(), o2)); + assertEquals(o1, JWO.or(o1, Optional.empty())); + assertEquals(Optional.empty(), JWO.or(Optional.empty(), Optional.empty())); + } + @RequiredArgsConstructor enum IndexOfWithEscapeTestCase { SIMPLE(" dsds $sdsa \\$dfivbdsf \\\\$sdgsga", '$', '\\', - immutableList(6, 25)), + immutableList(6, 25)), SIMPLE2("asdasd$$vdfv$", '$', '$', - immutableList(12)), + immutableList(12)), NO_NEEDLE("asdasd$$vdfv$", '#', '\\', - immutableList()), + immutableList()), ESCAPED_NEEDLE("asdasd$$vdfv$#sdfs", '#', '$', - immutableList()), + immutableList()), NOT_ESCAPED_NEEDLE("asdasd$$#vdfv$#sdfs", '#', '$', - immutableList(8)), + immutableList(8)), SDFSD("\n${sys:user.home}${env:HOME}", ':', '\\', - immutableList(6, 22)) - - ; + immutableList(6, 22)); final String haystack; final Character needle; @@ -86,13 +137,13 @@ public class JWOTest { String haystack = testCase.haystack; List solution = newArrayList(); int i = 0; - while(true) { + while (true) { i = JWO.indexOfWithEscape(haystack, testCase.needle, testCase.escape, i, haystack.length()); - if(i < 0) break; + if (i < 0) break; solution.add(i); ++i; } - Assertions.assertEquals(testCase.solution, solution); + assertEquals(testCase.solution, solution); } @Test @@ -103,35 +154,35 @@ public class JWOTest { valuesMap.put("date", "2020-03-25 16:22"); valuesMap.put("adjective", "simple"); String expected = """ - This is a simple test made by John Doe on 2020-03-25 16:22. It's really simple! - /home/user - /home/user - defaultValue - """; + This is a simple test made by John Doe on 2020-03-25 16:22. It's really simple! + /home/user + /home/user + defaultValue + """; Map> contextMap = new MapBuilder>() - .entry("env", - new MapBuilder() - .entry("HOME", "/home/user") - .build(TreeMap::new, Collections::unmodifiableMap) - ) - .entry("sys", - new MapBuilder() - .entry("user.home", "/home/user") - .build(TreeMap::new, Collections::unmodifiableMap) ).build(TreeMap::new, Collections::unmodifiableMap); + .entry("env", + new MapBuilder() + .entry("HOME", "/home/user") + .build(TreeMap::new, Collections::unmodifiableMap) + ) + .entry("sys", + new MapBuilder() + .entry("user.home", "/home/user") + .build(TreeMap::new, Collections::unmodifiableMap)).build(TreeMap::new, Collections::unmodifiableMap); try (Reader reader = new InputStreamReader( - JWOTest.class.getResourceAsStream("/render_template_test.txt"))) { + JWOTest.class.getResourceAsStream("/render_template_test.txt"))) { String rendered = JWO.renderTemplate(reader, valuesMap, contextMap); - Assertions.assertEquals(expected, rendered); + assertEquals(expected, rendered); } try (Reader reader = new InputStreamReader( - JWOTest.class.getResourceAsStream("/render_template_test.txt"))) { + JWOTest.class.getResourceAsStream("/render_template_test.txt"))) { String rendered = JWO.renderTemplate(JWO.readAll(reader), valuesMap, contextMap); - Assertions.assertEquals(expected, rendered); + assertEquals(expected, rendered); } } - public static String renderTemplateNaive(String template, Map valuesMap){ + public static String renderTemplateNaive(String template, Map valuesMap) { StringBuilder formatter = new StringBuilder(template); Object absent = new Object(); @@ -149,7 +200,7 @@ public class JWOTest { // If the key is not present, leave the variable untouched. if (index != -1) { Object value = valuesMap.getOrDefault(key, absent); - if(value != absent) { + if (value != absent) { String valueStr = value != null ? value.toString() : ""; formatter.replace(index, index + formatKey.length(), valueStr); } @@ -169,6 +220,451 @@ public class JWOTest { m.setAccessible(true); int expectedUserId = (Integer) m.invoke(expectedUser); int uid = (int) JWO.uid(); - Assertions.assertEquals(expectedUserId, uid); + assertEquals(expectedUserId, uid); + } + + @Nested + class ReplaceFileIfDifferentTest { + + private interface CommonInterface { + void replaceFileIfDifferent( + Supplier inputStreamSupplier, + Path destination, + FileAttribute... attrs + ); + } + + + @RequiredArgsConstructor + public enum MethodToTest { + STREAM_METHOD((Supplier inputStreamSupplier, + Path destination, + FileAttribute... attrs) -> { + JWO.replaceFileIfDifferent(inputStreamSupplier.get(), destination, attrs); + }), + SUPPLIER_METHOD(JWO::replaceFileIfDifferent); + + private final CommonInterface xface; + + public void replaceFileIfDifferent( + Supplier inputStreamSupplier, + Path destination, + FileAttribute... attrs + ) { + xface.replaceFileIfDifferent(inputStreamSupplier, destination, attrs); + } + } + + private static final Supplier source = (Sup) () -> Optional.ofNullable( + ReplaceFileIfDifferentTest.class.getResourceAsStream(CRACKLIB_RESOURCE) + ).orElseThrow(Assertions::fail); + + @TempDir + private Path testDir; + + @SneakyThrows + @ParameterizedTest + @EnumSource(MethodToTest.class) + public void ensureFileCopy(MethodToTest methodToTest) { + final var dest = testDir.resolve("cracklib-small"); + methodToTest.replaceFileIfDifferent(source, dest); + Hash newFileHash, newContentHash; + try (final var inputStream = source.get()) { + newContentHash = Hash.md5(inputStream); + } + try (final var inputStream = Files.newInputStream(dest)) { + newFileHash = Hash.md5(inputStream); + } + assertEquals(newContentHash, newFileHash); + } + + @SneakyThrows + @ParameterizedTest + @EnumSource(MethodToTest.class) + public void ensureNoWriteWithNoChange(MethodToTest methodToTest) { + final var dest = testDir.resolve("cracklib-small"); + try (final var inputStream = source.get()) { + Files.copy(inputStream, dest); + } + final var initialTime = Files.getLastModifiedTime(dest); + methodToTest.replaceFileIfDifferent(source, dest); + final var replacedTime = Files.getLastModifiedTime(dest); + assertEquals(initialTime, replacedTime); + } + + @SneakyThrows + @ParameterizedTest + @EnumSource(MethodToTest.class) + public void ensureWriteWithContentChange(MethodToTest methodToTest) { + final var dest = testDir.resolve("cracklib-small"); + try (final var inputStream = source.get()) { + Files.copy(inputStream, dest); + } + final var newContent = (Sup) () -> new ByteArrayInputStream( + "new File content".getBytes(StandardCharsets.UTF_8) + ); + final var initialTime = Files.getLastModifiedTime(dest); + methodToTest.replaceFileIfDifferent(newContent, dest); + final var replacedTime = Files.getLastModifiedTime(dest); + assertTrue( + Comparator.naturalOrder() + .compare(initialTime, replacedTime) <= 0); + Hash newFileHash, newContentHash; + try (final var inputStream = newContent.get()) { + newContentHash = Hash.md5(inputStream); + } + try (final var inputStream = Files.newInputStream(dest)) { + newFileHash = Hash.md5(inputStream); + } + assertEquals(newContentHash, newFileHash); + } + } + + @Nested + class CapitalizeTest { + private static Stream> testCases() { + return Stream.of( + new TestCase<>("Home", "Home", null), + new TestCase<>("HOME", "HOME", null), + new TestCase<>("leilei", "Leilei", null), + new TestCase<>("4365", "4365", null), + new TestCase<>("芦 雷雷", "芦 雷雷", null), + new TestCase<>("dž123", "Dž123", null), + new TestCase<>(null, null, NullPointerException.class) + ); + } + + @ParameterizedTest + @MethodSource("testCases") + public void capitalizeTest(TestCase testCase) { + if (testCase.error() == null) { + assertEquals(testCase.expectedOutput(), JWO.capitalize(testCase.input())); + } else { + assertThrows( + testCase.error(), + JWO.curry( + (Consumer) JWO::capitalize, + (Supplier) testCase::input + )::run + ); + } + } + } + + + @Nested + class DecapitalizeTest { + private static Stream> testCases() { + return Stream.of( + new TestCase<>("Home", "home", null), + new TestCase<>("HOME", "hOME", null), + new TestCase<>("Leilei", "leilei", null), + new TestCase<>("4365", "4365", null), + new TestCase<>("芦 雷雷", "芦 雷雷", null), + new TestCase<>("Dž123", "dž123", null), + new TestCase<>(null, null, NullPointerException.class) + ); + } + + @ParameterizedTest + @MethodSource("testCases") + public void decapitalizeTest(TestCase testCase) { + if (testCase.error() == null) { + assertEquals(testCase.expectedOutput(), JWO.decapitalize(testCase.input())); + } else { + assertThrows( + testCase.error(), + JWO.curry( + (Consumer) JWO::decapitalize, + (Supplier) testCase::input + )::run + ); + } + } + } + + + @Nested + class InstallResourceTest { + @TempDir + private Path testDir; + + @SneakyThrows + @Test + public void ensureFileCopy() { + final var dest = testDir.resolve("some/nested/path/cracklib-small"); + JWO.installResource(CRACKLIB_RESOURCE, dest, getClass()); + Hash newFileHash, newContentHash; + try (final var inputStream = getClass().getResourceAsStream(CRACKLIB_RESOURCE)) { + newContentHash = Hash.md5(inputStream); + } + try (final var inputStream = Files.newInputStream(dest)) { + newFileHash = Hash.md5(inputStream); + } + assertEquals(newContentHash, newFileHash); + } + } + + @Nested + public class ArrayIteratorTest { + public static Stream> test() { + return Stream.of( + new TestCase<>(new Integer[]{}, null, null), + new TestCase<>(new Integer[]{1, 2, 3, 4, 5, null, 6, 7, 8, 9, 10}, null, null) + ); + } + + @MethodSource + @ParameterizedTest + public void test(TestCase testCase) { + final var it = JWO.iterator(testCase.input()); + + for (Integer n : testCase.input()) { + final var m = it.next(); + assertEquals(n, m); + } + assertFalse(it.hasNext()); + } + } + + @Test + @SneakyThrows + public void copyTest() { + MessageDigest md1 = Hash.Algorithm.MD5.newMessageDigest(); + final Supplier source = JWO.compose( + JWO.compose( + () -> getClass().getResourceAsStream(CRACKLIB_RESOURCE), + (InputStream is) -> new DigestInputStream(is, md1) + ), + (InputStream is) -> new InputStreamReader(is) + ); + MessageDigest md2 = Hash.Algorithm.MD5.newMessageDigest(); + final Supplier destination = JWO.compose( + JWO.compose( + NullOutputStream::new, + (OutputStream os) -> new DigestOutputStream(os, md2) + ), + (OutputStream is) -> new OutputStreamWriter(is) + ); + + try (final var reader = source.get()) { + try (Writer writer = destination.get()) { + JWO.copy(reader, writer); + } + } + assertArrayEquals(md1.digest(), md2.digest()); + } + + @Test + @SneakyThrows + public void extractZipTest(@TempDir Path testDir) { + final var testBundle = Optional.ofNullable(System.getProperty("zip.test.bundle")) + .map(Path::of) + .orElseGet(Assertions::fail); + final var destination1 = testDir.resolve("bundle"); + final var destination2 = testDir.resolve("bundle2"); + final var reassembledBundle = testDir.resolve("bundle3.zip"); + final var destination3 = testDir.resolve("bundle3"); + try (final var zos = new ZipOutputStream(Files.newOutputStream(reassembledBundle))) { + JWO.expandZip(testBundle, (zis, zipEntry) -> { + final var baos = new ByteArrayOutputStream(); + JWO.copy(zis, baos); + if (zipEntry.isDirectory()) { + final var dir = destination1.resolve(zipEntry.getName()); + Files.createDirectories(dir); + JWO.writeZipEntry(zos, + () -> new ByteArrayInputStream(baos.toByteArray()), + zipEntry.getName(), + ZipEntry.STORED + ); + } else { + final var file = destination1.resolve(zipEntry.getName()); + Files.createDirectories(file.getParent()); + try (final var os = Files.newOutputStream(file)) { + JWO.copy(new ByteArrayInputStream(baos.toByteArray()), os); + } + JWO.writeZipEntry(zos, + () -> new ByteArrayInputStream(baos.toByteArray()), + zipEntry.getName(), + ZipEntry.DEFLATED + ); + } + }); + } + JWO.extractZip(testBundle, destination2); + JWO.extractZip(reassembledBundle, destination3); + final var visitor = new FileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + final var relativePath = destination1.relativize(file); + final var hashes = Stream.of( + destination1.resolve(relativePath), + destination2.resolve(relativePath), + destination3.resolve(relativePath) + ).map((Fun) p -> { + Hash hash; + try (final var is = Files.newInputStream(p)) { + hash = Hash.md5(is); + } + return hash; + }).collect(Collectors.toSet()); + assertTrue(hashes.size() == 1); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) { + return FileVisitResult.CONTINUE; + } + }; + Files.walkFileTree(destination1, visitor); + Stream.of(destination1, destination2, destination3) + .forEach(p -> { + JWO.deletePath(p); + assertFalse(Files.exists(p)); + }); + } + + @Test + @SneakyThrows + public void readResource2StringTest() { + final var hash1 = Optional.of(JWO.readResource2String(CRACKLIB_RESOURCE)) + .map(it -> it.getBytes(StandardCharsets.UTF_8)) + .map(ByteArrayInputStream::new) + .map(Hash::md5) + .orElseGet(Assertions::fail); + + Hash hash2; + try (final var is = getClass().getResourceAsStream(CRACKLIB_RESOURCE)) { + hash2 = Hash.md5(is); + } + assertEquals(hash1, hash2); + } + + @Nested + class NewThrowableTest { + private static class SomeWeirdException extends RuntimeException { + public SomeWeirdException() { + super(); + } + + public SomeWeirdException(String msg) { + super(msg); + } + + public SomeWeirdException(String msg, Throwable cause) { + super(msg, cause); + } + } + } + + @Test + public void newThrowableTest1() { + final var ex = JWO.newThrowable(NewThrowableTest.SomeWeirdException.class); + assertThrows(NewThrowableTest.SomeWeirdException.class, () -> { + throw ex; + }); + assertThrowsExactly(NewThrowableTest.SomeWeirdException.class, () -> { + JWO.raise(NewThrowableTest.SomeWeirdException.class); + }); + } + + @Test + public void newThrowableTest2() { + final var ex = JWO.newThrowable(NewThrowableTest.SomeWeirdException.class, "some message without placeholder"); + assertThrowsExactly(NewThrowableTest.SomeWeirdException.class, () -> { + throw ex; + }); + assertThrowsExactly(NewThrowableTest.SomeWeirdException.class, () -> { + JWO.raise(NewThrowableTest.SomeWeirdException.class, + "some message without placeholder" + ); + }); + } + + @Test + public void newThrowableTest3() { + final var ex = JWO.newThrowable(NewThrowableTest.SomeWeirdException.class, + "some message with placeholder %d", + 25 + ); + assertThrowsExactly(NewThrowableTest.SomeWeirdException.class, () -> { + throw ex; + }); + assertThrowsExactly(NewThrowableTest.SomeWeirdException.class, () -> { + JWO.raise(NewThrowableTest.SomeWeirdException.class, + "some message with placeholder %d", + 25 + ); + }); + } + + @Test + public void newThrowableTest4() { + final var cause = new RuntimeException(); + final var ex = JWO.newThrowable(NewThrowableTest.SomeWeirdException.class, + cause, + "some message with placeholder %d", + 25 + ); + assertThrowsExactly(NewThrowableTest.SomeWeirdException.class, () -> { + try { + throw ex; + } catch (Throwable t) { + assertTrue(t.getCause() == cause); + throw t; + } + }); + assertThrowsExactly(NewThrowableTest.SomeWeirdException.class, () -> { + try { + JWO.raise(NewThrowableTest.SomeWeirdException.class, + cause, + "some message with placeholder %d", + 25 + ); + } catch (Throwable t) { + assertTrue(t.getCause() == cause); + throw t; + } + }); + } + + @Test + public void enumerationTest() { + final var list = immutableList(1, 2, 3, 4, 5, 6, 7); + final var enumeration = Collections.enumeration(list); + final var list2 = JWO.iterator2Stream( + JWO.enumeration2Iterator(enumeration) + ).collect(Collectors.toList()); + assertEquals(list, list2); + } + + @Test + @SneakyThrows + public void readFile(@TempDir Path testDir) { + final var destination = testDir.resolve("cracklib-small"); + final var dis = Hash.Algorithm.MD5.newInputStream(getClass().getResourceAsStream(CRACKLIB_RESOURCE)); + final var md = dis.getMessageDigest(); + try { + try(final var os = Files.newOutputStream(destination)) { + JWO.copy(dis, os); + } + } finally { + dis.close(); + } + final var originalHash = md.digest(); + final var content = JWO.readFile2String(destination.toFile()); + final var readHash = Hash.md5(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))); + assertArrayEquals(originalHash, readHash.getBytes()); } } diff --git a/src/test/java/net/woggioni/jwo/JavaVersionTest.java b/src/test/java/net/woggioni/jwo/JavaVersionTest.java new file mode 100644 index 0000000..9f99b30 --- /dev/null +++ b/src/test/java/net/woggioni/jwo/JavaVersionTest.java @@ -0,0 +1,88 @@ +package net.woggioni.jwo; + +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.ByteArrayOutputStream; +import java.util.Comparator; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class JavaVersionTest { + + @Test + public void test() { + final var current = JavaVersion.current(); + assertTrue(current.isCompatibleWith(current)); + assertTrue(current.isCompatibleWith(JavaVersion.VERSION_1_8)); + assertFalse(JavaVersion.VERSION_1_4.isCompatibleWith(current)); + assertFalse(JavaVersion.VERSION_1_4.isCompatibleWith(JavaVersion.VERSION_1_7)); + assertTrue(JavaVersion.VERSION_17.isCompatibleWith(JavaVersion.VERSION_1_8)); + } + + public static Stream, Integer>> comparatorTest() { + return Stream.of( + new TestCase<>( + Tuple2.newInstance(JavaVersion.VERSION_1_4, JavaVersion.VERSION_1_4), + 0, + null + ), + new TestCase<>( + Tuple2.newInstance(JavaVersion.VERSION_1_1, JavaVersion.VERSION_1_4), + -1, + null + ), + new TestCase<>( + Tuple2.newInstance(JavaVersion.VERSION_21, JavaVersion.VERSION_1_8), + 1, + null + ), + new TestCase<>( + Tuple2.newInstance(JavaVersion.VERSION_17, JavaVersion.VERSION_1_5), + 1, + null + ), + new TestCase<>( + Tuple2.newInstance(JavaVersion.VERSION_17, JavaVersion.VERSION_19), + -1, + null + ), + new TestCase<>( + Tuple2.newInstance(JavaVersion.VERSION_21, JavaVersion.VERSION_20), + 1, + null + ) + ); + } + + @MethodSource + @ParameterizedTest + public void comparatorTest(TestCase, Integer> testCase) { + final var comparator = Comparator.naturalOrder(); + final var pair = testCase.input(); + final var comparisonResult = Optional.of(comparator.compare(pair.get_1(), pair.get_2())) + .map(v -> v / (v == 0 ? 1 : Math.abs(v))) + .orElseGet(Assertions::fail); + assertEquals(testCase.expectedOutput(), comparisonResult); + } + + @Test + @SneakyThrows + public void parseClassTest() { + try(final var is = getClass().getResourceAsStream("/net/woggioni/jwo/JWO.class")) { + final var baos = new ByteArrayOutputStream(); + try (baos) { + JWO.copy(is, baos); + } + final var jwoClassVersion = JavaVersion.forClass(baos.toByteArray()); + assertEquals(JavaVersion.VERSION_1_8, jwoClassVersion); + } + } +} diff --git a/src/test/java/net/woggioni/jwo/Misc.java b/src/test/java/net/woggioni/jwo/Misc.java new file mode 100644 index 0000000..f4c7bc1 --- /dev/null +++ b/src/test/java/net/woggioni/jwo/Misc.java @@ -0,0 +1,9 @@ +package net.woggioni.jwo; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class Misc { + public static final String CRACKLIB_RESOURCE = "/cracklib-small"; +} diff --git a/src/test/java/net/woggioni/jwo/RetryExecutorTest.java b/src/test/java/net/woggioni/jwo/RetryExecutorTest.java new file mode 100644 index 0000000..7f4025f --- /dev/null +++ b/src/test/java/net/woggioni/jwo/RetryExecutorTest.java @@ -0,0 +1,49 @@ +package net.woggioni.jwo; + +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class RetryExecutorTest { + @Test + @SneakyThrows + public void test() { + int expectedValue = 42; + ExecutorService executorService = Executors.newSingleThreadExecutor(); + RetryExecutor.ExceptionHandler exceptionHandler = err -> RetryExecutor.ExceptionHandlerOutcome.CONTINUE; + CompletableFuture n = RetryExecutor.submit( + new Callable<>() { + private int counter = 0; + + @Override + public Integer call() { + log.info("Attempt {}", counter); + try { + if (counter != 5) { + throw new RuntimeException(); + } else { + return expectedValue; + } + } finally { + counter++; + } + } + }, + 10, + Duration.ofMillis(100), + 1.3, + exceptionHandler, + executorService + ); + Assertions.assertEquals(expectedValue, n.get(10, TimeUnit.SECONDS)); + } +} \ No newline at end of file diff --git a/src/test/java/net/woggioni/jwo/TestCase.java b/src/test/java/net/woggioni/jwo/TestCase.java new file mode 100644 index 0000000..5c8716e --- /dev/null +++ b/src/test/java/net/woggioni/jwo/TestCase.java @@ -0,0 +1,4 @@ +package net.woggioni.jwo; + +public record TestCase(T input, U expectedOutput, Class error) { +} \ No newline at end of file diff --git a/src/test/java/net/woggioni/jwo/UnmodifiableDelegatingMapTest.java b/src/test/java/net/woggioni/jwo/UnmodifiableDelegatingMapTest.java new file mode 100644 index 0000000..6ad3ebd --- /dev/null +++ b/src/test/java/net/woggioni/jwo/UnmodifiableDelegatingMapTest.java @@ -0,0 +1,94 @@ +package net.woggioni.jwo; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Stream; + +import static net.woggioni.jwo.CollectionUtils.toMap; +import static net.woggioni.jwo.CollectionUtils.toUnmodifiableMap; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class UnmodifiableDelegatingMapTest { + + private static Map createMap() { + final var m1 = Stream.of( + Map.entry("a", 1), + Map.entry("b", 2), + Map.entry("c", 3), + Map.entry("d", 4), + Map.entry("e", 5), + Map.entry("f", 6) + ).collect(toUnmodifiableMap(TreeMap::new, Map.Entry::getKey, Map.Entry::getValue)); + final var m2 = Stream.of( + Map.entry("a", 7), + Map.entry("c", 8), + Map.entry("e", 9), + Map.entry("g", 10), + Map.entry("h", 11) + ).collect(toUnmodifiableMap(TreeMap::new, Map.Entry::getKey, Map.Entry::getValue)); + return UnmodifiableDelegatingMap.of(TreeMap::new, m1, m2); + } + + private final Map delegatingMap = createMap(); + @Test + public void test() { + assertEquals(1, delegatingMap.get("a")); + assertEquals(2, delegatingMap.get("b")); + assertEquals(10, delegatingMap.get("g")); + + for(final var entry : delegatingMap.entrySet()) { + assertEquals(delegatingMap.get(entry.getKey()), entry.getValue()); + } + } + + @Test + public void test2() { + assertFalse(delegatingMap.isEmpty()); + assertEquals(1, delegatingMap.get("a")); + assertEquals(2, delegatingMap.get("b")); + assertEquals(10, delegatingMap.get("g")); + + for(final var entry : delegatingMap.entrySet()) { + assertTrue(delegatingMap.containsKey(entry.getKey()), + String.format("Expected key '%s' was not found in map", entry.getKey())); + assertTrue(delegatingMap.containsValue(entry.getValue()), + String.format("Expected value '%d' was not found in map", entry.getValue())); + assertEquals(delegatingMap.get(entry.getKey()), entry.getValue()); + } + assertThrows(UnsupportedOperationException.class, () -> { + delegatingMap.put("key", 42); + }); + } + + @Test + public void checkPutNotAllowed() { + assertThrows(UnsupportedOperationException.class, () -> { + delegatingMap.put("key", 42); + }); + } + @Test + public void checkClearNotAllowed() { + assertThrows(UnsupportedOperationException.class, () -> { + delegatingMap.clear(); + }); + } + @Test + public void checkRemoveNotAllowed() { + assertThrows(UnsupportedOperationException.class, () -> { + delegatingMap.remove("a"); + }); + } + @Test + public void checkPutAllNotAllowed() { + assertThrows(UnsupportedOperationException.class, () -> { + delegatingMap.putAll(Stream.of(Map.entry("c", 42)) + .collect(toMap(HashMap::new, Map.Entry::getKey, Map.Entry::getValue))); + }); + } +} diff --git a/src/test/java/net/woggioni/jwo/internal/LazyValueTest.java b/src/test/java/net/woggioni/jwo/internal/LazyValueTest.java new file mode 100644 index 0000000..e657cfa --- /dev/null +++ b/src/test/java/net/woggioni/jwo/internal/LazyValueTest.java @@ -0,0 +1,38 @@ +package net.woggioni.jwo.internal; + +import net.woggioni.jwo.LazyValue; +import org.junit.jupiter.api.Test; + +import java.util.function.Supplier; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class LazyValueTest { + @Test + void unsynchronizedLazyValueTest() { + final var bool = new boolean[1]; + final Supplier supplier = () -> { + bool[0] = true; + return null; + }; + assertFalse(bool[0]); + final var lazy = LazyValue.of(supplier, LazyValue.ThreadSafetyMode.NONE); + assertFalse(bool[0]); + lazy.get(); + assertTrue(bool[0]); + final Supplier throwingSupplier = () -> { + throw new RuntimeException(); + }; + + final var throwingLazy = LazyValue.of(throwingSupplier, LazyValue.ThreadSafetyMode.NONE); + throwingLazy.handle((v, ex) -> { + assertNotNull(ex); + assertEquals(RuntimeException.class, ex.getClass()); + return null; + }); + } + +} diff --git a/src/test/java/net/woggioni/jwo/internal/TupleTest.java b/src/test/java/net/woggioni/jwo/internal/TupleTest.java new file mode 100644 index 0000000..ba8e5b1 --- /dev/null +++ b/src/test/java/net/woggioni/jwo/internal/TupleTest.java @@ -0,0 +1,58 @@ +package net.woggioni.jwo.internal; + +import net.woggioni.jwo.CollectionUtils; +import net.woggioni.jwo.MutableTuple2; +import net.woggioni.jwo.MutableTuple3; +import net.woggioni.jwo.Tuple2; +import net.woggioni.jwo.Tuple3; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TupleTest { + + @Test + void mutableTuple2Test() { + final var t1 = MutableTuple2.newInstance(1, "a"); + final var t2 = MutableTuple2.newInstance(1, "a"); + assertEquals(t1, t2); + final var s = Stream.of(t1, t2).collect(CollectionUtils.toUnmodifiableSet()); + assertEquals(1, s.size()); + t1.set_1(2); + t1.set_2("b"); + assertEquals(t1, MutableTuple2.newInstance(2, "b")); + } + + @Test + void mutableTuple3Test() { + final var t1 = MutableTuple3.newInstance(1, "a", BigDecimal.ONE); + final var t2 = MutableTuple3.newInstance(1, "a", BigDecimal.ONE); + assertEquals(t1, t2); + final var s = Stream.of(t1, t2).collect(CollectionUtils.toUnmodifiableSet()); + assertEquals(1, s.size()); + t1.set_1(2); + t1.set_2("b"); + t1.set_3(BigDecimal.ZERO); + assertEquals(t1, MutableTuple3.newInstance(2, "b", BigDecimal.ZERO)); + } + @Test + void tuple2Test() { + final var t1 = Tuple2.newInstance(1, "a"); + final var t2 = Tuple2.newInstance(1, "a"); + assertEquals(t1, t2); + final var s = Stream.of(t1, t2).collect(CollectionUtils.toUnmodifiableSet()); + assertEquals(1, s.size()); + } + + @Test + void tuple3Test() { + final var t1 = Tuple3.newInstance(1, "a", BigDecimal.ONE); + final var t2 = Tuple3.newInstance(1, "a", BigDecimal.ONE); + assertEquals(t1, t2); + final var s = Stream.of(t1, t2).collect(CollectionUtils.toUnmodifiableSet()); + assertEquals(1, s.size()); + } +}