diff --git a/build.gradle b/build.gradle index 45618ce..abfd9ae 100644 --- a/build.gradle +++ b/build.gradle @@ -72,6 +72,8 @@ test { 'junit.jupiter.engine.jar' : junitJupiterEngineJar.toString(), 'path.classloader.test.bundle' : pathClassLoaderTestBundleTask.get().outputs.files.singleFile ]) + + jvmArgs(['--add-opens', 'java.base/sun.nio.fs=ALL-UNNAMED']) } publishing { diff --git a/src/main/java/net/woggioni/jwo/CPU.java b/src/main/java/net/woggioni/jwo/CPU.java new file mode 100644 index 0000000..7ec816d --- /dev/null +++ b/src/main/java/net/woggioni/jwo/CPU.java @@ -0,0 +1,31 @@ +package net.woggioni.jwo; + +public enum CPU { + X86("x86"), + AMD64("amd64"), + ARM("arm"), + AARCH64("aarch64"), + S390("s390"), + S390X("s390x"), + RISCV("riscv"), + RISCV64("riscv64"); + + + private final String value; + + CPU(String value) { + this.value = value; + } + + public static CPU current; + + static { + String archName = System.getProperty("os.arch").toLowerCase(); + for(CPU cpu : values()) { + if(archName.startsWith(cpu.value)) { + current = cpu; + } + } + if(current == null) throw new IllegalArgumentException(String.format("Unrecognized cpu arch '%s'", archName)); + } +} \ No newline at end of file diff --git a/src/main/java/net/woggioni/jwo/CollectionUtils.java b/src/main/java/net/woggioni/jwo/CollectionUtils.java index a917893..93b0341 100644 --- a/src/main/java/net/woggioni/jwo/CollectionUtils.java +++ b/src/main/java/net/woggioni/jwo/CollectionUtils.java @@ -205,4 +205,10 @@ public class CollectionUtils { Collections::unmodifiableNavigableMap ); } + public static Stream> mapValues(Map map, Fun xform) { + return map + .entrySet() + .stream() + .map(entry -> new AbstractMap.SimpleEntry<>(entry.getKey(), xform.apply(entry.getValue()))); + } } diff --git a/src/main/java/net/woggioni/jwo/JWO.java b/src/main/java/net/woggioni/jwo/JWO.java index 1318321..67c706b 100644 --- a/src/main/java/net/woggioni/jwo/JWO.java +++ b/src/main/java/net/woggioni/jwo/JWO.java @@ -2,19 +2,58 @@ package net.woggioni.jwo; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import net.woggioni.jwo.exception.ChildProcessException; +import net.woggioni.jwo.internal.CharFilterReader; -import javax.net.ssl.*; -import java.io.*; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +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.Constructor; +import java.nio.channels.Channels; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileAttribute; +import java.security.DigestOutputStream; +import java.security.MessageDigest; import java.security.cert.X509Certificate; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.MissingFormatArgumentException; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.regex.Pattern; +import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; import java.util.zip.CRC32; @@ -169,10 +208,6 @@ public class JWO { return stream.map(mappingFunction).filter(Optional::isPresent).map(Optional::get); } - public static Stream optional2Stream(Optional optional) { - return optional.map(Stream::of).orElse(Stream.empty()); - } - public static void setSystemPropertyIfNotDefined(String key, String value) { if (System.getProperty(key) == null) { System.setProperty(key, value); @@ -240,11 +275,7 @@ public class JWO { } public static Stream streamCat(Stream... streams) { - Stream result = Stream.empty(); - for (Stream s : streams) { - result = Stream.concat(result, s); - } - return result; + return Stream.of(streams).flatMap(Function.identity()); } /** @@ -562,6 +593,9 @@ public class JWO { public static Stream optional2Stream(Iterable> opts) { return iterable2Stream(opts).filter(Optional::isPresent).map(Optional::get); } + public static Stream optional2Stream(Optional optional) { + return optional.map(Stream::of).orElse(Stream.empty()); + } public static String decapitalize(String s, Locale locale) { if (!s.isEmpty() && !Character.isLowerCase(s.charAt(0))) @@ -613,4 +647,228 @@ public class JWO { byte[] buffer = new byte[0x10000]; copy(is, os, buffer); } + + @SneakyThrows + public static void copy(Reader reader, Writer writer, char[] buffer) { + while (true) { + int read = reader.read(buffer); + if (read < 0) break; + writer.write(buffer, 0, read); + } + } + + public static void copy(Reader reader, Writer writer, int bufferSize) { + char[] buffer = new char[bufferSize]; + copy(reader, writer, buffer); + } + + public static void copy(Reader reader, Writer writer) { + char[] buffer = new char[0x10000]; + copy(reader, writer, buffer); + } + + + public static void waitProcess( + List cmd, + Path cwd, + Map env) { + waitProcess(cmd, cwd, env, 0, null); + } + + @SneakyThrows + public static Process startProcess( + List cmd, + Path cwd, Map env) { + ProcessBuilder pb = new ProcessBuilder(); + pb.command(cmd); + pb.inheritIO(); + pb.directory(cwd.toFile()); + pb.environment().putAll(env); + if (log.isTraceEnabled()) { + String cmdLineListString = '[' + cmd.stream().map(s -> '\'' + s + '\'').collect(Collectors.joining(", ")) + ']'; + log.trace("Starting child java process with command line: {}", cmdLineListString); + } + return pb.start(); + } + + @SneakyThrows + public static void waitProcess( + List cmd, + Path workingDirectory, + Map env, + long tout, + TimeUnit tunit) { + Process process = startProcess(cmd, workingDirectory, env); + int rc; + if (tout > 0) { + boolean finished = process.waitFor(tout, tunit); + if (finished) + rc = process.exitValue(); + else + throw newThrowable(TimeoutException.class, "Timeout waiting for process [%s]", ""); + + } else { + rc = process.waitFor(); + } + if (rc != 0) { + throw new ChildProcessException(cmd, rc); + } + } + + public static Iterator enumeration2Iterator(Enumeration enumeration) { + return new Iterator() { + @Override + public boolean hasNext() { + return enumeration.hasMoreElements(); + } + + @Override + public T next() { + return enumeration.nextElement(); + } + }; + } + + public static Optional or(Optional ...opts) { + for(Optional opt : opts) { + if(opt.isPresent()) return opt; + } + return Optional.empty(); + } + + @SneakyThrows + public static String sh(String... cmdLine) { + ProcessBuilder pb = new ProcessBuilder(); + pb.command(cmdLine); + pb.redirectInput(ProcessBuilder.Redirect.PIPE); + Process process = pb.start(); + int rc = process.waitFor(); + if (rc == 0) { + try (Reader reader = new CharFilterReader(new InputStreamReader(process.getInputStream()), '\n')) { + return readAll(reader); + } + } else { + try (Reader reader = new InputStreamReader(process.getErrorStream())) { + throw new RuntimeException(readAll(reader)); + } + } + } + + @SneakyThrows + public static long uid() { + return Long.parseLong(sh("id", "-u")); + } + + @SneakyThrows + public static void replaceFileIfDifferent(Supplier inputStreamSupplier, Path destination, FileAttribute... attrs) { + Hash hash; + try (InputStream inputStream = inputStreamSupplier.get()) { + hash = Hash.md5(inputStream); + } + replaceFileIfDifferent(inputStreamSupplier, destination, hash, attrs); + } + + public static void replaceFileIfDifferent(InputStream inputStream, Path destination, FileAttribute... attrs) { + replaceFileIfDifferent(() -> inputStream, destination, attrs); + } + + @SneakyThrows + private static void replaceFileIfDifferent( + Supplier inputStreamSupplier, + Path destination, + Hash newFileHash, + FileAttribute... attrs) { + if (Files.exists(destination)) { + Hash existingFileHash; + try (InputStream existingFileStream = Files.newInputStream(destination)) { + existingFileHash = Hash.md5(existingFileStream); + } + if (newFileHash == null) { + MessageDigest md = MessageDigest.getInstance(Hash.Algorithm.MD5.name()); + Path tmpFile = Files.createTempFile(destination.getParent(), destination.getFileName().toString(), ".tmp", attrs); + try { + try (InputStream inputStream = inputStreamSupplier.get(); + OutputStream outputStream = new DigestOutputStream(Files.newOutputStream(tmpFile), md)) { + copy(inputStream, outputStream); + } + newFileHash = new Hash(Hash.Algorithm.MD5, md.digest()); + if (!Objects.equals(existingFileHash, newFileHash)) { + Files.move(tmpFile, destination, StandardCopyOption.ATOMIC_MOVE); + } + } finally { + if (Files.exists(tmpFile)) { + Files.delete(tmpFile); + } + } + } else { + if (!Objects.equals(existingFileHash, newFileHash)) { + EnumSet opts = EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); + try (InputStream inputStream = inputStreamSupplier.get(); + OutputStream outputStream = Channels.newOutputStream(Files.newByteChannel(destination, opts, attrs))) { + copy(inputStream, outputStream); + } + if (log.isTraceEnabled()) { + log.trace("File '{}' rewritten", destination); + } + } else { + if (log.isTraceEnabled()) { + log.trace("File '{}' unchanged", destination); + } + } + } + } else { + if (log.isTraceEnabled()) { + log.trace("Creating file '{}'", destination); + } + EnumSet opts = EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE); + try (InputStream inputStream = inputStreamSupplier.get(); + OutputStream outputStream = Channels.newOutputStream(Files.newByteChannel(destination, opts, attrs))) { + copy(inputStream, outputStream); + } + } + } + + public static Fun curry1(BiFun original, T arg) { + return u -> original.apply(arg, u); + } + + public static Fun curry2(BiFun original, U arg) { + return t -> original.apply(t, arg); + } + + public static Fun curry1(BiFun original, Supplier sup) { + return u -> original.apply(sup.get(), u); + } + + public static Fun curry2(BiFun original, Supplier sup) { + return t -> original.apply(t, sup.get()); + } + + public static Stream lazyValue(Supplier valueSupplier) { + return Stream.generate(valueSupplier).limit(1); + } + + public static Supplier compose(Supplier sup, Function fun) { + return () -> fun.apply(sup.get()); + } + + public static Function compose(Function fun1, Function fun2) { + return param -> fun2.apply(fun1.apply(param)); + } + + public static Consumer compose(Function fun, Consumer con) { + return param -> con.accept(fun.apply(param)); + } + + public static Predicate compose(Function fun, Predicate pred) { + return param -> pred.test(fun.apply(param)); + } + + public static Supplier compose(Supplier fun, Predicate pred) { + return () -> pred.test(fun.get()); + } + + public static Runnable compose(Supplier sup, Consumer con) { + return () -> con.accept(sup.get()); + } } diff --git a/src/main/java/net/woggioni/jwo/JavaVersion.java b/src/main/java/net/woggioni/jwo/JavaVersion.java new file mode 100644 index 0000000..97afca6 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/JavaVersion.java @@ -0,0 +1,303 @@ +package net.woggioni.jwo; + + +import lombok.SneakyThrows; + +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; + +/** + * An enumeration of Java versions. + * Before 9: http://www.oracle.com/technetwork/java/javase/versioning-naming-139433.html + * 9+: http://openjdk.java.net/jeps/223 + */ +public enum JavaVersion { + VERSION_1_1, VERSION_1_2, VERSION_1_3, VERSION_1_4, + VERSION_1_5, VERSION_1_6, VERSION_1_7, VERSION_1_8, + VERSION_1_9, VERSION_1_10, + /** + * Java 11 major version. + * + */ + VERSION_11, + + /** + * Java 12 major version. + * + */ + VERSION_12, + + /** + * Java 13 major version. + * + */ + VERSION_13, + + /** + * Java 14 major version. + * + */ + VERSION_14, + + /** + * Java 15 major version. + * + */ + VERSION_15, + + /** + * Java 16 major version. + * + */ + VERSION_16, + + /** + * Java 17 major version. + * + */ + VERSION_17, + + /** + * Java 18 major version. + * + */ + VERSION_18, + + /** + * Java 19 major version. + * + */ + VERSION_19, + + /** + * Java 20 major version. + */ + VERSION_20, + + /** + * Higher version of Java. + */ + VERSION_HIGHER; + // 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 final String versionName; + + JavaVersion() { + this.versionName = ordinal() >= FIRST_MAJOR_VERSION_ORDINAL ? getMajorVersion() : "1." + getMajorVersion(); + } + + /** + * Converts the given object into a {@code JavaVersion}. + * + * @param value An object whose toString() value is to be converted. May be null. + * @return The version, or null if the provided value is null. + * @throws IllegalArgumentException when the provided value cannot be converted. + */ + public static JavaVersion toVersion(Object value) throws IllegalArgumentException { + if (value == null) { + return null; + } + if (value instanceof JavaVersion) { + return (JavaVersion) value; + } + if (value instanceof Integer) { + return getVersionForMajor((Integer) value); + } + + String name = value.toString(); + + int firstNonVersionCharIndex = findFirstNonVersionCharIndex(name); + + String[] versionStrings = name.substring(0, firstNonVersionCharIndex).split("\\."); + List versions = convertToNumber(name, versionStrings); + + if (isLegacyVersion(versions)) { + assertTrue(name, versions.get(1) > 0); + return getVersionForMajor(versions.get(1)); + } else { + return getVersionForMajor(versions.get(0)); + } + } + + /** + * Returns the version of the current JVM. + * + * @return The version of the current JVM. + */ + public static JavaVersion current() { + if (currentJavaVersion == null) { + currentJavaVersion = toVersion(System.getProperty("java.version")); + } + return currentJavaVersion; + } + + static void resetCurrent() { + currentJavaVersion = null; + } + + public static JavaVersion forClassVersion(int classVersion) { + return getVersionForMajor(classVersion - 44); //class file versions: 1.1 == 45, 1.2 == 46... + } + + public static JavaVersion forClass(byte[] classData) { + if (classData.length < 8) { + throw new IllegalArgumentException("Invalid class format. Should contain at least 8 bytes"); + } + 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 + * + * @since 6.0 + */ + public boolean isCompatibleWith(JavaVersion otherVersion) { + return this.compareTo(otherVersion) >= 0; + } + + @Override + public String toString() { + return versionName; + } + + public String getMajorVersion() { + return String.valueOf(ordinal() + 1); + } + + private static JavaVersion getVersionForMajor(int major) { + return major >= values().length ? JavaVersion.VERSION_HIGHER : values()[major - 1]; + } + + private static void assertTrue(String value, boolean condition) { + if (!condition) { + throw new IllegalArgumentException("Could not determine java version from '" + value + "'."); + } + } + + private static boolean isLegacyVersion(List versions) { + return 1 == versions.get(0) && versions.size() > 1; + } + + private static List convertToNumber(String value, String[] versionStrs) { + List result = new ArrayList<>(); + for (String s : versionStrs) { + assertTrue(value, !isNumberStartingWithZero(s)); + try { + result.add(Integer.parseInt(s)); + } catch (NumberFormatException e) { + assertTrue(value, false); + } + } + assertTrue(value, !result.isEmpty() && result.get(0) > 0); + return result; + } + + private static boolean isNumberStartingWithZero(String number) { + return number.length() > 1 && number.startsWith("0"); + } + + private static int findFirstNonVersionCharIndex(String s) { + assertTrue(s, s.length() != 0); + + for (int i = 0; i < s.length(); ++i) { + if (!isDigitOrPeriod(s.charAt(i))) { + assertTrue(s, i != 0); + return i; + } + } + + return s.length(); + } + + private static boolean isDigitOrPeriod(char c) { + return (c >= '0' && c <= '9') || c == '.'; + } +} \ No newline at end of file diff --git a/src/main/java/net/woggioni/jwo/LazyValue.java b/src/main/java/net/woggioni/jwo/LazyValue.java index 1c3331e..cb06c27 100644 --- a/src/main/java/net/woggioni/jwo/LazyValue.java +++ b/src/main/java/net/woggioni/jwo/LazyValue.java @@ -1,50 +1,48 @@ package net.woggioni.jwo; -import lombok.RequiredArgsConstructor; +import net.woggioni.jwo.internal.SynchronizedLazyValue; +import net.woggioni.jwo.internal.UnsynchronizedLazyValue; import java.util.Optional; -import java.util.function.Consumer; +import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Stream; -@RequiredArgsConstructor -public class LazyValue { - private final Supplier valueSupplier; +public interface LazyValue { - private final Consumer finalizer; - private final MutableTuple2 instance = MutableTuple2.newInstance(null, false); - - public LazyValue(Supplier valueSupplier) { - this(valueSupplier, null); + enum ThreadSafetyMode { + SYNCHRONIZED, NONE } - public T get() { - if(instance.get_2()) return instance.get_1(); - synchronized (instance) { - if(instance.get_2()) return instance.get_1(); - else { - T value = valueSupplier.get(); - instance.set_1(value); - instance.set_2(true); - return value; - } - } - } + T get(); + + LazyValue handle(BiFunction bifun); + + LazyValue map(Function fun); + + Stream stream(); /** - * Execute the finalized on the wrapped object, if it has been initialized, and then returns it. + * Execute the finalizer on the wrapped object, if it has been initialized, and then returns it. * It does nothing if {@link LazyValue#get()} has never been invoked. * @return the wrapped value if initialized, otherwise an empty {@link Optional} */ - public Optional close() { - T result = null; - synchronized (instance) { - if(instance.get_2()) { - instance.set_2(false); - result = instance.get_1(); - instance.set_1(null); - } + Optional close(); + + + static LazyValue of(Supplier supplier, ThreadSafetyMode locking) { + LazyValue result; + switch (locking) { + case SYNCHRONIZED: + result = new SynchronizedLazyValue<>(supplier); + break; + case NONE: + result = new UnsynchronizedLazyValue<>(supplier); + break; + default: + throw new RuntimeException("This should never happen"); } - if(result != null) finalizer.accept(result); - return Optional.ofNullable(result); + return result; } } diff --git a/src/main/java/net/woggioni/jwo/MapBuilder.java b/src/main/java/net/woggioni/jwo/MapBuilder.java new file mode 100644 index 0000000..babce72 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/MapBuilder.java @@ -0,0 +1,28 @@ +package net.woggioni.jwo; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +public class MapBuilder { + private final List> entries = new ArrayList<>(); + + public MapBuilder entry(K key, V value) { + entries.add(new AbstractMap.SimpleEntry<>(key, value)); + return this; + } + + public T build(Sup> factory, Function, T> finalizer) { + Map result = factory.get(); + for(Map.Entry entry : entries) { + result.put(entry.getKey(), entry.getValue()); + } + return finalizer.apply(result); + } + + public Map build(Sup> factory) { + return build(factory, Function.identity()); + } +} diff --git a/src/main/java/net/woggioni/jwo/OS.java b/src/main/java/net/woggioni/jwo/OS.java index e1e02a9..01e85cc 100644 --- a/src/main/java/net/woggioni/jwo/OS.java +++ b/src/main/java/net/woggioni/jwo/OS.java @@ -21,23 +21,24 @@ public enum OS { this.value = value; } - public static final OS current; - public static final boolean isUnix; - public static final boolean isWindows; - public static final boolean isMac; + public static OS current; + public static boolean isUnix; + public static boolean isWindows; + public static boolean isMac; + + public static boolean isLinux; static { - OS currentOs = null; String osName = System.getProperty("os.name").toLowerCase(); for(OS os : values()) { if(osName.startsWith(os.value)) { - currentOs = os; + current = os; } } - if(currentOs == null) throw new IllegalArgumentException(String.format("Unrecognized OS '%s'", osName)); - current = currentOs; - isUnix = Stream.of(LINUX, SOLARIS, BSD, AIX, HP_UX).collect(Collectors.toSet()).contains(current); + if(current == null) throw new IllegalArgumentException(String.format("Unrecognized OS '%s'", osName)); + isUnix = Stream.of(LINUX, SOLARIS, BSD, AIX, HP_UX, MACOS).collect(Collectors.toSet()).contains(current); isWindows = current == WINDOWS; isMac = current == MACOS; + isLinux = current == LINUX; } -} +} \ No newline at end of file diff --git a/src/main/java/net/woggioni/jwo/SortedProperties.java b/src/main/java/net/woggioni/jwo/SortedProperties.java new file mode 100644 index 0000000..88c1df7 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/SortedProperties.java @@ -0,0 +1,52 @@ +package net.woggioni.jwo; + +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +public class SortedProperties extends Properties { + + @Override + public Enumeration keys() { + Iterator it = keySet().iterator(); + return new Enumeration() { + @Override + public boolean hasMoreElements() { + return it.hasNext(); + } + + @Override + public Object nextElement() { + return it.next(); + } + }; + } + + @Override + public Set keySet() { + Enumeration enumeration = super.keys(); + + TreeSet sortedSet = new TreeSet<>(); + while(enumeration.hasMoreElements()) { + String key = (String) enumeration.nextElement(); + sortedSet.add(key); + } + return (Set) sortedSet; + } + + @Override + public Set> entrySet() { + Enumeration enumeration = keys(); + + TreeMap sortedMap = new TreeMap<>(); + while(enumeration.hasMoreElements()) { + String key = (String) enumeration.nextElement(); + sortedMap.put(key, get(key)); + } + return (Set) sortedMap.entrySet(); + } +} \ No newline at end of file diff --git a/src/main/java/net/woggioni/jwo/exception/ChildProcessException.java b/src/main/java/net/woggioni/jwo/exception/ChildProcessException.java new file mode 100644 index 0000000..f42a843 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/exception/ChildProcessException.java @@ -0,0 +1,19 @@ +package net.woggioni.jwo.exception; + +import net.woggioni.jwo.JWO; + +import java.util.stream.Collectors; + +public class ChildProcessException extends RuntimeException { + + private static final String cmdLine2str(Iterable cmdLine) { + return JWO.iterable2Stream(cmdLine) + .map(it -> it.replace("\"", "\\\"")) + .map(it -> "\"" + it + "\"") + .collect(Collectors.joining(", ")); + } + + public ChildProcessException(Iterable cmdline, int exitCode) { + super(String.format("Child process [%s] terminated with exit code %d", cmdLine2str(cmdline), exitCode)); + } +} diff --git a/src/main/java/net/woggioni/jwo/internal/ByteFilterInputStream.java b/src/main/java/net/woggioni/jwo/internal/ByteFilterInputStream.java new file mode 100644 index 0000000..83ced88 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/internal/ByteFilterInputStream.java @@ -0,0 +1,54 @@ +package net.woggioni.jwo.internal; + +import net.woggioni.jwo.CollectionUtils; +import net.woggioni.jwo.JWO; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Set; + +public class ByteFilterInputStream extends FilterInputStream { + + private boolean finished = false; + + private final Set forbidden; + + public ByteFilterInputStream(InputStream source, Iterable filteredChars) { + super(source); + forbidden = JWO.iterable2Stream(filteredChars).collect(CollectionUtils.toUnmodifiableTreeSet()); + } + + + @Override + public int read() throws IOException { + while(true) { + int res = super.read(); + if(!forbidden.contains(res)) { + return res; + } + } + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if(finished) return -1; + int i = 0; + int lim = Math.min(len, b.length - off); + while(i < lim) { + int c = read(); + if(c < 0) { + break; + } + b[off + i] = (byte) c; + ++i; + } + if(i == 0) finished = true; + return i == 0 ? -1 : i; + } +} diff --git a/src/main/java/net/woggioni/jwo/internal/CharFilterReader.java b/src/main/java/net/woggioni/jwo/internal/CharFilterReader.java new file mode 100644 index 0000000..48ed402 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/internal/CharFilterReader.java @@ -0,0 +1,66 @@ +package net.woggioni.jwo.internal; + +import net.woggioni.jwo.CollectionUtils; +import net.woggioni.jwo.JWO; + +import java.io.FilterReader; +import java.io.IOException; +import java.io.Reader; +import java.nio.CharBuffer; +import java.util.Arrays; +import java.util.Set; + +public class CharFilterReader extends FilterReader { + + private boolean finished = false; + + private final Set forbidden; + + public CharFilterReader(Reader source, Iterable filteredChars) { + super(source); + forbidden = JWO.iterable2Stream(filteredChars).collect(CollectionUtils.toUnmodifiableTreeSet()); + } + + public CharFilterReader(Reader source, Character ...filteredChars) { + super(source); + forbidden = Arrays.stream(filteredChars).collect(CollectionUtils.toUnmodifiableTreeSet()); + } + + + @Override + public int read() throws IOException { + while(true) { + int res = super.read(); + if(!forbidden.contains((char) res)) { + return res; + } + } + } + + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + if(finished) return -1; + int i = 0; + int lim = Math.min(len, cbuf.length - off); + while(i < lim) { + int c = read(); + if(c < 0) { + break; + } + cbuf[off + i] = (char) c; + ++i; + } + if(i == 0) finished = true; + return i == 0 ? -1 : i; + } + + @Override + public int read(CharBuffer target) throws IOException { + return read(target.array()); + } + + @Override + public int read(char[] cbuf) throws IOException { + return read(cbuf, 0, cbuf.length); + } +} diff --git a/src/main/java/net/woggioni/jwo/internal/SynchronizedLazyValue.java b/src/main/java/net/woggioni/jwo/internal/SynchronizedLazyValue.java new file mode 100644 index 0000000..86a416d --- /dev/null +++ b/src/main/java/net/woggioni/jwo/internal/SynchronizedLazyValue.java @@ -0,0 +1,77 @@ +package net.woggioni.jwo.internal; + +import lombok.RequiredArgsConstructor; +import net.woggioni.jwo.LazyValue; +import net.woggioni.jwo.MutableTuple2; + +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + +@RequiredArgsConstructor +public class SynchronizedLazyValue implements LazyValue { + private final Supplier valueSupplier; + + private final Consumer finalizer; + private final MutableTuple2 instance = MutableTuple2.newInstance(null, false); + + public SynchronizedLazyValue(Supplier valueSupplier) { + this(valueSupplier, null); + } + + @Override + public LazyValue map(Function fun) { + return new SynchronizedLazyValue<>(() -> fun.apply(get())); + } + + public Stream stream() { + return Stream.generate(this::get).limit(1); + } + + @Override + public T get() { + if(instance.get_2()) return instance.get_1(); + synchronized (instance) { + if(instance.get_2()) return instance.get_1(); + else { + T value = valueSupplier.get(); + instance.set_1(value); + instance.set_2(true); + return value; + } + } + } + + @Override + public SynchronizedLazyValue handle(BiFunction bicon) { + return new SynchronizedLazyValue<>(() -> { + try { + return bicon.apply(get(), null); + } catch (Throwable t) { + return bicon.apply(null, t); + } + }); + } + + /** + * Execute the finalizer on the wrapped object, if it has been initialized, and then returns it. + * It does nothing if {@link SynchronizedLazyValue#get()} has never been invoked. + * @return the wrapped value if initialized, otherwise an empty {@link Optional} + */ + @Override + public Optional close() { + T result = null; + synchronized (instance) { + if(instance.get_2()) { + instance.set_2(false); + result = instance.get_1(); + instance.set_1(null); + } + } + if(result != null) finalizer.accept(result); + return Optional.ofNullable(result); + } +} diff --git a/src/main/java/net/woggioni/jwo/internal/UnsynchronizedLazyValue.java b/src/main/java/net/woggioni/jwo/internal/UnsynchronizedLazyValue.java new file mode 100644 index 0000000..ecc331c --- /dev/null +++ b/src/main/java/net/woggioni/jwo/internal/UnsynchronizedLazyValue.java @@ -0,0 +1,72 @@ +package net.woggioni.jwo.internal; + +import lombok.RequiredArgsConstructor; +import net.woggioni.jwo.LazyValue; +import net.woggioni.jwo.MutableTuple2; + +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + +@RequiredArgsConstructor +public class UnsynchronizedLazyValue implements LazyValue { + private final Supplier valueSupplier; + + private final Consumer finalizer; + private final MutableTuple2 instance = MutableTuple2.newInstance(null, false); + + public UnsynchronizedLazyValue(Supplier valueSupplier) { + this(valueSupplier, null); + } + + @Override + public LazyValue map(Function fun) { + return new UnsynchronizedLazyValue<>(() -> fun.apply(get())); + } + + public Stream stream() { + return Stream.generate(this::get).limit(1); + } + + @Override + public T get() { + if(instance.get_2()) return instance.get_1(); + else { + T value = valueSupplier.get(); + instance.set_1(value); + instance.set_2(true); + return value; + } + } + + @Override + public UnsynchronizedLazyValue handle(BiFunction bicon) { + return new UnsynchronizedLazyValue<>(() -> { + try { + return bicon.apply(get(), null); + } catch (Throwable t) { + return bicon.apply(null, t); + } + }); + } + + /** + * Execute the finalizer on the wrapped object, if it has been initialized, and then returns it. + * It does nothing if {@link UnsynchronizedLazyValue#get()} has never been invoked. + * @return the wrapped value if initialized, otherwise an empty {@link Optional} + */ + @Override + public Optional close() { + T result = null; + if(instance.get_2()) { + instance.set_2(false); + result = instance.get_1(); + instance.set_1(null); + } + if(result != null) finalizer.accept(result); + return Optional.ofNullable(result); + } +} diff --git a/src/main/java9/module-info.java b/src/main/java9/module-info.java index 43be8b9..1a7a9cb 100644 --- a/src/main/java9/module-info.java +++ b/src/main/java9/module-info.java @@ -1,4 +1,5 @@ module net.woggioni.jwo { requires org.slf4j; exports net.woggioni.jwo; + exports net.woggioni.jwo.exception; } \ No newline at end of file diff --git a/src/test/java/net/woggioni/jwo/JWOTest.java b/src/test/java/net/woggioni/jwo/JWOTest.java index d218a83..de98aad 100644 --- a/src/test/java/net/woggioni/jwo/JWOTest.java +++ b/src/test/java/net/woggioni/jwo/JWOTest.java @@ -4,8 +4,16 @@ import lombok.SneakyThrows; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; 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 java.io.*; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.UserPrincipal; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -82,4 +90,18 @@ public class JWOTest { } return formatter.toString(); } + + @Test + @SneakyThrows + @EnabledOnOs(OS.LINUX) + public void uidTest(@TempDir Path testDir) { + PosixFileAttributes pfa = Files.readAttributes(testDir, PosixFileAttributes.class); + UserPrincipal expectedUser = pfa.owner(); + Class userClass = expectedUser.getClass(); + Method m = userClass.getDeclaredMethod("uid"); + m.setAccessible(true); + int expectedUserId = (Integer) m.invoke(expectedUser); + int uid = (int) JWO.uid(); + Assertions.assertEquals(expectedUserId, uid); + } }