diff --git a/Jenkinsfile b/Jenkinsfile index d4b2c07..633e781 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -19,7 +19,7 @@ pipeline { classPattern: '**/build/classes/java/main', sourcePattern: '**/src/main' ) - javadoc javadocDir: "build/docs/javadoc", keepAll: true + //javadoc javadocDir: "build/docs/javadoc", keepAll: true archiveArtifacts artifacts: '**/build/libs/*.jar', allowEmptyArchive: false, fingerprint: true, diff --git a/build.gradle b/build.gradle index 583a822..f784598 100644 --- a/build.gradle +++ b/build.gradle @@ -17,12 +17,11 @@ allprojects { } mavenCentral() } - pluginManager.withPlugin('java-library') { java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(21) vendor = JvmVendorSpec.GRAAL_VM } } @@ -63,7 +62,7 @@ allprojects { pluginManager.withPlugin('java-library') { java { - withJavadocJar() +// withJavadocJar() withSourcesJar() } } diff --git a/gradle.properties b/gradle.properties index 73c0908..43f2691 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -jwo.version = 2024.01.29 +jwo.version = 2024.01.31 lys.version = 2024.01.29 guice.version = 5.0.1 diff --git a/jmath-benchmark/build.gradle b/jmath-benchmark/build.gradle index 851256f..6dd8386 100644 --- a/jmath-benchmark/build.gradle +++ b/jmath-benchmark/build.gradle @@ -16,7 +16,7 @@ dependencies { implementation project(':jmath') } -envelopeJar { +application { mainClass = 'net.woggioni.jmath.benchmark.Main' } diff --git a/jmath-benchmark/native-image/resource-config.json b/jmath-benchmark/native-image/resource-config.json index 791ea0f..bfa2585 100644 --- a/jmath-benchmark/native-image/resource-config.json +++ b/jmath-benchmark/native-image/resource-config.json @@ -1,5 +1,7 @@ { "resources":{ - "includes":[]}, + "includes":[{ + "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" + }]}, "bundles":[] } diff --git a/jmath-benchmark/src/main/java/net/woggioni/jmath/benchmark/Main.java b/jmath-benchmark/src/main/java/net/woggioni/jmath/benchmark/Main.java index 867810c..71bb725 100644 --- a/jmath-benchmark/src/main/java/net/woggioni/jmath/benchmark/Main.java +++ b/jmath-benchmark/src/main/java/net/woggioni/jmath/benchmark/Main.java @@ -24,10 +24,15 @@ public class Main { Matrix mtx = Matrix.of(numericTypeFactory, size, size, init); Matrix lu = mtx.clone(); Matrix.Pivot pivot = lu.lup(); - IntFunction initVector = (i) -> Rational.of(rnd.nextInt(0, size), size); - Vector b = Vector.of(numericTypeFactory, size, initVector); - Vector x = lu.luSolve(b, pivot); - Vector error = mtx.mmul(x).sub(b); - System.out.println(error.norm()); + for(int i = 0; i < size; i++) { + IntFunction initVector = (j) -> Rational.of(rnd.nextInt(0, size), size); + Vector b = Vector.of(numericTypeFactory, size, initVector); + Vector x = lu.luSolve(b, pivot); + Vector error = mtx.mmul(x).sub(b); + Rational norm = error.norm(); + if(norm.compareTo(Rational.ZERO) != 0) { + throw new RuntimeException(String.format("Error is %s", norm)); + } + } } } diff --git a/src/main/java/net/woggioni/jwo/ChannerWriter.java b/src/main/java/net/woggioni/jwo/ChannerWriter.java new file mode 100644 index 0000000..09e6065 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/ChannerWriter.java @@ -0,0 +1,36 @@ +package net.woggioni.jwo; + +import lombok.RequiredArgsConstructor; + +import java.io.IOException; +import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.Charset; + +@RequiredArgsConstructor +public class ChannerWriter extends Writer { + + private final WritableByteChannel ch; + + private final Charset charset; + + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + write(new String(cbuf, off, len)); + } + + @Override + public void write(String str) throws IOException { + ch.write(ByteBuffer.wrap(str.getBytes(charset))); + } + + @Override + public void flush() {} + + @Override + public void close() throws IOException { + ch.close(); + } +} diff --git a/src/main/java/net/woggioni/jwo/CollectionUtils.java b/src/main/java/net/woggioni/jwo/CollectionUtils.java index b759c4a..cfc73f6 100644 --- a/src/main/java/net/woggioni/jwo/CollectionUtils.java +++ b/src/main/java/net/woggioni/jwo/CollectionUtils.java @@ -25,6 +25,10 @@ import java.util.stream.Stream; public class CollectionUtils { + public enum MapMergeStrategy { + THROW, REPLACE, KEEP + } + @SafeVarargs public static ArrayList newArrayList(T... args) { return new ArrayList<>(Arrays.asList(args)); @@ -125,18 +129,43 @@ public class CollectionUtils { }; } - public static T throwingMerger(T v1, T v2) { - throw new IllegalStateException(String.format("Duplicate key %s", v1)); + private static BinaryOperator throwingMerger() { + return (v1, v2) -> { + throw new IllegalStateException(String.format("Duplicate key %s", v1)); + }; + } + private static BinaryOperator updatingMerger() { + return (v1, v2) -> v2; } - public static T oldValueMerger(T oldValue, T newValue) { - return oldValue; + private static BinaryOperator conservativeMerger() { + return (v1, v2) -> v1; } - public static T newValueMerger(T oldValue, T newValue) { - return newValue; + private static BinaryOperator getMerger(MapMergeStrategy mapMergeStrategy) { + BinaryOperator result; + switch (mapMergeStrategy) { + case KEEP: + result = conservativeMerger(); + break; + case THROW: + result = throwingMerger(); + break; + case REPLACE: + result = updatingMerger(); + break; + default: + throw new NullPointerException(); + } + return result; } + public static Collector> toUnmodifiableHashMap( + Function keyExtractor, + Function valueExtractor, + MapMergeStrategy mapMergeStrategy) { + return toUnmodifiableMap(HashMap::new, keyExtractor, valueExtractor, mapMergeStrategy); + } public static Collector> toUnmodifiableHashMap( Function keyExtractor, Function valueExtractor) { @@ -149,6 +178,19 @@ public class CollectionUtils { return toUnmodifiableNavigableMap(TreeMap::new, keyExtractor, valueExtractor); } + public static Collector> toUnmodifiableTreeMap( + Function keyExtractor, + Function valueExtractor, + MapMergeStrategy mapMergeStrategy) { + return toUnmodifiableNavigableMap(TreeMap::new, keyExtractor, valueExtractor, mapMergeStrategy); + } + public static Collector> toUnmodifiableTreeMap( + Function keyExtractor, + Function valueExtractor, + Comparator comparator, + MapMergeStrategy mapMergeStrategy) { + return toUnmodifiableNavigableMap(() -> new TreeMap<>(comparator), keyExtractor, valueExtractor, mapMergeStrategy); + } public static Collector> toUnmodifiableTreeMap( Function keyExtractor, Function valueExtractor, @@ -156,6 +198,12 @@ public class CollectionUtils { return toUnmodifiableNavigableMap(() -> new TreeMap<>(comparator), keyExtractor, valueExtractor); } + public static Collector> toTreeMap( + Function keyExtractor, + Function valueExtractor, + MapMergeStrategy mapMergeStrategy) { + return toNavigableMap(TreeMap::new, keyExtractor, valueExtractor, mapMergeStrategy); + } public static Collector> toTreeMap( Function keyExtractor, Function valueExtractor) { @@ -169,6 +217,14 @@ public class CollectionUtils { return toNavigableMap(() -> new TreeMap<>(comparator), keyExtractor, valueExtractor); } + public static Collector> toTreeMap( + Function keyExtractor, + Function valueExtractor, + Comparator comparator, + MapMergeStrategy mapMergeStrategy) { + return toNavigableMap(() -> new TreeMap<>(comparator), keyExtractor, valueExtractor, mapMergeStrategy); + } + public static > Collector toNavigableMap( Supplier constructor, Function keyExtractor, @@ -177,7 +233,7 @@ public class CollectionUtils { constructor, keyExtractor, valueExtractor, - CollectionUtils::throwingMerger + MapMergeStrategy.THROW ); } @@ -185,7 +241,8 @@ public class CollectionUtils { Supplier constructor, Function keyExtractor, Function valueExtractor, - BinaryOperator valueMerger) { + MapMergeStrategy mapMergeStrategy) { + BinaryOperator valueMerger = getMerger(mapMergeStrategy); BiConsumer accumulator = (map, streamElement) -> { map.merge(keyExtractor.apply(streamElement), valueExtractor.apply(streamElement), valueMerger @@ -202,14 +259,15 @@ public class CollectionUtils { Supplier> constructor, Function keyExtractor, Function valueExtractor) { - return toMap(constructor, keyExtractor, valueExtractor, CollectionUtils::throwingMerger); + return toMap(constructor, keyExtractor, valueExtractor, MapMergeStrategy.THROW); } public static Collector> toMap( Supplier> constructor, Function keyExtractor, Function valueExtractor, - BinaryOperator valueMerger) { + MapMergeStrategy mapMergeStrategy) { + BinaryOperator valueMerger = getMerger(mapMergeStrategy); BiConsumer, T> accumulator = (map, streamElement) -> { map.merge(keyExtractor.apply(streamElement), valueExtractor.apply(streamElement), valueMerger @@ -226,13 +284,14 @@ public class CollectionUtils { Supplier> constructor, Function keyExtractor, Function valueExtractor) { - return toUnmodifiableMap(constructor, keyExtractor, valueExtractor, CollectionUtils::throwingMerger); + return toUnmodifiableMap(constructor, keyExtractor, valueExtractor, MapMergeStrategy.THROW); } public static Collector> toUnmodifiableMap( Supplier> constructor, Function keyExtractor, Function valueExtractor, - BinaryOperator valueMerger) { + MapMergeStrategy mapMergeStrategy) { + BinaryOperator valueMerger = getMerger(mapMergeStrategy); BiConsumer, T> accumulator = (map, streamElement) -> { map.merge(keyExtractor.apply(streamElement), valueExtractor.apply(streamElement), @@ -251,8 +310,9 @@ public class CollectionUtils { Supplier> constructor, Function keyExtractor, Function valueExtractor, - BinaryOperator valueMerger + MapMergeStrategy mapMergeStrategy ) { + BinaryOperator valueMerger = getMerger(mapMergeStrategy); BiConsumer, T> accumulator = (map, streamElement) -> { map.merge( keyExtractor.apply(streamElement), @@ -271,7 +331,7 @@ public class CollectionUtils { Supplier> constructor, Function keyExtractor, Function valueExtractor) { - return toUnmodifiableNavigableMap(constructor, keyExtractor, valueExtractor, CollectionUtils::throwingMerger); + return toUnmodifiableNavigableMap(constructor, keyExtractor, valueExtractor, MapMergeStrategy.THROW); } public static Stream> mapValues(Map map, Fun xform) { return map diff --git a/src/main/java/net/woggioni/jwo/JWO.java b/src/main/java/net/woggioni/jwo/JWO.java index 141d551..1e12abe 100644 --- a/src/main/java/net/woggioni/jwo/JWO.java +++ b/src/main/java/net/woggioni/jwo/JWO.java @@ -4,6 +4,7 @@ import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import net.woggioni.jwo.exception.ChildProcessException; import net.woggioni.jwo.internal.CharFilterReader; +import org.slf4j.Logger; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; @@ -12,9 +13,7 @@ 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; @@ -25,6 +24,8 @@ import java.io.Reader; import java.io.StringWriter; import java.io.Writer; import java.lang.reflect.Constructor; +import java.net.URL; +import java.net.URLStreamHandlerFactory; import java.nio.channels.Channels; import java.nio.file.Files; import java.nio.file.Path; @@ -35,6 +36,8 @@ import java.nio.file.attribute.FileAttribute; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.cert.X509Certificate; +import java.util.AbstractMap; +import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.EnumSet; @@ -69,6 +72,10 @@ import java.util.zip.ZipOutputStream; @Slf4j public class JWO { + + private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs"; + private static final String HANDLERS_PACKAGE = "net.woggioni.jwo.url"; + public static Stream iterable2Stream(Iterable iterable) { return StreamSupport.stream(iterable.spliterator(), false); } @@ -1004,4 +1011,100 @@ public class JWO { } public void run() { executor.execute(action); } } + + public static String toUnixPath(Path path) { + String result; + if (OS.isUnix) { + result = path.toString(); + } else { + result = (path.isAbsolute() ? "/" : "") + + iterable2Stream(path) + .map(Path::toString) + .collect(Collectors.joining("/")); + } + return result; + } + + public static void registerUrlProtocolHandler() { + String handlers = System.getProperty(PROTOCOL_HANDLER, ""); + System.setProperty(PROTOCOL_HANDLER, + ((handlers == null || handlers.isEmpty()) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE)); + resetCachedUrlHandlers(); + } + + /** + * Reset any cached handlers just in case a jar protocol has already been used. We + * reset the handler by trying to set a null {@link URLStreamHandlerFactory} which + * should have no effect other than clearing the handlers cache. + */ + private static void resetCachedUrlHandlers() { + try { + URL.setURLStreamHandlerFactory(null); + } catch (Error ex) { + // Ignore + } + } + + @SneakyThrows + public static void deletePath(Logger log, Path path) { + if (Files.exists(path)) { + if (log.isInfoEnabled()) { + log.info("Wiping '{}'", path); + } + deletePath(path); + } + } + + public static U downCast(T param) { + return (U) param; + } + + public static U upCast(T param) { + return param; + } + + public static Optional asInstance(T param, Class cls) { + return Optional.ofNullable(param) + .filter(cls::isInstance) + .map(cls::cast); + } + + public static Stream> mapValues(Map map, Fun xform) { + return map + .entrySet() + .stream() + .map(entry -> new AbstractMap.SimpleEntry<>(entry.getKey(), xform.apply(entry.getValue()))); + } + + @SneakyThrows + public static Process startJavaProcess( + Logger log, + Path javaHome, + List args, + Path cwd, + Map env) { + Path javaExecutable = javaHome.resolve("bin/java" + (OS.isWindows ? ".exe" : "")); + ProcessBuilder pb = new ProcessBuilder(); + List cmd = new ArrayList<>(); + cmd.add(javaExecutable.toString()); + cmd.addAll(args); + 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(); + } + + public static U let(T object, Function cb) { + return cb.apply(object); + } + + public static T also(T object, Consumer cb) { + cb.accept(object); + return object; + } } diff --git a/src/main/java/net/woggioni/jwo/LazyOptional.java b/src/main/java/net/woggioni/jwo/LazyOptional.java new file mode 100644 index 0000000..6874983 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/LazyOptional.java @@ -0,0 +1,77 @@ +package net.woggioni.jwo; + + +import lombok.RequiredArgsConstructor; + +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +@RequiredArgsConstructor +public class LazyOptional { + private static final LazyOptional empty = new LazyOptional<>(() -> null); + + private final Supplier producer; + private final MutableTuple2 instance = MutableTuple2.newInstance(null, false); + + public static LazyOptional of(Supplier producer) { + return new LazyOptional<>(producer); + } + + public static LazyOptional or(LazyOptional... opts) { + return LazyOptional.of(() -> { + for (LazyOptional opt : opts) { + U value = opt.get(); + if (value != null) return value; + } + return null; + } + ); + } + + public static LazyOptional empty() { + return (LazyOptional) empty; + } + + public LazyOptional map(Function mapping) { + return LazyOptional.of(() -> { + T prevValue = producer.get(); + if (prevValue == null) return null; + else return mapping.apply(prevValue); + }); + } + + public LazyOptional filter(Predicate predicate) { + return LazyOptional.of(() -> { + T prevValue = producer.get(); + if (predicate.test(prevValue)) return prevValue; + else return null; + }); + } + + public T get() { + if (instance.get_2()) return instance.get_1(); + synchronized (instance) { + if (instance.get_2()) return instance.get_1(); + else { + T value = producer.get(); + instance.set_1(value); + instance.set_2(true); + return value; + } + } + } + + public LazyOptional flatMap(Function> mapping) { + return new LazyOptional<>(() -> { + T prevValue = producer.get(); + if (prevValue == null) return null; + else return mapping.apply(prevValue).get(); + }); + } + + public Optional getOptional() { + return Optional.ofNullable(get()); + } +} diff --git a/src/main/java/net/woggioni/jwo/LazyValue.java b/src/main/java/net/woggioni/jwo/LazyValue.java index cb06c27..8c155f8 100644 --- a/src/main/java/net/woggioni/jwo/LazyValue.java +++ b/src/main/java/net/woggioni/jwo/LazyValue.java @@ -1,10 +1,12 @@ 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.BiFunction; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; @@ -30,19 +32,22 @@ public interface LazyValue { */ Optional close(); - - static LazyValue of(Supplier supplier, ThreadSafetyMode locking) { + static LazyValue of(Supplier supplier, ThreadSafetyMode locking, Consumer finalizer) { LazyValue result; switch (locking) { case SYNCHRONIZED: - result = new SynchronizedLazyValue<>(supplier); + result = new SynchronizedLazyValue<>(supplier, finalizer); break; case NONE: - result = new UnsynchronizedLazyValue<>(supplier); + result = new UnsynchronizedLazyValue<>(supplier, finalizer); break; default: throw new RuntimeException("This should never happen"); } return result; } + + static LazyValue of(Supplier supplier, ThreadSafetyMode locking) { + return of(supplier, locking, null); + } } diff --git a/src/main/java/net/woggioni/jwo/LockFile.java b/src/main/java/net/woggioni/jwo/LockFile.java index 3d1def3..f5e2343 100644 --- a/src/main/java/net/woggioni/jwo/LockFile.java +++ b/src/main/java/net/woggioni/jwo/LockFile.java @@ -1,39 +1,40 @@ package net.woggioni.jwo; -import lombok.SneakyThrows; - -import java.io.RandomAccessFile; +import java.io.Closeable; +import java.io.IOException; +import java.nio.channels.FileChannel; import java.nio.channels.FileLock; -import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.EnumSet; -public class LockFile implements AutoCloseable { +public class LockFile implements Closeable { - private final Path path; private final FileLock lock; - private final RandomAccessFile randomAccessFile; - public LockFile(Path path) { - this(path, false); + private static FileChannel openFileChannel(Path path) throws IOException { + Files.createDirectories(path.getParent()); + return FileChannel.open(path, EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE)); } - @SneakyThrows - public LockFile(Path path, boolean shared) { - this.path = path; - try { - Files.createDirectories(path.getParent()); - Files.createFile(path); - } catch(FileAlreadyExistsException faee) { - } - randomAccessFile = new RandomAccessFile(path.toFile(), "rw"); - lock = randomAccessFile.getChannel().lock(0L, Long.MAX_VALUE, shared); + public static LockFile acquire(Path path, boolean shared) throws IOException { + FileChannel channel = openFileChannel(path); + return new LockFile(channel.lock(0L, Long.MAX_VALUE, shared)); + } + + public static LockFile tryAcquire(Path path, boolean shared) throws IOException { + FileChannel channel = openFileChannel(path); + FileLock lock = channel.tryLock(0L, Long.MAX_VALUE, shared); + return (lock != null) ? new LockFile(lock) : null; + } + + private LockFile(FileLock lock) { + this.lock = lock; } @Override - @SneakyThrows - public void close() { - lock.release(); - randomAccessFile.close(); + public void close() throws IOException { + lock.channel().close(); } } diff --git a/src/main/java/net/woggioni/jwo/MapBuilder.java b/src/main/java/net/woggioni/jwo/MapBuilder.java index babce72..9ffaf98 100644 --- a/src/main/java/net/woggioni/jwo/MapBuilder.java +++ b/src/main/java/net/woggioni/jwo/MapBuilder.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Function; +import java.util.stream.Stream; public class MapBuilder { private final List> entries = new ArrayList<>(); @@ -22,7 +23,15 @@ public class MapBuilder { return finalizer.apply(result); } + public Stream> stream() { + return entries.stream(); + } + public Map build(Sup> factory) { return build(factory, Function.identity()); } + + public static MapBuilder with(K key, V value) { + return new MapBuilder().entry(key, value); + } } diff --git a/src/main/java/net/woggioni/jwo/MavenVersion.java b/src/main/java/net/woggioni/jwo/MavenVersion.java new file mode 100644 index 0000000..1229d0d --- /dev/null +++ b/src/main/java/net/woggioni/jwo/MavenVersion.java @@ -0,0 +1,701 @@ +package net.woggioni.jwo; + +import java.math.BigInteger; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Properties; + +/** + *

