From b212fd2559216fcf25624c3edad4f7d2490032a6 Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Fri, 9 Dec 2022 21:31:13 +0800 Subject: [PATCH] improved `renderTemplate` method --- src/main/java/net/woggioni/jwo/JWO.java | 119 +++++++++++++++++--- src/test/java/net/woggioni/jwo/JWOTest.java | 68 ++++++++++- src/test/resources/render_template_test.txt | 3 + 3 files changed, 173 insertions(+), 17 deletions(-) diff --git a/src/main/java/net/woggioni/jwo/JWO.java b/src/main/java/net/woggioni/jwo/JWO.java index 67c706b..2df0e95 100644 --- a/src/main/java/net/woggioni/jwo/JWO.java +++ b/src/main/java/net/woggioni/jwo/JWO.java @@ -34,7 +34,6 @@ import java.nio.file.attribute.FileAttribute; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.cert.X509Certificate; -import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.EnumSet; @@ -46,6 +45,8 @@ import java.util.Map; import java.util.MissingFormatArgumentException; import java.util.Objects; import java.util.Optional; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; @@ -293,27 +294,96 @@ public class JWO { * "This template was created by John Doe." */ public static String renderTemplate(String template, Map valuesMap) { + return renderTemplate(template, valuesMap, null); + } + + public static int indexOfWithEscape(String haystack, char needle, char escape, int begin, int end) { + int result = -1; + int cursor = begin; + if(end == 0) { + end = haystack.length(); + } + int escapeCount = 0; + while(cursor < end) { + char c = haystack.charAt(cursor); + if(escapeCount > 0) { + --escapeCount; + if(c == escape) { + result = -1; + } + } else if(escapeCount == 0) { + if (c == escape) { + ++escapeCount; + } + if (c == needle) { + result = cursor; + } + } + if(result >= 0 && escapeCount == 0) { + break; + } + ++cursor; + } + return result; + } + + public static String renderTemplate( + String template, + Map valuesMap, + Map> dictMap) { 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) == '{') { + while(cursor < template.length()) { + int nextPlaceHolder = indexOfWithEscape(template, '$', '$', cursor, template.length()); + if (nextPlaceHolder < 0) { + nextPlaceHolder = template.length(); + } + while (cursor < nextPlaceHolder) { + char ch = template.charAt(cursor++); + sb.append(ch); + } + if (cursor + 1 < template.length() && template.charAt(cursor + 1) == '{') { + String key; + String context = null; + String defaultValue = null; + Object value; int end = template.indexOf('}', cursor + 1); - key = template.substring(cursor + 2, end); - Object value = valuesMap.getOrDefault(key, absent); + int colon; + if (dictMap == null) + colon = -1; + else { + colon = indexOfWithEscape(template, ':', '\\', cursor + 1, template.length()); + if (colon >= end) colon = -1; + } + if (colon < 0) { + key = template.substring(cursor + 2, end); + value = valuesMap.getOrDefault(key, absent); + } else { + context = template.substring(cursor + 2, colon); + int secondColon = indexOfWithEscape(template, ':', '\\', colon + 1, end); + if(secondColon < 0) { + key = template.substring(colon + 1, end); + } else { + key = template.substring(colon + 1, secondColon); + defaultValue = template.substring(secondColon + 1, end); + } + value = Optional.ofNullable(dictMap.get(context)) + .map(m -> m.get(key)) + .orElse(absent); + } if (value != absent) { sb.append(value.toString()); } else { - raise(MissingFormatArgumentException.class, "Missing value for placeholder '%s'", key); + if (defaultValue != null) { + sb.append(defaultValue); + } else { + raise(MissingFormatArgumentException.class, "Missing value for placeholder '%s'", + context == null ? key : context + ':' + key); + } } cursor = end + 1; - } else { - sb.append(template.charAt(cursor++)); } } return sb.toString(); @@ -329,14 +399,20 @@ public class JWO { * "This template was created by John Doe." */ @SneakyThrows - public static String renderTemplate(Reader reader, Map valuesMap) { + public static String renderTemplate(Reader reader, + Map valuesMap, + Map> dictMap) { 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); + return renderTemplate(sb.toString(), valuesMap, dictMap); + } + public static String renderTemplate(Reader reader, + Map valuesMap) { + return renderTemplate(reader, valuesMap, null); } @SneakyThrows @@ -871,4 +947,19 @@ public class JWO { public static Runnable compose(Supplier sup, Consumer con) { return () -> con.accept(sup.get()); } + + public static T loadService(Class serviceClass) { + return StreamSupport.stream(ServiceLoader.load(serviceClass).spliterator(), false) + .findFirst() + .orElseThrow( + () -> newThrowable( + ServiceConfigurationError.class, + "Unable to find a valid implementation of '%s'", + serviceClass.getName())); + } + + public static Optional zip(Optional opt1, Optional opt2, BiFun cb) { + if (!opt1.isPresent() || !opt2.isPresent()) return Optional.empty(); + else return Optional.ofNullable(cb.apply(opt1.get(), opt2.get())); + } } diff --git a/src/test/java/net/woggioni/jwo/JWOTest.java b/src/test/java/net/woggioni/jwo/JWOTest.java index de98aad..f45875a 100644 --- a/src/test/java/net/woggioni/jwo/JWOTest.java +++ b/src/test/java/net/woggioni/jwo/JWOTest.java @@ -1,5 +1,6 @@ package net.woggioni.jwo; +import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; @@ -7,6 +8,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import java.io.*; import java.lang.reflect.Method; @@ -20,6 +23,9 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; +import static net.woggioni.jwo.CollectionUtils.immutableList; +import static net.woggioni.jwo.CollectionUtils.newArrayList; + public class JWOTest { @Test @@ -43,6 +49,47 @@ public class JWOTest { ); } + + @RequiredArgsConstructor + enum IndexOfWithEscapeTestCase { + SIMPLE(" dsds $sdsa \\$dfivbdsf \\\\$sdgsga", '$', '\\', + immutableList(6, 25)), + SIMPLE2("asdasd$$vdfv$", '$', '$', + immutableList(12)), + NO_NEEDLE("asdasd$$vdfv$", '#', '\\', + immutableList()), + ESCAPED_NEEDLE("asdasd$$vdfv$#sdfs", '#', '$', + immutableList()), + NOT_ESCAPED_NEEDLE("asdasd$$#vdfv$#sdfs", '#', '$', + immutableList(8)), + + SDFSD("\n${sys:user.home}${env:HOME}", ':', '\\', + immutableList(6, 22)) + + ; + final String haystack; + final Character needle; + + final Character escape; + + final List solution; + } + + @ParameterizedTest + @EnumSource(IndexOfWithEscapeTestCase.class) + public void testIndexOfWithEscape(IndexOfWithEscapeTestCase testCase) { + String haystack = testCase.haystack; + List solution = newArrayList(); + int i = 0; + while(true) { + i = JWO.indexOfWithEscape(haystack, testCase.needle, testCase.escape, i, haystack.length()); + if(i < 0) break; + solution.add(i); + ++i; + } + Assertions.assertEquals(testCase.solution, solution); + } + @Test @SneakyThrows public void testRenderTemplate() { @@ -50,15 +97,30 @@ public class JWOTest { 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"; + String expected = """ + This is a simple test made by John Doe on 2020-03-25 16:22. It's really simple! + /home/user + /home/user + defaultValue + """; + Map> contextMap = new MapBuilder>() + .entry("env", + new MapBuilder() + .entry("HOME", "/home/user") + .build(TreeMap::new, Collections::unmodifiableMap) + ) + .entry("sys", + new MapBuilder() + .entry("user.home", "/home/user") + .build(TreeMap::new, Collections::unmodifiableMap) ).build(TreeMap::new, Collections::unmodifiableMap); try (Reader reader = new InputStreamReader( JWOTest.class.getResourceAsStream("/render_template_test.txt"))) { - String rendered = JWO.renderTemplate(reader, valuesMap); + String rendered = JWO.renderTemplate(reader, valuesMap, contextMap); Assertions.assertEquals(expected, rendered); } try (Reader reader = new InputStreamReader( JWOTest.class.getResourceAsStream("/render_template_test.txt"))) { - String rendered = JWO.renderTemplate(JWO.readAll(reader), valuesMap); + String rendered = JWO.renderTemplate(JWO.readAll(reader), valuesMap, contextMap); Assertions.assertEquals(expected, rendered); } } diff --git a/src/test/resources/render_template_test.txt b/src/test/resources/render_template_test.txt index 560aa12..8cf0a24 100644 --- a/src/test/resources/render_template_test.txt +++ b/src/test/resources/render_template_test.txt @@ -1 +1,4 @@ This is a ${adjective} test made by ${author} on ${date}. It's really ${adjective}! +${sys:user.home} +${env:HOME} +${env:SOME_STRANGE_VAR:defaultValue}