From 5a644ac6b5c4e60d4f4198b6959928b3c8a3a1b5 Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Mon, 28 Oct 2019 19:02:45 +0000 Subject: [PATCH] initial commit --- benchmark/build.sbt | 15 + .../jwo/benchmark/NaiveTemplateRenderer.java | 34 ++ .../main/resources/render_template_test.txt | 1 + .../net/woggioni/jwo/benchmark/Main.scala | 47 +++ build.sbt | 28 ++ project/build.properties | 1 + project/plugins.sbt | 7 + .../java/net/woggioni/jwo/Chronometer.java | 44 +++ .../net/woggioni/jwo/CollectionUtils.java | 138 +++++++ src/main/java/net/woggioni/jwo/JWO.java | 341 ++++++++++++++++++ .../net/woggioni/jwo/JavaProcessBuilder.java | 60 +++ src/main/java/net/woggioni/jwo/ListView.java | 244 +++++++++++++ src/main/java/net/woggioni/jwo/LockFile.java | 39 ++ .../net/woggioni/jwo/http/HttpClient.java | 52 +++ .../net/woggioni/jwo/http/HttpMethod.java | 16 + .../net/woggioni/jwo/http/HttpRequest.java | 47 +++ .../net/woggioni/jwo/http/HttpStatus.java | 17 + .../net/woggioni/jwo/io/CircularBuffer.java | 38 ++ .../woggioni/jwo/io/CircularInputStream.java | 55 +++ .../woggioni/jwo/io/LookAheadInputStream.java | 39 ++ .../jwo/io/LookAheadTextInputStream.java | 41 +++ .../net/woggioni/jwo/tree/StackContext.java | 25 ++ .../java/net/woggioni/jwo/tree/TreeNode.java | 7 + .../woggioni/jwo/tree/TreeNodeVisitor.java | 38 ++ .../net/woggioni/jwo/tree/TreeWalker.java | 76 ++++ .../net/woggioni/jwo/tuple/MutableTuple2.java | 21 ++ .../net/woggioni/jwo/tuple/MutableTuple3.java | 24 ++ .../java/net/woggioni/jwo/tuple/Tuple2.java | 19 + .../java/net/woggioni/jwo/tuple/Tuple3.java | 21 ++ .../java/net/woggioni/jwo/utils/JWOTest.java | 86 +++++ .../jwo/utils/io/CircularBufferTest.java | 37 ++ .../jwo/utils/tree/TreeWalkerTest.java | 110 ++++++ src/test/resources/render_template_test.txt | 1 + 33 files changed, 1769 insertions(+) create mode 100644 benchmark/build.sbt create mode 100644 benchmark/src/main/java/net/woggioni/jwo/benchmark/NaiveTemplateRenderer.java create mode 100644 benchmark/src/main/resources/render_template_test.txt create mode 100644 benchmark/src/main/scala/net/woggioni/jwo/benchmark/Main.scala create mode 100644 build.sbt create mode 100644 project/build.properties create mode 100644 project/plugins.sbt create mode 100644 src/main/java/net/woggioni/jwo/Chronometer.java create mode 100644 src/main/java/net/woggioni/jwo/CollectionUtils.java create mode 100644 src/main/java/net/woggioni/jwo/JWO.java create mode 100644 src/main/java/net/woggioni/jwo/JavaProcessBuilder.java create mode 100644 src/main/java/net/woggioni/jwo/ListView.java create mode 100644 src/main/java/net/woggioni/jwo/LockFile.java create mode 100644 src/main/java/net/woggioni/jwo/http/HttpClient.java create mode 100644 src/main/java/net/woggioni/jwo/http/HttpMethod.java create mode 100644 src/main/java/net/woggioni/jwo/http/HttpRequest.java create mode 100644 src/main/java/net/woggioni/jwo/http/HttpStatus.java create mode 100644 src/main/java/net/woggioni/jwo/io/CircularBuffer.java create mode 100644 src/main/java/net/woggioni/jwo/io/CircularInputStream.java create mode 100644 src/main/java/net/woggioni/jwo/io/LookAheadInputStream.java create mode 100644 src/main/java/net/woggioni/jwo/io/LookAheadTextInputStream.java create mode 100644 src/main/java/net/woggioni/jwo/tree/StackContext.java create mode 100644 src/main/java/net/woggioni/jwo/tree/TreeNode.java create mode 100644 src/main/java/net/woggioni/jwo/tree/TreeNodeVisitor.java create mode 100644 src/main/java/net/woggioni/jwo/tree/TreeWalker.java create mode 100644 src/main/java/net/woggioni/jwo/tuple/MutableTuple2.java create mode 100644 src/main/java/net/woggioni/jwo/tuple/MutableTuple3.java create mode 100644 src/main/java/net/woggioni/jwo/tuple/Tuple2.java create mode 100644 src/main/java/net/woggioni/jwo/tuple/Tuple3.java create mode 100644 src/test/java/net/woggioni/jwo/utils/JWOTest.java create mode 100644 src/test/java/net/woggioni/jwo/utils/io/CircularBufferTest.java create mode 100644 src/test/java/net/woggioni/jwo/utils/tree/TreeWalkerTest.java create mode 100644 src/test/resources/render_template_test.txt diff --git a/benchmark/build.sbt b/benchmark/build.sbt new file mode 100644 index 0000000..50f7dd4 --- /dev/null +++ b/benchmark/build.sbt @@ -0,0 +1,15 @@ +organization := (organization in LocalRootProject).value +name := "jwo-benchmark" +version := (version in LocalRootProject).value +resourceDirectory in Compile := (resourceDirectory in(LocalRootProject, Test)).value +skip in publish := true +maintainer := (maintainer in LocalRootProject).value +mainClass := Some("net.woggioni.jwo.benchmark.Main") +fork := true +// libraryDependencies += "com.fasterxml.jackson.core" % "jackson-databind" % "2.9.6", +// libraryDependencies += "org.tukaani" % "xz" % "1.8", +// libraryDependencies += "com.beust" % "jcommander" % "1.72", +// libraryDependencies += "org.projectlombok" % "lombok" % "1.18.8" % Provided + +enablePlugins(JavaAppPackaging) +enablePlugins(UniversalPlugin) \ No newline at end of file diff --git a/benchmark/src/main/java/net/woggioni/jwo/benchmark/NaiveTemplateRenderer.java b/benchmark/src/main/java/net/woggioni/jwo/benchmark/NaiveTemplateRenderer.java new file mode 100644 index 0000000..791f1d9 --- /dev/null +++ b/benchmark/src/main/java/net/woggioni/jwo/benchmark/NaiveTemplateRenderer.java @@ -0,0 +1,34 @@ +package net.woggioni.jwo.benchmark; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class NaiveTemplateRenderer { + public static String renderTemplateNaive(String template, Map valuesMap){ + StringBuffer formatter = new StringBuffer(template); + Object absent = new Object(); + + Matcher matcher = Pattern.compile("\\$\\{(\\w+)}").matcher(template); + + while (matcher.find()) { + String key = matcher.group(1); + + String formatKey = String.format("${%s}", key); + int index = formatter.indexOf(formatKey); + + // If the key is present: + // - If the value is not null, then replace the variable for the value + // - If the value is null, replace the variable for empty string + // If the key is not present, leave the variable untouched. + if (index != -1) { + Object value = valuesMap.getOrDefault(key, absent); + if(value != absent) { + String valueStr = value != null ? value.toString() : ""; + formatter.replace(index, index + formatKey.length(), valueStr); + } + } + } + return formatter.toString(); + } +} diff --git a/benchmark/src/main/resources/render_template_test.txt b/benchmark/src/main/resources/render_template_test.txt new file mode 100644 index 0000000..560aa12 --- /dev/null +++ b/benchmark/src/main/resources/render_template_test.txt @@ -0,0 +1 @@ +This is a ${adjective} test made by ${author} on ${date}. It's really ${adjective}! diff --git a/benchmark/src/main/scala/net/woggioni/jwo/benchmark/Main.scala b/benchmark/src/main/scala/net/woggioni/jwo/benchmark/Main.scala new file mode 100644 index 0000000..354a569 --- /dev/null +++ b/benchmark/src/main/scala/net/woggioni/jwo/benchmark/Main.scala @@ -0,0 +1,47 @@ +package net.woggioni.jwo.benchmark + +import java.io.{InputStream, InputStreamReader, Reader} + +import net.woggioni.jwo.io.CircularInputStream +import net.woggioni.jwo.{Chronometer, JWO} + +import scala.collection.JavaConverters._ + +object Main { + + def withBenchmarkReader(fn : Reader => Unit) = { + val reader = new InputStreamReader(classOf[JWO].getResourceAsStream("/render_template_test.txt")) + val is : InputStream = new CircularInputStream(JWO.readAll(reader).getBytes, 10000) + try { + fn(new InputStreamReader(is)) + } finally { + if (reader != null) reader.close() + } + } + + def renderTemplateBenchmark() { + val valuesMap = Map[String, Object]("author" -> "John Doe", + "date" -> "2020-03-25 16:22", + "adjective" -> "simple") + withBenchmarkReader( + reader => { + val chronometer = new Chronometer + val result = JWO.renderTemplate(reader, valuesMap.asJava) + printf("Elapsed time: %.3f\n", chronometer.elapsed(Chronometer.UnitOfMeasure.SECONDS)) + } + ) + + withBenchmarkReader( + reader => { + val chronometer = new Chronometer + val result = NaiveTemplateRenderer.renderTemplateNaive(JWO.readAll(reader), valuesMap.asJava) + printf("Elapsed time: %.3f\n", chronometer.elapsed(Chronometer.UnitOfMeasure.SECONDS)) + } + ) + } + + def main(argv : Array[String]) = { + renderTemplateBenchmark() + } + +} diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..6486a8b --- /dev/null +++ b/build.sbt @@ -0,0 +1,28 @@ +name := "jwo" + +organization := "net.woggioni" + +maintainer := "oggioni.walter@gmail.com" + +version := "1.0" +resolvers += Resolver.mavenLocal + +git.useGitDescribe := true +crossPaths := false + +autoScalaLibrary := false + +libraryDependencies += "org.projectlombok" % "lombok" % "1.18.8" % Provided + +libraryDependencies += "org.slf4j" % "slf4j-api" % "1.7.28" + +libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % Test +libraryDependencies += "org.apache.logging.log4j" % "log4j-core" % "2.12.1" % Test +libraryDependencies += "org.apache.logging.log4j" % "log4j-slf4j-impl" % "2.12.1" % Test + +javacOptions ++= Seq("-source", "1.8", "-target", "1.8") +javacOptions in (Compile, doc) := Seq("-source", "1.8") + +enablePlugins(Delombok) +enablePlugins(DelombokJavadoc) +lazy val benchmark = (project in file("benchmark")).dependsOn(LocalRootProject) diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..ea6d47b --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.3.1 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..feec9a3 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,7 @@ +addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0") +addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "0.9.3") +addSbtPlugin("de.heikoseeberger" % "sbt-header" % "4.1.0") +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.6") +addSbtPlugin("com.thoughtworks.sbt" % "delombokjavadoc" % "1.0.0+66-8fbcf18c") + +//addSbtPlugin("com.thoughtworks.sbt" % "delombokjavadoc" % "latest.release") \ No newline at end of file diff --git a/src/main/java/net/woggioni/jwo/Chronometer.java b/src/main/java/net/woggioni/jwo/Chronometer.java new file mode 100644 index 0000000..1244c8d --- /dev/null +++ b/src/main/java/net/woggioni/jwo/Chronometer.java @@ -0,0 +1,44 @@ +package net.woggioni.jwo; + +import lombok.SneakyThrows; + +public class Chronometer { + + public enum UnitOfMeasure { + NANOSECONDS(1), + MICROSECONDS(NANOSECONDS.nanoseconds_size * 1000), + MILLISECONDS(MICROSECONDS.nanoseconds_size * 1000), + SECONDS(MILLISECONDS.nanoseconds_size * 1000), + MINUTES(SECONDS.nanoseconds_size * 60), + HOURS(MINUTES.nanoseconds_size * 60), + DAYS(HOURS.nanoseconds_size * 24), + WEEKS(DAYS.nanoseconds_size * 7), + MONTHS(DAYS.nanoseconds_size * 30), + YEARS(DAYS.nanoseconds_size * 365); + + public long nanoseconds_size; + + UnitOfMeasure(long nanoseconds_size) { + this.nanoseconds_size = nanoseconds_size; + } + } + + private long start; + + @SneakyThrows + public Chronometer() { + start = System.nanoTime(); + } + + public void reset() { + start = System.nanoTime(); + } + + public long elapsed() { + return System.nanoTime() - start; + } + + public double elapsed(UnitOfMeasure unitOfMeasure) { + return ((double) (System.nanoTime() - start)) / unitOfMeasure.nanoseconds_size; + } +} diff --git a/src/main/java/net/woggioni/jwo/CollectionUtils.java b/src/main/java/net/woggioni/jwo/CollectionUtils.java new file mode 100644 index 0000000..5627471 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/CollectionUtils.java @@ -0,0 +1,138 @@ +package net.woggioni.jwo; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Stream; + +public class CollectionUtils { + + @SafeVarargs + public static ArrayList newArrayList(T... args) { + return new ArrayList<>(Arrays.asList(args)); + } + + public static Collector> toUnmodifiableList() { + return Collector.of( + ArrayList::new, + List::add, + (ArrayList l1, ArrayList l2) -> { + l1.addAll(l2); + return l1; + }, + Collections::unmodifiableList); + } + + @SafeVarargs + public static List immutableList(T... elements) { + return Stream.of(elements).collect(toUnmodifiableList()); + } + + public static Collector> toUnmodifiableSet() { + return Collector.of( + HashSet::new, + Set::add, + (Set s1, Set s2) -> { + s1.addAll(s2); + return s1; + }, + Collections::unmodifiableSet); + } + + @SafeVarargs + public static Set immutableSet(T... elements) { + return Stream.of(elements).collect(toUnmodifiableSet()); + } + + private static Collector> createTreeSetCollector(Function, Set> finalizer, Comparator comparator) { + return Collector.of( + () -> new TreeSet<>(comparator), + Set::add, + (Set s1, Set s2) -> { + s1.addAll(s2); + return s1; + }, + finalizer); + } + + private static > Collector> + createTreeSetCollector(Function, Set> finalizer) { + return Collector.of( + TreeSet::new, + Set::add, + (Set s1, Set s2) -> { + s1.addAll(s2); + return s1; + }, + finalizer); + } + + public static > Collector> toTreeSet() { + return createTreeSetCollector(Function.identity()); + } + + public static Collector> toTreeSet(Comparator comparator) { + return createTreeSetCollector(Function.identity(), comparator); + } + + public static Collector> toUnmodifiableTreeSet(Comparator comparator) { + return createTreeSetCollector(Collections::unmodifiableSet, comparator); + } + + public static > Collector> toUnmodifiableTreeSet() { + return createTreeSetCollector(Collections::unmodifiableSet); + } + + @SafeVarargs + public static Set immutableTreeSet(Comparator comparator, T... elements) { + return Stream.of(elements).collect(toUnmodifiableTreeSet(comparator)); + } + + @SafeVarargs + public static > Set immutableTreeSet(T... elements) { + return Stream.of(elements).collect(toUnmodifiableTreeSet()); + } + + private static > BinaryOperator mapMerger(BinaryOperator var0) { + return (m1, m2) -> { + Iterator> it = m2.entrySet().iterator(); + + while (it.hasNext()) { + Map.Entry entry = it.next(); + m1.merge(entry.getKey(), entry.getValue(), var0); + } + + return m1; + }; + } + + private static BinaryOperator throwingMerger() { + return (v1, v2) -> { + throw new IllegalStateException(String.format("Duplicate key %s", v1)); + }; + } + + public static Collector> toUnmodifiableMap(Function keyExtractor, Function valueExtractor) { + BiConsumer, T> accumulator = (map, streamElement) -> { + map.merge(keyExtractor.apply(streamElement), valueExtractor.apply(streamElement), throwingMerger()); + }; + return Collector.of( + HashMap::new, + accumulator, + mapMerger(throwingMerger()), + Collections::unmodifiableMap + ); + } +} diff --git a/src/main/java/net/woggioni/jwo/JWO.java b/src/main/java/net/woggioni/jwo/JWO.java new file mode 100644 index 0000000..34dd6f2 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/JWO.java @@ -0,0 +1,341 @@ +package net.woggioni.jwo; + +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import javax.net.ssl.*; +import java.io.*; +import java.lang.reflect.Constructor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +@Slf4j +public class JWO { + public static Stream iterable2stream(Iterable iterable) { + return StreamSupport.stream(iterable.spliterator(), false); + } + + @SneakyThrows + public static void writeObject2File(Path file, Object o) { + try (Writer writer = new OutputStreamWriter(new FileOutputStream(file.toString()))) { + writer.write(o.toString()); + } + } + + public static void writeObject2File(String fileName, Object o) { + writeObject2File(new File(fileName), o); + } + + @SneakyThrows + public static void writeObject2File(File file, Object o) { + try (Writer writer = new OutputStreamWriter(new FileOutputStream(file.getPath()))) { + writer.write(o.toString()); + } + } + + @SneakyThrows + public static void writeBytes2File(Path file, byte[] bytes) { + try (OutputStream os = new FileOutputStream(file.toString())) { + 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())))) { + char[] buffer = new char[1024]; + while (true) { + int read = reader.read(buffer); + builder.append(buffer, 0, read); + if (read < buffer.length) break; + } + } + return builder.toString(); + } + + @SneakyThrows + public static String readResource2String(String classpath) { + StringBuilder builder = new StringBuilder(); + try (Reader reader = new InputStreamReader(JWO.class.getResourceAsStream(classpath))) { + char[] buffer = new char[1024]; + while (true) { + int read = reader.read(buffer); + builder.append(buffer, 0, read); + if (read < buffer.length) break; + } + } + return builder.toString(); + } + + @SneakyThrows + public static T newThrowable(Class cls, String format, Object... args) { + Constructor constructor = cls.getDeclaredConstructor(String.class); + constructor.setAccessible(true); + return constructor.newInstance(String.format(format, args)); + } + + @SneakyThrows + public static T newThrowable(Class cls, Throwable throwable, String format, Object... args) { + Constructor constructor = cls.getConstructor(String.class, Throwable.class); + return constructor.newInstance(String.format(format, args), throwable); + } + + @SneakyThrows + public static void raise(Class cls, Throwable throwable, String format, Object... args) { + throw newThrowable(cls, throwable, format, args); + } + + @SneakyThrows + public static void raise(Class cls, String format, Object... args) { + throw newThrowable(cls, format, args); + } + + + private static SSLSocketFactory defaultSSLSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + private static HostnameVerifier defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); + + @SneakyThrows + public static void setSSLVerifyPeerHostName(boolean verify) { + if (verify) { + HttpsURLConnection.setDefaultHostnameVerifier(defaultHostnameVerifier); + } else { + HostnameVerifier allHostsValid = new HostnameVerifier() { + public boolean verify(String hostname, SSLSession session) { + return true; + } + }; + HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid); + } + } + + @SneakyThrows + public static void setSSLVerifyPeerCertificate(boolean verify) { + if (verify) { + HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory); + } else { + // Create a trust manager that does not validate certificate chains + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + + public void checkClientTrusted(X509Certificate[] certs, String authType) { + } + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + } + } + }; + // Install the all-trusting trust manager + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + } + } + + public static Predicate not(Predicate p) { + return p.negate(); + } + + public static Stream flatMap(Stream stream, + Function> mappingFunction) { + 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); + } + } + + final private static char[] hexArray = "0123456789ABCDEF".toCharArray(); + + public static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + @SneakyThrows + public static void deletePath(Path path) { + Files.walk(path) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + + /** + * This method returns an element of a list at the given position counting from the end of the list, + * inspiration comes from the Python programming language where the element + * at position -1 is the last element of the list, the one at position -2 is the element before the last and so on + * + * @param list the list from which the element will be retrieved + * @param negativeOffset a negative integer representing the offset from the end of the list + * @param the type parameter of the list + * @return the designated list element + * @throws IndexOutOfBoundsException if the negativeOffset is out of range + * (negativeOffset ≥ 0 || negativeOffset < -size()) + */ + public static T tail(List list, int negativeOffset) { + return list.get(list.size() + negativeOffset); + } + + /** + * @param list the input list + * @param the type parameter of the list + * @return the last element of the input list + * @throws IndexOutOfBoundsException if the list is empty + */ + public static T tail(List list) { + return tail(list, -1); + } + + + /** + * This methods simply removes the last element of the list and returns it + * + * @param list the input list + * @param the type parameter of the {@link List} + * @throws IndexOutOfBoundsException if the list is empty + * @throws UnsupportedOperationException if the remove operation is not supported by this list + * @return the input list + */ + public static T pop(List list) { + return list.remove(list.size() - 1); + } + + public static Stream streamCat(Stream... streams) { + Stream result = Stream.empty(); + for (Stream s : streams) { + result = Stream.concat(result, s); + } + return result; + } + + /** + * @param template Template text containing the variables to be replaced by this method.
+ * Variables follow the format ${variable_name}.
+ * Example:
+ * "This template was created by ${author}." + * @param valuesMap A hashmap with the values of the variables to be replaced.
+ * The key is the variable name and the value is the value to be replaced in the template.
+ * Example:
+ * {"author" => "John Doe"} + * @return The template text (String) with the variable names replaced by the values passed in the map.
+ * If any of the variable names is not contained in the map it will be replaced by an empty string.
+ * Example:
+ * "This template was created by John Doe." + */ + public static String renderTemplate(String template, Map valuesMap) { + StringBuilder sb = new StringBuilder(); + Object absent = new Object(); + + int cursor = 0; + while (cursor < template.length()) { + String key; + char ch = template.charAt(cursor); + if (ch != '$' || (cursor > 0 && template.charAt(cursor - 1) == '\\')) { + sb.append(template.charAt(cursor++)); + } else if (cursor + 1 < template.length() && template.charAt(cursor + 1) == '{') { + int end = template.indexOf('}', cursor + 1); + key = template.substring(cursor + 2, end); + Object value = valuesMap.getOrDefault(key, absent); + if (value != absent) { + sb.append(value.toString()); + } else { + raise(MissingFormatArgumentException.class, "Missing value for placeholder '%s'", key); + } + cursor = end + 1; + } else { + sb.append(template.charAt(cursor++)); + } + } + return sb.toString(); + } + + /** + * @param reader the reader reading the template text + * @param valuesMap a map containing the values to replace in the template + * {@link #renderTemplate(String, Map)} + */ + @SneakyThrows + public static String renderTemplate(Reader reader, Map valuesMap) { + StringBuilder sb = new StringBuilder(); + char[] buf = new char[1024]; + int read; + while (!((read = reader.read(buf)) < 0)) { + sb.append(buf, 0, read); + } + return renderTemplate(sb.toString(), valuesMap); + } + + @SneakyThrows + public static String readAll(Reader reader) { + char[] buffer = new char[1024]; + StringBuilder sb = new StringBuilder(); + int read; + while (!((read = reader.read(buffer, 0, buffer.length)) < 0)) { + sb.append(buffer, 0, read); + } + return sb.toString(); + } + + @SneakyThrows + public static Path computeCacheDirectory(String appName) { + return Stream.of( + Optional.ofNullable(System.getProperty("user.home")) + .map(prefix -> Paths.get(prefix, ".cache", appName)), + Optional.ofNullable(System.getProperty("java.io.tmpdir")).map(Paths::get).map(p -> p.resolve(appName)), + Optional.of(Paths.get("/tmp", appName))) + .flatMap(JWO::optional2Stream) + .filter(JWO::validateCacheDirectory) + .findFirst() + .orElseThrow(() -> newThrowable(FileNotFoundException.class, "Unable to find a usable cache directory")); + } + + private static boolean validateCacheDirectory(Path candidate) { + try { + if (!Files.exists(candidate)) { + Files.createDirectories(candidate); + return true; + } else if (!Files.isDirectory(candidate)) { + log.debug("Cache directory '{}' discarded because it is not a directory", candidate.toString()); + return false; + } else if (!Files.isWritable(candidate)) { + log.debug("Cache directory '{}' discarded because it is not writable", candidate.toString()); + return false; + } else { + log.info("Using cache directory '{}'", candidate.toString()); + return true; + } + } catch (Exception ioe) { + log.debug(String.format("Cache directory '%s' discarded: %s", candidate.toString(), ioe.getMessage()), ioe); + return false; + } + } + + public static Optional cast(T value, Class cls) { + if(cls.isInstance(value)) { + return Optional.of((U) value); + } else { + return Optional.empty(); + } + } +} diff --git a/src/main/java/net/woggioni/jwo/JavaProcessBuilder.java b/src/main/java/net/woggioni/jwo/JavaProcessBuilder.java new file mode 100644 index 0000000..f7a714a --- /dev/null +++ b/src/main/java/net/woggioni/jwo/JavaProcessBuilder.java @@ -0,0 +1,60 @@ +package net.woggioni.jwo; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Properties; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@RequiredArgsConstructor +public class JavaProcessBuilder { + + private final Class mainClass; + + private String javaHome = System.getProperty("java.home"); + private String classPath = System.getProperty("java.class.path"); + private Properties properties = new Properties(); + private String[] cliArgs = null; + + public JavaProcessBuilder javaHome(String javaHome) { + this.javaHome = javaHome; + return this; + } + + public JavaProcessBuilder classPath(String classPath) { + this.classPath = classPath; + return this; + } + + public JavaProcessBuilder properties(Properties properties) { + this.properties = properties; + return this; + } + + public JavaProcessBuilder cliArgs(String ...cliArgs) { + this.cliArgs = cliArgs; + return this; + } + + @SneakyThrows + public ProcessBuilder exec() { + Path javaBin = Paths.get(javaHome, "bin", "java"); + Stream propertyStream = Optional.ofNullable(properties) + .map(p -> p.entrySet().stream()) + .orElse(Stream.empty()) + .map(entry -> String.format("-D%s=%s", entry.getKey(), entry.getValue())); + List cmd = JWO.streamCat( + Stream.of(javaBin.toString(), "-cp", classPath), + propertyStream, + Stream.of(mainClass.getCanonicalName()), + Optional.ofNullable(cliArgs).map(Arrays::stream).orElse(Stream.empty())) + .collect(Collectors.toList()); + return new ProcessBuilder(cmd); + } +} diff --git a/src/main/java/net/woggioni/jwo/ListView.java b/src/main/java/net/woggioni/jwo/ListView.java new file mode 100644 index 0000000..a06dabf --- /dev/null +++ b/src/main/java/net/woggioni/jwo/ListView.java @@ -0,0 +1,244 @@ +package net.woggioni.jwo; + +import lombok.RequiredArgsConstructor; + +import java.util.*; + +import static java.lang.Math.min; + +@RequiredArgsConstructor +public class ListView implements List { + private final List delegate; + private final int start; + private final int end; + + public ListView(List delegate, int start) { + this(delegate, start, -1); + } + + @Override + public int size() { + return end < 0 ? delegate.size() : min(end, delegate.size()) - start; + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public boolean contains(Object o) { + Iterator it = iterator(); + while (it.hasNext()) { + if(Objects.equals(o, it.next())) { + return true; + } + } + return false; + } + + @Override + public Iterator iterator() { + return new Iterator() { + int index = start; + @Override + public boolean hasNext() { + return end < 0 ? index < size() : index < min(end, size()); + } + + @Override + public T next() { + return get(index++); + } + }; + } + + @Override + public Object[] toArray() { + int size = size(); + Object[] result = new Object[size]; + for(int i = 0; i < size; i++) { + result[i] = get(i); + } + return result; + } + + @Override + public T1[] toArray(T1[] t1s) { + int size = size(); + T1[] result = Arrays.copyOf(t1s, size); + for(int i = 0; i < size; i++) { + result[i] = (T1) get(i); + } + return result; + } + + @Override + public boolean add(T t) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsAll(Collection collection) { + return false; + } + + @Override + public boolean addAll(Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(int i, Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public T get(int i) { + int index = start + i; + if(end >= 0 && index < end) { + throw new IndexOutOfBoundsException(Integer.toString(i)); + } + return delegate.get(start + i); + } + + @Override + public T set(int i, T t) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(int i, T t) { + throw new UnsupportedOperationException(); + } + + @Override + public T remove(int i) { + throw new UnsupportedOperationException(); + } + + @Override + public int indexOf(Object o) { + int size = size(); + for(int i = 0; i < size; i++) { + if(Objects.equals(o, get(i))) { + return i; + } + } + return -1; + } + + @Override + public int lastIndexOf(Object o) { + int size = size(); + for(int i = size - 1; i >= 0; i--) { + if(Objects.equals(o, get(i))) { + return i; + } + } + return -1; + } + + @Override + public ListIterator listIterator() { + return new ListViewIterator<>(this, 0); + } + + @Override + public ListIterator listIterator(int i) { + if(i < 0 || i > size()) { + throw new IndexOutOfBoundsException(Integer.toString(0)); + } else { + return new ListViewIterator<>(this, start); + } + } + + @Override + public List subList(int i, int i1) { + if(i < 0) { + throw new IndexOutOfBoundsException(Integer.toString(0)); + } else if(i1 > size()) { + throw new IndexOutOfBoundsException(Integer.toString(i1)); + } else { + return new ListView<>(delegate, start + i, start + i1); + } + } + + @RequiredArgsConstructor + private static class ListViewIterator implements ListIterator { + private final ListView listView; + int size; + int i; + + public ListViewIterator(ListView listView, int start) { + this.listView = listView; + size = listView.size(); + i = start; + } + + @Override + public boolean hasNext() { + return i < size; + } + + @Override + public T next() { + return listView.get(i++); + } + + @Override + public boolean hasPrevious() { + return i > 0; + } + + @Override + public T previous() { + return listView.get(--i); + } + + @Override + public int nextIndex() { + return i + 1; + } + + @Override + public int previousIndex() { + return i; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public void set(T t) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(T t) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/src/main/java/net/woggioni/jwo/LockFile.java b/src/main/java/net/woggioni/jwo/LockFile.java new file mode 100644 index 0000000..3d1def3 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/LockFile.java @@ -0,0 +1,39 @@ +package net.woggioni.jwo; + +import lombok.SneakyThrows; + +import java.io.RandomAccessFile; +import java.nio.channels.FileLock; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class LockFile implements AutoCloseable { + + private final Path path; + private final FileLock lock; + private final RandomAccessFile randomAccessFile; + + public LockFile(Path path) { + this(path, false); + } + + @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); + } + + @Override + @SneakyThrows + public void close() { + lock.release(); + randomAccessFile.close(); + } +} diff --git a/src/main/java/net/woggioni/jwo/http/HttpClient.java b/src/main/java/net/woggioni/jwo/http/HttpClient.java new file mode 100644 index 0000000..47616f5 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/http/HttpClient.java @@ -0,0 +1,52 @@ +package net.woggioni.jwo.http; + +import lombok.SneakyThrows; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import java.io.OutputStream; + +public class HttpClient { + + private SSLSocketFactory socketFactory; + + public HttpClient() {} + + public HttpClient(final SSLContext sslContext) { + socketFactory = sslContext.getSocketFactory(); + } + + @SneakyThrows + public HttpsURLConnection call(HttpRequest httpRequest) { + HttpsURLConnection conn = (HttpsURLConnection) httpRequest.url.openConnection(); + if(socketFactory != null) { + conn.setSSLSocketFactory(socketFactory); + } + conn.setInstanceFollowRedirects(false); + conn.setRequestMethod(httpRequest.method.text); + httpRequest.headers.forEach((key, value) -> + value.forEach(headerValue -> conn.addRequestProperty(key, headerValue))); + switch (httpRequest.method) { + case PUT: + case POST: + case DELETE: + if (httpRequest.body != null) { + conn.setDoOutput(true); + byte[] buffer = new byte[1024]; + OutputStream os = conn.getOutputStream(); + while (true) { + int read = httpRequest.body.read(buffer, 0, buffer.length); + if (read < 0) break; + os.write(buffer, 0, read); + } + } + break; + case GET: + case HEAD: + case OPTIONS: + break; + } + return conn; + } +} diff --git a/src/main/java/net/woggioni/jwo/http/HttpMethod.java b/src/main/java/net/woggioni/jwo/http/HttpMethod.java new file mode 100644 index 0000000..874790b --- /dev/null +++ b/src/main/java/net/woggioni/jwo/http/HttpMethod.java @@ -0,0 +1,16 @@ +package net.woggioni.jwo.http; + +public enum HttpMethod { + PUT("PUT"), + GET("GET"), + POST("POST"), + DELETE("DELETE"), + HEAD("HEAD"), + OPTIONS("OPTIONS"); + + public final String text; + + HttpMethod(String text) { + this.text = text; + } +} diff --git a/src/main/java/net/woggioni/jwo/http/HttpRequest.java b/src/main/java/net/woggioni/jwo/http/HttpRequest.java new file mode 100644 index 0000000..68afe8b --- /dev/null +++ b/src/main/java/net/woggioni/jwo/http/HttpRequest.java @@ -0,0 +1,47 @@ +package net.woggioni.jwo.http; + +import lombok.AccessLevel; +import lombok.Builder; +import net.woggioni.jwo.http.HttpMethod; + +import java.io.InputStream; +import java.net.URL; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +@Builder(builderMethodName = "privateBuilder", access = AccessLevel.PUBLIC) +public class HttpRequest { + + final URL url; + + final HttpMethod method = HttpMethod.GET; + + final Map> headers = Collections.emptyMap(); + + final InputStream body = null; + + public static HttpRequestBuilder builder(URL url) { + return HttpRequest.privateBuilder().url(url); + } + + public static class Builder { + private final URL url; + + private HttpMethod method = HttpMethod.GET; + + private Map> headers = Collections.emptyMap(); + + private InputStream body = null; + + private Builder(URL url) { + this.url = url; + } + + public Builder method(HttpMethod method) { + this.method = method; + return this; + } + } +} + diff --git a/src/main/java/net/woggioni/jwo/http/HttpStatus.java b/src/main/java/net/woggioni/jwo/http/HttpStatus.java new file mode 100644 index 0000000..f1eeb79 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/http/HttpStatus.java @@ -0,0 +1,17 @@ +package net.woggioni.jwo.http; + +public enum HttpStatus { + OK(200), + INTERNAL_SERVER_ERROR(500), + BAD_REQUEST(400), + UNAUTHORIZED(401), + FORBIDDEN(403), + NOT_FOUND(404), + CONFLICT(409); + + public final int code; + + HttpStatus(int code) { + this.code = code; + } +} diff --git a/src/main/java/net/woggioni/jwo/io/CircularBuffer.java b/src/main/java/net/woggioni/jwo/io/CircularBuffer.java new file mode 100644 index 0000000..9b02bb8 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/io/CircularBuffer.java @@ -0,0 +1,38 @@ +package net.woggioni.jwo.io; + +import lombok.SneakyThrows; + +import java.io.Reader; + +public class CircularBuffer { + + private int[] buffer; + private Reader reader; + private int delta = 0, cursor = 0; + + public CircularBuffer(Reader reader, int size) { + this.reader = reader; + buffer = new int[size]; + } + + @SneakyThrows + public int next() { + if (delta < 0) + return buffer[Math.floorMod(cursor + delta++, buffer.length)]; + else { + int result = reader.read(); + if (result < 0) return result; + buffer[cursor] = result; + cursor = (cursor + 1) % buffer.length; + return result; + } + } + + public int prev() { + return buffer[cursor + --delta >= 0 ? cursor + delta : cursor + delta + buffer.length]; + } + + public int size() { + return buffer.length; + } +} diff --git a/src/main/java/net/woggioni/jwo/io/CircularInputStream.java b/src/main/java/net/woggioni/jwo/io/CircularInputStream.java new file mode 100644 index 0000000..03704d0 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/io/CircularInputStream.java @@ -0,0 +1,55 @@ +package net.woggioni.jwo.io; + +import lombok.RequiredArgsConstructor; + +import java.io.InputStream; + +@RequiredArgsConstructor +public class CircularInputStream extends InputStream { + final byte[] monomer; + final int maxLoops; + int loops = 0; + int cursor = 0; + + public CircularInputStream(byte[] monomer) { + this(monomer, -1); + } + + @Override + public int read() { + if (cursor < 0) { + return cursor; + } else { + int result = monomer[cursor]; + incrementCursor(); + return result; + } + } + + @Override + public int read(byte[] b, int off, int len) { + int read = 0; + while (read < len) { + if(cursor < 0) break; + int toBeRead = Math.min(monomer.length - cursor, len - read); + System.arraycopy(monomer, cursor, b, off + read, toBeRead); + incrementCursor(toBeRead); + read += toBeRead; + } + return read > 0 ? read : -1; + } + + int incrementCursor() { + return incrementCursor(1); + } + + int incrementCursor(int increment) { + loops = (loops * monomer.length + increment) / monomer.length; + if (maxLoops < 0 || loops < maxLoops) { + cursor = (cursor + increment) % monomer.length; + } else { + cursor = -1; + } + return cursor; + } +} \ No newline at end of file diff --git a/src/main/java/net/woggioni/jwo/io/LookAheadInputStream.java b/src/main/java/net/woggioni/jwo/io/LookAheadInputStream.java new file mode 100644 index 0000000..b65401f --- /dev/null +++ b/src/main/java/net/woggioni/jwo/io/LookAheadInputStream.java @@ -0,0 +1,39 @@ +package net.woggioni.jwo.io; + +import lombok.SneakyThrows; + +import java.io.InputStream; + +public class LookAheadInputStream extends InputStream { + + private final byte[] buffer = new byte[1024]; + private final InputStream stream; + private int bufferFill = -1; + private int cursor = -1; + private int currentByte; + + public LookAheadInputStream(InputStream stream) { + this.stream = stream; + } + + @Override + @SneakyThrows + public int read() { + if (cursor > bufferFill) { + return -1; + } else if (cursor == bufferFill) { + do { + bufferFill = stream.read(buffer, 0, buffer.length) - 1; + cursor = 0; + } while(bufferFill == -1); + currentByte = bufferFill == -2 ? -1 : Math.floorMod(buffer[0], 256); + } else { + currentByte = Math.floorMod(buffer[++cursor], 256); + } + return currentByte; + } + + public int getCurrentByte() { + return currentByte; + } +} diff --git a/src/main/java/net/woggioni/jwo/io/LookAheadTextInputStream.java b/src/main/java/net/woggioni/jwo/io/LookAheadTextInputStream.java new file mode 100644 index 0000000..dadb989 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/io/LookAheadTextInputStream.java @@ -0,0 +1,41 @@ +package net.woggioni.jwo.io; + +import lombok.SneakyThrows; + +import java.io.InputStream; +import java.io.Reader; + +public class LookAheadTextInputStream extends InputStream { + + private final char[] buffer = new char[1024]; + private final Reader reader; + private int bufferFill = -1; + private int cursor = -1; + private int currentChar; + + + public LookAheadTextInputStream(Reader reader) { + this.reader = reader; + } + + @Override + @SneakyThrows + public int read() { + if (cursor > bufferFill) { + return -1; + } else if (cursor == bufferFill) { + do { + bufferFill = reader.read(buffer, 0, buffer.length) - 1; + cursor = 0; + } while(bufferFill == -1); + currentChar = bufferFill == -2 ? -1 : buffer[0]; + } else { + currentChar = buffer[++cursor]; + } + return currentChar; + } + + public int getCurrentByte() { + return currentChar; + } +} diff --git a/src/main/java/net/woggioni/jwo/tree/StackContext.java b/src/main/java/net/woggioni/jwo/tree/StackContext.java new file mode 100644 index 0000000..e421d19 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/tree/StackContext.java @@ -0,0 +1,25 @@ +package net.woggioni.jwo.tree; + +/** + * This interface exposes the methods that are visible to the user of + * {@link TreeWalker}, it allows to + * set/get a custom object in the current stack context or to get the current link's Aci + * @param the type of the context object used + */ +public interface StackContext { + + /** + * @param ctx the user object to set for this stack level + */ + void setContext(T ctx); + + /** + * @return the current user object + */ + T getContext(); + + /** + * @return the current TreeNode + */ + NODE getNode(); +} diff --git a/src/main/java/net/woggioni/jwo/tree/TreeNode.java b/src/main/java/net/woggioni/jwo/tree/TreeNode.java new file mode 100644 index 0000000..0ebfc4e --- /dev/null +++ b/src/main/java/net/woggioni/jwo/tree/TreeNode.java @@ -0,0 +1,7 @@ +package net.woggioni.jwo.tree; + +import java.util.Iterator; + +public interface TreeNode { + Iterator children(); +} diff --git a/src/main/java/net/woggioni/jwo/tree/TreeNodeVisitor.java b/src/main/java/net/woggioni/jwo/tree/TreeNodeVisitor.java new file mode 100644 index 0000000..6411a24 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/tree/TreeNodeVisitor.java @@ -0,0 +1,38 @@ +package net.woggioni.jwo.tree; + +import java.util.List; + +/** + * This interface must be implemented by the user of {@link TreeWalker} and its methods will be called by + * {@link TreeWalker#walk(TreeNode)}. The methods will receive as an input a list of {@link StackContext} + * instances each one correspond to a node in the tree, each node is preceded in the list + * by its parents in the tree. Each instance has a method, {@link StackContext#setContext(Object)} + * to set a custom object that can be used in the {@link #visitPre(List)} method and the method + * {@link StackContext#getContext()} that can be used in the {@link #visitPost(List)} method to retrieve + * the same instance. This is to provide support for algorithms that require both pre-order and post-order logic. + * The last element of the list corresponds to the node currently being traversed. + * @param the type of the context object used + */ +public interface TreeNodeVisitor, T> { + + enum VisitOutcome { + CONTINUE, SKIP, EARLY_EXIT + } + + /** + * This method will be called for each link using + * a Depth-first pre-oder algorithm + * @param stack is a list of {@link StackContext} instances corresponding to the full path from the root to the + * current node in the tree + * @return a boolean that will be used to decide whether to traverse the subtree rooted in the current link or not + */ + default VisitOutcome visitPre(List> stack) { return VisitOutcome.CONTINUE; } + + /** + * This method will be called for each node using + * a Depth-first post-oder algorithm + * @param stack is a list of {@link StackContext} instances corresponding to the full path from the root to the + * current node in the tree + */ + default void visitPost(List> stack) {} +} diff --git a/src/main/java/net/woggioni/jwo/tree/TreeWalker.java b/src/main/java/net/woggioni/jwo/tree/TreeWalker.java new file mode 100644 index 0000000..1134f48 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/tree/TreeWalker.java @@ -0,0 +1,76 @@ +package net.woggioni.jwo.tree; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import static net.woggioni.jwo.JWO.pop; +import static net.woggioni.jwo.JWO.tail; + +@RequiredArgsConstructor +public class TreeWalker, T> { + + @RequiredArgsConstructor + private static class StackElement, T> implements StackContext { + + @Getter + final NODE node; + + Iterator childrenIterator; + + @Getter + @Setter + T context; + } + + private final TreeNodeVisitor visitor; + + /** + * This methods does the actual job of traversing the tree calling the methods of the provided + * {@link TreeNodeVisitor} instance + * @param root the root node of the tree + */ + public void walk(NODE root) { + List> stack = new ArrayList<>(); + StackElement rootStackElement = new StackElement<>(root); + stack.add(rootStackElement); + List> publicStack = Collections.unmodifiableList(stack); + switch (visitor.visitPre(publicStack)) { + case CONTINUE: + rootStackElement.childrenIterator = root.children(); + break; + case SKIP: + rootStackElement.childrenIterator = null; + break; + case EARLY_EXIT: + return; + } + while(!stack.isEmpty()) { + StackElement lastElement = tail(stack); + if(lastElement.childrenIterator != null && lastElement.childrenIterator.hasNext()) { + NODE childNode = lastElement.childrenIterator.next(); + StackElement childStackElement = + new StackElement<>(childNode); + stack.add(childStackElement); + switch (visitor.visitPre(publicStack)) { + case CONTINUE: + childStackElement.childrenIterator = childNode.children(); + break; + case SKIP: + childStackElement.childrenIterator = null; + break; + case EARLY_EXIT: + return; + } + } else { + visitor.visitPost(publicStack); + pop(stack); + } + } + } +} diff --git a/src/main/java/net/woggioni/jwo/tuple/MutableTuple2.java b/src/main/java/net/woggioni/jwo/tuple/MutableTuple2.java new file mode 100644 index 0000000..840a0a2 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/tuple/MutableTuple2.java @@ -0,0 +1,21 @@ +package net.woggioni.jwo.tuple; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.Comparator; + +@EqualsAndHashCode +@NoArgsConstructor +@AllArgsConstructor +public class MutableTuple2 { + public T _1; + public U _2; + + public static , Y extends Comparable> Comparator> getComparator(Class cls1, Class cls2) { + return Comparator + .comparing((MutableTuple2 t) -> t._1) + .thenComparing((MutableTuple2 t) -> t._2); + } +} diff --git a/src/main/java/net/woggioni/jwo/tuple/MutableTuple3.java b/src/main/java/net/woggioni/jwo/tuple/MutableTuple3.java new file mode 100644 index 0000000..a78e1c0 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/tuple/MutableTuple3.java @@ -0,0 +1,24 @@ +package net.woggioni.jwo.tuple; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.Comparator; + +@EqualsAndHashCode +@NoArgsConstructor +@AllArgsConstructor +public class MutableTuple3 { + public T _1; + public U _2; + public V _3; + + public static , Y extends Comparable, Z extends Comparable> Comparator> getComparator(Class cls1, Class cls2, Class cls3) { + return Comparator + .comparing((MutableTuple3 t) -> t._1) + .thenComparing((MutableTuple3 t) -> t._2) + .thenComparing((MutableTuple3 t) -> t._3); + } +} + diff --git a/src/main/java/net/woggioni/jwo/tuple/Tuple2.java b/src/main/java/net/woggioni/jwo/tuple/Tuple2.java new file mode 100644 index 0000000..2852663 --- /dev/null +++ b/src/main/java/net/woggioni/jwo/tuple/Tuple2.java @@ -0,0 +1,19 @@ +package net.woggioni.jwo.tuple; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; + +import java.util.Comparator; + +@EqualsAndHashCode +@RequiredArgsConstructor +public class Tuple2 { + public final T _1; + public final U _2; + + public static , Y extends Comparable> Comparator> getComparator(Class cls1, Class cls2) { + return Comparator + .comparing((Tuple2 t) -> t._1) + .thenComparing((Tuple2 t) -> t._2); + } +} diff --git a/src/main/java/net/woggioni/jwo/tuple/Tuple3.java b/src/main/java/net/woggioni/jwo/tuple/Tuple3.java new file mode 100644 index 0000000..3658f4d --- /dev/null +++ b/src/main/java/net/woggioni/jwo/tuple/Tuple3.java @@ -0,0 +1,21 @@ +package net.woggioni.jwo.tuple; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; + +import java.util.Comparator; + +@EqualsAndHashCode +@RequiredArgsConstructor +public class Tuple3 { + public final T _1; + public final U _2; + public final V _3; + + public static , Y extends Comparable, Z extends Comparable> Comparator> getComparator(Class cls1, Class cls2, Class cls3) { + return Comparator + .comparing((Tuple3 t) -> t._1) + .thenComparing((Tuple3 t) -> t._2) + .thenComparing((Tuple3 t) -> t._3); + } +} diff --git a/src/test/java/net/woggioni/jwo/utils/JWOTest.java b/src/test/java/net/woggioni/jwo/utils/JWOTest.java new file mode 100644 index 0000000..82892ab --- /dev/null +++ b/src/test/java/net/woggioni/jwo/utils/JWOTest.java @@ -0,0 +1,86 @@ +package net.woggioni.jwo.utils; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import net.woggioni.jwo.Chronometer; +import net.woggioni.jwo.JWO; +import org.junit.Assert; +import org.junit.Test; + +import java.io.*; +import java.net.URI; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class JWOTest { + + @Test + public void flatMapTest() { + Stream s = Stream.of(3, 4); + List l = JWO.flatMap(s, (n) -> { + if (n > 3) return Optional.of(n); + else return Optional.empty(); + }).collect(Collectors.toList()); + Assert.assertEquals(Collections.singletonList(4), l); + } + + @Test + public void optional2StreamTest() { + Integer integer = 3; + Optional s = Optional.of(integer); + JWO.optional2Stream(s).forEach(n -> Assert.assertEquals(integer, n)); + s = Optional.empty(); + JWO.optional2Stream(s).forEach(n -> Assert.fail()); + } + + @Test + @SneakyThrows + public void testRenderTemplate() { + Map valuesMap = new HashMap(); + valuesMap.put("author", "John Doe"); + 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!\n"; + try (Reader reader = new InputStreamReader( + JWOTest.class.getResourceAsStream("/render_template_test.txt"))) { + String rendered = JWO.renderTemplate(reader, valuesMap); + Assert.assertEquals(expected, rendered); + } + try (Reader reader = new InputStreamReader( + JWOTest.class.getResourceAsStream("/render_template_test.txt"))) { + String rendered = JWO.renderTemplate(JWO.readAll(reader), valuesMap); + Assert.assertEquals(expected, rendered); + } + } + + + public static String renderTemplateNaive(String template, Map valuesMap){ + StringBuffer formatter = new StringBuffer(template); + Object absent = new Object(); + + Matcher matcher = Pattern.compile("\\$\\{(\\w+)}").matcher(template); + + while (matcher.find()) { + String key = matcher.group(1); + + String formatKey = String.format("${%s}", key); + int index = formatter.indexOf(formatKey); + + // If the key is present: + // - If the value is not null, then replace the variable for the value + // - If the value is null, replace the variable for empty string + // If the key is not present, leave the variable untouched. + if (index != -1) { + Object value = valuesMap.getOrDefault(key, absent); + if(value != absent) { + String valueStr = value != null ? value.toString() : ""; + formatter.replace(index, index + formatKey.length(), valueStr); + } + } + } + return formatter.toString(); + } +} diff --git a/src/test/java/net/woggioni/jwo/utils/io/CircularBufferTest.java b/src/test/java/net/woggioni/jwo/utils/io/CircularBufferTest.java new file mode 100644 index 0000000..4a6f9df --- /dev/null +++ b/src/test/java/net/woggioni/jwo/utils/io/CircularBufferTest.java @@ -0,0 +1,37 @@ +package net.woggioni.jwo.utils.io; + +import lombok.SneakyThrows; +import net.woggioni.jwo.io.CircularBuffer; +import org.junit.Assert; +import org.junit.Test; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.util.Random; + +public class CircularBufferTest { + + @Test + @SneakyThrows + public void test() { + MessageDigest streamDigest = MessageDigest.getInstance("MD5"), outputDigest = MessageDigest.getInstance("MD5"); + InputStream is = new DigestInputStream(getClass().getResourceAsStream("/render_template_test.txt"), streamDigest); + CircularBuffer cb = new CircularBuffer(new InputStreamReader(is), 32); + Random rand = new Random(); + while (true) { + int b = cb.next(); + if (b < 0) break; + if (rand.nextInt() % 2 == 0) { + cb.prev(); + } else { + char c = (char) b; + outputDigest.update((byte) b); + System.out.print(c); + } + } + System.out.println(); + Assert.assertArrayEquals(streamDigest.digest(), outputDigest.digest()); + } +} diff --git a/src/test/java/net/woggioni/jwo/utils/tree/TreeWalkerTest.java b/src/test/java/net/woggioni/jwo/utils/tree/TreeWalkerTest.java new file mode 100644 index 0000000..99cec23 --- /dev/null +++ b/src/test/java/net/woggioni/jwo/utils/tree/TreeWalkerTest.java @@ -0,0 +1,110 @@ +package net.woggioni.jwo.utils.tree; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.woggioni.jwo.tree.StackContext; +import net.woggioni.jwo.tree.TreeNode; +import net.woggioni.jwo.tree.TreeNodeVisitor; +import net.woggioni.jwo.tree.TreeWalker; +import net.woggioni.jwo.tuple.Tuple2; +import org.junit.Assert; +import org.junit.Test; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@RequiredArgsConstructor +class Node implements TreeNode { + @Getter + private final Integer id; + private final List children; + + @Override + public Iterator children() { + return children.iterator(); + } +} + + +public class TreeWalkerTest { + + private Map> parentChildRelationshipMap = + Stream.of( + new Tuple2<>(1, Collections.singletonList(2)), + new Tuple2<>(2, Collections.singletonList(3)), + new Tuple2<>(3, Arrays.asList(4, 5)), + new Tuple2<>(4, Arrays.asList(6, 7)), + new Tuple2<>(5, Collections.singletonList(8)) + ).collect(Collectors.toMap(t -> t._1, t -> t._2)); + + private Map testNodeMap = parentChildRelationshipMap.entrySet().stream() + .map(entry -> newNode(entry.getKey(), entry.getValue())) + .collect(Collectors.toMap(Node::getId, Function.identity())); + + private Node newNode(int id, List children) { + if(children == null) { + return new Node(id, Collections.emptyList()); + } else { + return new Node(id, children.stream() + .map(nodeId -> newNode(nodeId, parentChildRelationshipMap.get(nodeId))) + .collect(Collectors.toList())); + } + } + + @Test + public void treeTraversalOrderTest() { + List expected_pre_sequence = Stream.of(1, 2, 3, 4, 6, 7, 5, 8) + .collect(Collectors.toList()); + List expected_post_sequence = Stream.of(6, 7, 4, 8, 5, 3, 2, 1) + .collect(Collectors.toList()); + Iterator it_pre = expected_pre_sequence.iterator(); + Iterator it_post = expected_post_sequence.iterator(); + TreeNodeVisitor nodeVisitor = new TreeNodeVisitor() { + @Override + public VisitOutcome visitPre(List> stackContextList) { + Assert.assertTrue(it_pre.hasNext()); + Assert.assertEquals(it_pre.next(), + stackContextList.get(stackContextList.size() - 1).getNode().getId()); + return VisitOutcome.CONTINUE; + } + + @Override + public void visitPost(List> stackContextList) { + Assert.assertTrue(it_post.hasNext()); + Assert.assertEquals(it_post.next(), + stackContextList.get(stackContextList.size() - 1).getNode().getId()); + } + }; + TreeWalker walker = + new TreeWalker<>(nodeVisitor); + walker.walk(testNodeMap.get(1)); + Assert.assertFalse(it_pre.hasNext()); + Assert.assertFalse(it_post.hasNext()); + } + + @Test + public void filterTest() { + List expected_pre_sequence = Stream.of(1, 2, 3, 4, 5, 8) + .collect(Collectors.toList()); + Iterator it = expected_pre_sequence.iterator(); + TreeNodeVisitor linkVisitor = new TreeNodeVisitor() { + @Override + public VisitOutcome visitPre(List> nodePath) { + Assert.assertTrue(it.hasNext()); + Integer id = nodePath.get(nodePath.size() - 1).getNode().getId(); + Assert.assertEquals(it.next(), id); + if(Objects.equals(4, nodePath.get(nodePath.size() - 1).getNode().getId())) { + return VisitOutcome.SKIP; + } else { + return VisitOutcome.CONTINUE; + } + } + }; + TreeWalker walker = + new TreeWalker<>(linkVisitor); + walker.walk(testNodeMap.get(1)); + Assert.assertFalse(it.hasNext()); + } +} diff --git a/src/test/resources/render_template_test.txt b/src/test/resources/render_template_test.txt new file mode 100644 index 0000000..560aa12 --- /dev/null +++ b/src/test/resources/render_template_test.txt @@ -0,0 +1 @@ +This is a ${adjective} test made by ${author} on ${date}. It's really ${adjective}!