+ * Generic implementation of version comparison. Shamelessly borrowed from + * https://github.com/apache/maven/blob/master/maven-artifact/src/main/java/org/apache/maven/artifact/versioning/ComparableVersion.java + *

+ * + * Features: + *
    + *
  • mixing of '-' (hyphen) and '.' (dot) separators,
  • + *
  • transition between characters and digits also constitutes a separator: + * 1.0alpha1 => [1, 0, alpha, 1]
  • + *
  • unlimited number of version components,
  • + *
  • version components in the text can be digits or strings,
  • + *
  • strings are checked for well-known qualifiers and the qualifier ordering is used for version ordering. + * Well-known qualifiers (case insensitive) are:
      + *
    • alpha or a
    • + *
    • beta or b
    • + *
    • milestone or m
    • + *
    • rc or cr
    • + *
    • snapshot
    • + *
    • (the empty string) or ga or final
    • + *
    • sp
    • + *
    + * Unknown qualifiers are considered after known qualifiers, with lexical order (always case insensitive), + *
  • + *
  • a hyphen usually precedes a qualifier, and is always less important than digits/number, for example + * {@code 1.0.RC2 < 1.0-RC3 < 1.0.1}; but prefer {@code 1.0.0-RC1} over {@code 1.0.0.RC1}, and more + * generally: {@code 1.0.X2 < 1.0-X3 < 1.0.1} for any string {@code X}; but prefer {@code 1.0.0-X1} + * over {@code 1.0.0.X1}.
  • + *
