improved renderTemplate method

This commit is contained in:
2022-12-09 21:31:13 +08:00
parent 806a96f6d5
commit b212fd2559
3 changed files with 173 additions and 17 deletions

View File

@@ -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<String, Object> 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<String, Object> valuesMap,
Map<String, Map<String, Object>> dictMap) {
StringBuilder sb = new StringBuilder();
Object absent = new Object();
int cursor = 0;
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;
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) == '{') {
String context = null;
String defaultValue = null;
Object value;
int end = template.indexOf('}', cursor + 1);
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);
Object value = valuesMap.getOrDefault(key, absent);
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<String, Object> valuesMap) {
public static String renderTemplate(Reader reader,
Map<String, Object> valuesMap,
Map<String, Map<String, Object>> 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<String, Object> valuesMap) {
return renderTemplate(reader, valuesMap, null);
}
@SneakyThrows
@@ -871,4 +947,19 @@ public class JWO {
public static <T> Runnable compose(Supplier<T> sup, Consumer<T> con) {
return () -> con.accept(sup.get());
}
public static <T> T loadService(Class<T> 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 <T, U, R> Optional<R> zip(Optional<T> opt1, Optional<U> opt2, BiFun<T, U, R> cb) {
if (!opt1.isPresent() || !opt2.isPresent()) return Optional.empty();
else return Optional.ofNullable(cb.apply(opt1.get(), opt2.get()));
}
}

View File

@@ -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<Integer> solution;
}
@ParameterizedTest
@EnumSource(IndexOfWithEscapeTestCase.class)
public void testIndexOfWithEscape(IndexOfWithEscapeTestCase testCase) {
String haystack = testCase.haystack;
List<Integer> 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<String, Map<String, Object>> contextMap = new MapBuilder<String, Map<String, Object>>()
.entry("env",
new MapBuilder<String, String>()
.entry("HOME", "/home/user")
.build(TreeMap::new, Collections::unmodifiableMap)
)
.entry("sys",
new MapBuilder<String, String>()
.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);
}
}

View File

@@ -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}