+ * + * @see "Versioning" on Maven Wiki + * @author Kenney Westerhof + * @author Hervè Boutemy + */ +public class MavenVersion implements Comparable { + private static final int MAX_INTITEM_LENGTH = 9; + + private static final int MAX_LONGITEM_LENGTH = 18; + + private String value; + + private String canonical; + + private ListItem items; + + private interface Item { + int INT_ITEM = 3; + int LONG_ITEM = 4; + int BIGINTEGER_ITEM = 0; + int STRING_ITEM = 1; + int LIST_ITEM = 2; + + int compareTo(Item item); + + int getType(); + + boolean isNull(); + } + + /** + * Represents a numeric item in the version item list that can be represented with an int. + */ + private static class IntItem implements Item { + private final int value; + + public static final IntItem ZERO = new IntItem(); + + private IntItem() { + this.value = 0; + } + + IntItem(String str) { + this.value = Integer.parseInt(str); + } + + @Override + public int getType() { + return INT_ITEM; + } + + @Override + public boolean isNull() { + return value == 0; + } + + @Override + public int compareTo(Item item) { + if (item == null) { + return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1 + } + + switch (item.getType()) { + case INT_ITEM: + int itemValue = ((IntItem) item).value; + return Integer.compare(value, itemValue); + case LONG_ITEM: + case BIGINTEGER_ITEM: + return -1; + + case STRING_ITEM: + return 1; // 1.1 > 1-sp + + case LIST_ITEM: + return 1; // 1.1 > 1-1 + + default: + throw new IllegalStateException("invalid item: " + item.getClass()); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + IntItem intItem = (IntItem) o; + + return value == intItem.value; + } + + @Override + public int hashCode() { + return value; + } + + @Override + public String toString() { + return Integer.toString(value); + } + } + + /** + * Represents a numeric item in the version item list that can be represented with a long. + */ + private static class LongItem implements Item { + private final long value; + + LongItem(String str) { + this.value = Long.parseLong(str); + } + + @Override + public int getType() { + return LONG_ITEM; + } + + @Override + public boolean isNull() { + return value == 0; + } + + @Override + public int compareTo(Item item) { + if (item == null) { + return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1 + } + + switch (item.getType()) { + case INT_ITEM: + return 1; + case LONG_ITEM: + long itemValue = ((LongItem) item).value; + return Long.compare(value, itemValue); + case BIGINTEGER_ITEM: + return -1; + + case STRING_ITEM: + return 1; // 1.1 > 1-sp + + case LIST_ITEM: + return 1; // 1.1 > 1-1 + + default: + throw new IllegalStateException("invalid item: " + item.getClass()); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + LongItem longItem = (LongItem) o; + + return value == longItem.value; + } + + @Override + public int hashCode() { + return (int) (value ^ (value >>> 32)); + } + + @Override + public String toString() { + return Long.toString(value); + } + } + + /** + * Represents a numeric item in the version item list. + */ + private static class BigIntegerItem implements Item { + private final BigInteger value; + + BigIntegerItem(String str) { + this.value = new BigInteger(str); + } + + @Override + public int getType() { + return BIGINTEGER_ITEM; + } + + @Override + public boolean isNull() { + return BigInteger.ZERO.equals(value); + } + + @Override + public int compareTo(Item item) { + if (item == null) { + return BigInteger.ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1 + } + + switch (item.getType()) { + case INT_ITEM: + case LONG_ITEM: + return 1; + + case BIGINTEGER_ITEM: + return value.compareTo(((BigIntegerItem) item).value); + + case STRING_ITEM: + return 1; // 1.1 > 1-sp + + case LIST_ITEM: + return 1; // 1.1 > 1-1 + + default: + throw new IllegalStateException("invalid item: " + item.getClass()); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + BigIntegerItem that = (BigIntegerItem) o; + + return value.equals(that.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + public String toString() { + return value.toString(); + } + } + + /** + * Represents a string in the version item list, usually a qualifier. + */ + private static class StringItem implements Item { + private static final List QUALIFIERS = + Arrays.asList("alpha", "beta", "milestone", "rc", "snapshot", "", "sp"); + + private static final Properties ALIASES = new Properties(); + + static { + ALIASES.put("ga", ""); + ALIASES.put("final", ""); + ALIASES.put("release", ""); + ALIASES.put("cr", "rc"); + } + + /** + * A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes + * the version older than one without a qualifier, or more recent. + */ + private static final String RELEASE_VERSION_INDEX = String.valueOf(QUALIFIERS.indexOf("")); + + private final String value; + + StringItem(String value, boolean followedByDigit) { + if (followedByDigit && value.length() == 1) { + // a1 = alpha-1, b1 = beta-1, m1 = milestone-1 + switch (value.charAt(0)) { + case 'a': + value = "alpha"; + break; + case 'b': + value = "beta"; + break; + case 'm': + value = "milestone"; + break; + default: + } + } + this.value = ALIASES.getProperty(value, value); + } + + @Override + public int getType() { + return STRING_ITEM; + } + + @Override + public boolean isNull() { + return (comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0); + } + + /** + * Returns a comparable value for a qualifier. + * + * This method takes into account the ordering of known qualifiers then unknown qualifiers with lexical + * ordering. + * + * just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for -1 + * or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first character, + * so this is still fast. If more characters are needed then it requires a lexical sort anyway. + * + * @param qualifier + * @return an equivalent value that can be used with lexical comparison + */ + public static String comparableQualifier(String qualifier) { + int i = QUALIFIERS.indexOf(qualifier); + + return i == -1 ? (QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i); + } + + @Override + public int compareTo(Item item) { + if (item == null) { + // 1-rc < 1, 1-ga > 1 + return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX); + } + switch (item.getType()) { + case INT_ITEM: + case LONG_ITEM: + case BIGINTEGER_ITEM: + return -1; // 1.any < 1.1 ? + + case STRING_ITEM: + return comparableQualifier(value).compareTo(comparableQualifier(((StringItem) item).value)); + + case LIST_ITEM: + return -1; // 1.any < 1-1 + + default: + throw new IllegalStateException("invalid item: " + item.getClass()); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + StringItem that = (StringItem) o; + + return value.equals(that.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + public String toString() { + return value; + } + } + + /** + * Represents a version list item. This class is used both for the global item list and for sub-lists (which start + * with '-(number)' in the version specification). + */ + private static class ListItem extends ArrayList implements Item { + @Override + public int getType() { + return LIST_ITEM; + } + + @Override + public boolean isNull() { + return (size() == 0); + } + + void normalize() { + for (int i = size() - 1; i >= 0; i--) { + Item lastItem = get(i); + + if (lastItem.isNull()) { + // remove null trailing items: 0, "", empty list + remove(i); + } else if (!(lastItem instanceof ListItem)) { + break; + } + } + } + + @Override + public int compareTo(Item item) { + if (item == null) { + if (size() == 0) { + return 0; // 1-0 = 1- (normalize) = 1 + } + // Compare the entire list of items with null - not just the first one, MNG-6964 + for (Item i : this) { + int result = i.compareTo(null); + if (result != 0) { + return result; + } + } + return 0; + } + switch (item.getType()) { + case INT_ITEM: + case LONG_ITEM: + case BIGINTEGER_ITEM: + return -1; // 1-1 < 1.0.x + + case STRING_ITEM: + return 1; // 1-1 > 1-sp + + case LIST_ITEM: + Iterator left = iterator(); + Iterator right = ((ListItem) item).iterator(); + + while (left.hasNext() || right.hasNext()) { + Item l = left.hasNext() ? left.next() : null; + Item r = right.hasNext() ? right.next() : null; + + // if this is shorter, then invert the compare and mul with -1 + int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r); + + if (result != 0) { + return result; + } + } + + return 0; + + default: + throw new IllegalStateException("invalid item: " + item.getClass()); + } + } + + @Override + public String toString() { + StringBuilder buffer = new StringBuilder(); + for (Item item : this) { + if (buffer.length() > 0) { + buffer.append((item instanceof ListItem) ? '-' : '.'); + } + buffer.append(item); + } + return buffer.toString(); + } + + /** + * Return the contents in the same format that is used when you call toString() on a List. + */ + private String toListString() { + StringBuilder buffer = new StringBuilder(); + buffer.append("["); + for (Item item : this) { + if (buffer.length() > 1) { + buffer.append(", "); + } + if (item instanceof ListItem) { + buffer.append(((ListItem) item).toListString()); + } else { + buffer.append(item); + } + } + buffer.append("]"); + return buffer.toString(); + } + } + + public MavenVersion(String version) { + parseVersion(version); + } + + @SuppressWarnings("checkstyle:innerassignment") + public final void parseVersion(String version) { + this.value = version; + + items = new ListItem(); + + version = version.toLowerCase(Locale.ENGLISH); + + ListItem list = items; + + Deque stack = new ArrayDeque<>(); + stack.push(list); + + boolean isDigit = false; + + int startIndex = 0; + + for (int i = 0; i < version.length(); i++) { + char c = version.charAt(i); + + if (c == '.') { + if (i == startIndex) { + list.add(IntItem.ZERO); + } else { + list.add(parseItem(isDigit, version.substring(startIndex, i))); + } + startIndex = i + 1; + } else if (c == '-') { + if (i == startIndex) { + list.add(IntItem.ZERO); + } else { + list.add(parseItem(isDigit, version.substring(startIndex, i))); + } + startIndex = i + 1; + + list.add(list = new ListItem()); + stack.push(list); + } else if (Character.isDigit(c)) { + if (!isDigit && i > startIndex) { + // 1.0.0.X1 < 1.0.0-X2 + // treat .X as -X for any string qualifier X + if (!list.isEmpty()) { + list.add(list = new ListItem()); + stack.push(list); + } + + list.add(new StringItem(version.substring(startIndex, i), true)); + startIndex = i; + + list.add(list = new ListItem()); + stack.push(list); + } + + isDigit = true; + } else { + if (isDigit && i > startIndex) { + list.add(parseItem(true, version.substring(startIndex, i))); + startIndex = i; + + list.add(list = new ListItem()); + stack.push(list); + } + + isDigit = false; + } + } + + if (version.length() > startIndex) { + // 1.0.0.X1 < 1.0.0-X2 + // treat .X as -X for any string qualifier X + if (!isDigit && !list.isEmpty()) { + list.add(list = new ListItem()); + stack.push(list); + } + + list.add(parseItem(isDigit, version.substring(startIndex))); + } + + while (!stack.isEmpty()) { + list = (ListItem) stack.pop(); + list.normalize(); + } + } + + private static Item parseItem(boolean isDigit, String buf) { + if (isDigit) { + buf = stripLeadingZeroes(buf); + if (buf.length() <= MAX_INTITEM_LENGTH) { + // lower than 2^31 + return new IntItem(buf); + } else if (buf.length() <= MAX_LONGITEM_LENGTH) { + // lower than 2^63 + return new LongItem(buf); + } + return new BigIntegerItem(buf); + } + return new StringItem(buf, false); + } + + private static String stripLeadingZeroes(String buf) { + if (buf == null || buf.isEmpty()) { + return "0"; + } + for (int i = 0; i < buf.length(); ++i) { + char c = buf.charAt(i); + if (c != '0') { + return buf.substring(i); + } + } + return buf; + } + + @Override + public int compareTo(MavenVersion o) { + return items.compareTo(o.items); + } + + @Override + public String toString() { + return value; + } + + public String getCanonical() { + if (canonical == null) { + canonical = items.toString(); + } + return canonical; + } + + @Override + public boolean equals(Object o) { + return (o instanceof MavenVersion) && items.equals(((MavenVersion) o).items); + } + + @Override + public int hashCode() { + return items.hashCode(); + } + + // CHECKSTYLE_OFF: LineLength + /** + * Main to test version parsing and comparison. + *

+ * To check how "1.2.7" compares to "1.2-SNAPSHOT", for example, you can issue + *

java -jar ${maven.repo.local}/org/apache/maven/maven-artifact/${maven.version}/maven-artifact-${maven.version}.jar "1.2.7" "1.2-SNAPSHOT"
+ * command to command line. Result of given command will be something like this: + *
+     * Display parameters as parsed by Maven (in canonical form) and comparison result:
+     * 1. 1.2.7 == 1.2.7
+     *    1.2.7 > 1.2-SNAPSHOT
+     * 2. 1.2-SNAPSHOT == 1.2-snapshot
+     * 
+ * + * @param args the version strings to parse and compare. You can pass arbitrary number of version strings and always + * two adjacent will be compared + */ + // CHECKSTYLE_ON: LineLength + public static void main(String... args) { + System.out.println("Display parameters as parsed by Maven (in canonical form and as a list of tokens) and" + + " comparison result:"); + if (args.length == 0) { + return; + } + + MavenVersion prev = null; + int i = 1; + for (String version : args) { + MavenVersion c = new MavenVersion(version); + + if (prev != null) { + int compare = prev.compareTo(c); + System.out.println(" " + prev.toString() + ' ' + ((compare == 0) ? "==" : ((compare < 0) ? "<" : ">")) + + ' ' + version); + } + + System.out.println( + (i++) + ". " + version + " -> " + c.getCanonical() + "; tokens: " + c.items.toListString()); + + prev = c; + } + } +} \ No newline at end of file diff --git a/src/main/java/net/woggioni/jwo/Run.java b/src/main/java/net/woggioni/jwo/Run.java index 12a149e..1eab778 100644 --- a/src/main/java/net/woggioni/jwo/Run.java +++ b/src/main/java/net/woggioni/jwo/Run.java @@ -10,5 +10,5 @@ public interface Run extends Runnable { exec(); } - boolean exec() throws Throwable; + void exec() throws Throwable; } diff --git a/src/main/java/net/woggioni/jwo/SQL.java b/src/main/java/net/woggioni/jwo/SQL.java new file mode 100644 index 0000000..232f173 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/SQL.java @@ -0,0 +1,96 @@ +package net.woggioni.jwo; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import static net.woggioni.jwo.JWO.newThrowable; + +public class SQL { + + public enum Operation { + INSERT + } + + @RequiredArgsConstructor + public static class QueryBuilder { + private final Operation operation; + private final String tableName; + + private final Map>> fields = new TreeMap<>(); + + public QueryBuilder field(String name, Object value, Class cls) { + fields.put(name, Tuple2.newInstance(value, cls)); + return this; + } + + public QueryBuilder field(String name, Object value) { + if(value == null) { + throw newThrowable(IllegalArgumentException.class, "Class argument required for null value"); + } + return field(name, value, value.getClass()); + } + + @SneakyThrows + public PreparedStatement buildStatement(Connection conn) { + StringBuilder sb = new StringBuilder(); + switch (operation) { + case INSERT: + sb.append("INSERT INTO "); + sb.append(tableName); + sb.append(" ("); + int i = 0; + List>>> entries = new ArrayList<>(fields.entrySet()); + for(Map.Entry>> entry : entries) { + if(i++ > 0) sb.append(','); + sb.append(entry.getKey()); + } + sb.append(") VALUES("); + while(i-->0) { + sb.append("?"); + if(i > 0) sb.append(','); + } + sb.append(");"); + PreparedStatement stmt = conn.prepareStatement(sb.toString()); + i = 1; + for(Map.Entry>> entry : entries) { + Tuple2> tuple2 = entry.getValue(); + Object value = tuple2.get_1(); + Class cls = tuple2.get_2(); + if(cls.isAssignableFrom(String.class)) { + stmt.setString(i, (String) value); + } else if(cls.isAssignableFrom(Integer.class)) { + stmt.setInt(i, (Integer) value); + } else if(cls.isAssignableFrom(Double.class)) { + stmt.setDouble(i, (Double) value); + } else if(cls.isAssignableFrom(Float.class)) { + stmt.setFloat(i, (Float) value); + } else if(cls.isAssignableFrom(Short.class)) { + stmt.setShort(i, (Short) value); + } else if(cls.isAssignableFrom(Byte.class)) { + stmt.setByte(i, (Byte) value); + } else if(cls.isAssignableFrom(Boolean.class)) { + stmt.setBoolean(i, (Boolean) value); + } else if(cls.isAssignableFrom(Timestamp.class)) { + stmt.setTimestamp(i, (Timestamp) value); + } else { + throw newThrowable(IllegalArgumentException.class, "Class '%s' is not supported", + value.getClass()); + } + ++i; + } + return stmt; + default: + throw new RuntimeException("This should never happen"); + } + } + + } +} \ No newline at end of file diff --git a/src/main/java/net/woggioni/jwo/UncloseableWriter.java b/src/main/java/net/woggioni/jwo/UncloseableWriter.java new file mode 100644 index 0000000..0a74d53 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/UncloseableWriter.java @@ -0,0 +1,14 @@ +package net.woggioni.jwo; + +import java.io.FilterWriter; +import java.io.IOException; +import java.io.Writer; + +public class UncloseableWriter extends FilterWriter { + public UncloseableWriter(Writer destination) { + super(destination); + } + + @Override + public void close() throws IOException {} +} \ 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 index d2a9c92..455f3d7 100644 --- a/src/main/java/net/woggioni/jwo/UnmodifiableDelegatingMap.java +++ b/src/main/java/net/woggioni/jwo/UnmodifiableDelegatingMap.java @@ -100,7 +100,7 @@ public class UnmodifiableDelegatingMap implements Map { mapFactory, Map.Entry::getKey, Map.Entry::getValue, - CollectionUtils::newValueMerger + CollectionUtils.MapMergeStrategy.REPLACE ) ); } diff --git a/src/main/java/net/woggioni/jwo/url/classpath/Handler.java b/src/main/java/net/woggioni/jwo/url/classpath/Handler.java new file mode 100644 index 0000000..68ff75b --- /dev/null +++ b/src/main/java/net/woggioni/jwo/url/classpath/Handler.java @@ -0,0 +1,24 @@ +package net.woggioni.jwo.url.classpath; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +public class Handler extends URLStreamHandler { + private final ClassLoader classLoader; + + public Handler() { + this.classLoader = getClass().getClassLoader(); + } + + public Handler(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + protected URLConnection openConnection(URL u) throws IOException { + final URL resourceUrl = classLoader.getResource(u.getPath()); + return resourceUrl.openConnection(); + } +} diff --git a/src/main/java/net/woggioni/jwo/xml/DocumentWalker.java b/src/main/java/net/woggioni/jwo/xml/DocumentWalker.java new file mode 100644 index 0000000..9e58354 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/xml/DocumentWalker.java @@ -0,0 +1,102 @@ +package net.woggioni.jwo.xml; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.w3c.dom.Node; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + +@Getter +class StackElement { + + @Setter + private XMLNodeVisitor.NodeVisitResultPre resultPre; + private final Node node; + private final NodeListIterator iterator; + + StackElement(Node node) { + this.resultPre = null; + this.node = node; + this.iterator = new NodeListIterator(node.getChildNodes()); + } +} + +class Stack { + private final List stack = new ArrayList<>(); + + private final List nodeStack = new ArrayList<>(); + private final List nodes = Collections.unmodifiableList(nodeStack); + + + public void push(Node node) { + stack.add(new StackElement(node)); + nodeStack.add(node); + } + + public StackElement last() { + return stack.get(stack.size() - 1); + } + + public StackElement pop() { + nodeStack.remove(nodeStack.size() -1); + return stack.remove(stack.size() - 1); + } + + public List nodes() { + return nodes; + } + + public boolean isNotEmpty() { + return !stack.isEmpty(); + } +} + +@RequiredArgsConstructor +public class DocumentWalker { + + public static void walk(Node root, XMLNodeVisitor visitor) { + new DocumentWalker(root).walk(visitor); + } + + private final Node root; + + public void walk(XMLNodeVisitor visitor) { + Stack stack = new Stack(); + stack.push(root); + + loop: + while(stack.isNotEmpty()) { + StackElement se = stack.last(); + if(se.getIterator().hasNext()) { + Node childNode = se.getIterator().next(); + XMLNodeVisitor.NodeVisitResultPre result = se.getResultPre(); + if(result == null) { + result = visitor.visitNodePre(stack.nodes()); + se.setResultPre(result); + } + switch (result) { + case CONTINUE: + stack.push(childNode); + break; + case SKIP_SUBTREE: + break; + case END_TRAVERSAL: + break loop; + } + } else { + XMLNodeVisitor.NodeVisitResultPost result = visitor.visitNodePost(stack.nodes()); + stack.pop(); + switch (result) { + case CONTINUE: + break; + case END_TRAVERSAL: + break loop; + } + } + } + } +} diff --git a/src/main/java/net/woggioni/jwo/xml/ElementBuilder.java b/src/main/java/net/woggioni/jwo/xml/ElementBuilder.java new file mode 100644 index 0000000..445aa78 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/xml/ElementBuilder.java @@ -0,0 +1,77 @@ +package net.woggioni.jwo.xml; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.woggioni.jwo.MapBuilder; +import net.woggioni.jwo.Tuple2; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Consumer; + +@RequiredArgsConstructor +public class ElementBuilder { + + private final Document doc; + + @Getter + private final Element root; + + public ElementBuilder node(String name) { + return node(name, eb -> { + }); + } + + public ElementBuilder node(String name, Consumer cb) { + Element child = doc.createElement(name); + if(root == null) { + doc.appendChild(child); + } else { + root.appendChild(child); + } + ElementBuilder eb = new ElementBuilder(doc, child); + cb.accept(eb); + return eb; + } + + public final ElementBuilder node(String name, String textContent, Map attrs) { + return node(name, eb -> { + if(textContent != null) eb.text(textContent); + for(Map.Entry attr : attrs.entrySet()) { + eb.attr(attr.getKey(), attr.getValue()); + } + }); + } + + @SafeVarargs + public final ElementBuilder node(String name, String textContent, Tuple2...attrs) { + MapBuilder mapBuilder = new MapBuilder<>(); + for(Tuple2 attr : attrs) { + mapBuilder.entry(attr.get_1(), attr.get_2()); + } + return node(name, textContent, mapBuilder.build(TreeMap::new)); + } + + @SafeVarargs + public final ElementBuilder node(String name, Tuple2...attrs) { + return node(name, null, attrs); + } + + public final ElementBuilder node(String name, Map attrs) { + return node(name, null, attrs); + } + + public ElementBuilder text(String textContent) { + root.setTextContent(textContent); + return this; + } + + public ElementBuilder attr(String name, String value) { + root.setAttribute(name, value); + return this; + } + + +} diff --git a/src/main/java/net/woggioni/jwo/xml/ElementIterator.java b/src/main/java/net/woggioni/jwo/xml/ElementIterator.java new file mode 100644 index 0000000..d942b2d --- /dev/null +++ b/src/main/java/net/woggioni/jwo/xml/ElementIterator.java @@ -0,0 +1,49 @@ +package net.woggioni.jwo.xml; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; + +public class ElementIterator implements Iterator { + private final NodeListIterator it; + private final String name; + private Element next; + + public ElementIterator(Element parent) { + this(parent, null); + } + + public ElementIterator(Element parent, String name) { + it = new NodeListIterator(parent.getChildNodes()); + this.name = name; + next = getNext(); + } + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public Element next() { + if(next == null) throw new NoSuchElementException(); + Element result = next; + next = getNext(); + return result; + } + + private Element getNext() { + Element result = null; + while(it.hasNext()) { + Node node = it.next(); + if(node instanceof Element && (name == null || Objects.equals(name, ((Element) node).getTagName()))) { + result = (Element) node; + break; + } + } + return result; + } +} \ No newline at end of file diff --git a/src/main/java/net/woggioni/jwo/xml/NodeListIterator.java b/src/main/java/net/woggioni/jwo/xml/NodeListIterator.java new file mode 100644 index 0000000..1381d19 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/xml/NodeListIterator.java @@ -0,0 +1,27 @@ +package net.woggioni.jwo.xml; + +import lombok.RequiredArgsConstructor; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +@RequiredArgsConstructor +public class NodeListIterator implements Iterator { + + private final NodeList nodeList; + + private int cursor = 0; + + @Override + public boolean hasNext() { + return cursor < nodeList.getLength(); + } + + @Override + public Node next() { + if(hasNext()) return nodeList.item(cursor++); + else throw new NoSuchElementException(); + } +} \ No newline at end of file diff --git a/src/main/java/net/woggioni/jwo/xml/XMLNodeVisitor.java b/src/main/java/net/woggioni/jwo/xml/XMLNodeVisitor.java new file mode 100644 index 0000000..2d16889 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/xml/XMLNodeVisitor.java @@ -0,0 +1,48 @@ +package net.woggioni.jwo.xml; + +import org.w3c.dom.Node; + +import java.util.List; +import java.util.Objects; + +public interface XMLNodeVisitor { + default NodeVisitResultPre visitNodePre(List stack) { + return NodeVisitResultPre.CONTINUE; + } + + default NodeVisitResultPost visitNodePost(List stack) { + return NodeVisitResultPost.CONTINUE; + } + + enum NodeVisitResultPre { + CONTINUE, SKIP_SUBTREE, END_TRAVERSAL + } + + enum NodeVisitResultPost { + CONTINUE, END_TRAVERSAL + } + + static boolean stackMatches(List nodes, String... names) { + return stackMatches(nodes, false, names); + } + + static boolean stackSame(List nodes, String... names) { + return stackMatches(nodes, true, names); + } + + static boolean stackMatches(List nodes, boolean strict, String... names) { + if(nodes.size() < names.length) return false; + int nameIndex = 0; + int nodeIndex = 0; + while(nameIndex < names.length) { + if(nodeIndex >= nodes.size()) return false; + Node node = nodes.get(nodeIndex++); + if(!strict && node.getNodeType() != Node.ELEMENT_NODE) continue; + String name = names[nameIndex++]; + if(name != null && + node.getNodeType() == Node.ELEMENT_NODE && + !Objects.equals(name, node.getNodeName())) return false; + } + return !strict || (nodeIndex == nodes.size() - 1); + } +} diff --git a/src/main/java/net/woggioni/jwo/xml/Xml.java b/src/main/java/net/woggioni/jwo/xml/Xml.java new file mode 100644 index 0000000..3c2abcd --- /dev/null +++ b/src/main/java/net/woggioni/jwo/xml/Xml.java @@ -0,0 +1,91 @@ +package net.woggioni.jwo.xml; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import net.woggioni.jwo.Con; +import net.woggioni.jwo.Fun; +import net.woggioni.jwo.JWO; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class Xml { + + public static Iterator iterator(NodeList node) { + return new NodeListIterator(node); + } + + public static Iterator iterator(Element element, String tagName) { + return new ElementIterator(element, tagName); + } + + public static Iterator iterator(Element element) { + return new ElementIterator(element); + } + + public static Spliterator spliterator(NodeList node) { + return Spliterators.spliteratorUnknownSize(iterator(node), 0); + } + + public static Spliterator spliterator(Element element) { + return Spliterators.spliteratorUnknownSize(iterator(element), 0); + } + + public static Stream stream(NodeList node) { + return StreamSupport.stream(spliterator(node), false); + } + + public static Stream stream(Element element) { + return StreamSupport.stream(spliterator(element), false); + } + public static Iterable iterable(NodeList node) { + return () -> iterator(node); + } + + public static Iterable iterable(Element element, String tagName) { + return () -> iterator(element, tagName); + } + + public static Iterable iterable(Element element) { + return () -> iterator(element); + } + + @SuppressWarnings("unchecked") + public static T withChild(Node node, Fun callback, String... path) { + Object[] result = new Object[1]; + XMLNodeVisitor visitor = new XMLNodeVisitor() { + @Override + public NodeVisitResultPre visitNodePre(List stack) { + if (XMLNodeVisitor.stackMatches(stack, path)) { + result[0] = callback.apply(JWO.tail(stack)); + return NodeVisitResultPre.END_TRAVERSAL; + } else if (stack.size() < path.length) return NodeVisitResultPre.CONTINUE; + else { + return NodeVisitResultPre.SKIP_SUBTREE; + } + } + }; + new DocumentWalker(node).walk(visitor); + return (T) result[0]; + } + + public static void withChild(Element element, String tagName, Con callback) { + for (Element el : iterable(element, tagName)) { + callback.accept(el); + } + } + + public static void withChild(Element element, Con callback) { + for (Element el : iterable(element)) { + callback.accept(el); + } + } +} diff --git a/src/main/java9/module-info.java b/src/main/java9/module-info.java index 1ad081d..5191282 100644 --- a/src/main/java9/module-info.java +++ b/src/main/java9/module-info.java @@ -1,6 +1,11 @@ module net.woggioni.jwo { + requires static java.xml; + requires static java.sql; requires static lombok; - requires org.slf4j; + requires static org.slf4j; + exports net.woggioni.jwo; exports net.woggioni.jwo.exception; + exports net.woggioni.jwo.url.classpath; + exports net.woggioni.jwo.xml; } \ No newline at end of file