improved renderTemplate
method
This commit is contained in:
@@ -34,7 +34,6 @@ import java.nio.file.attribute.FileAttribute;
|
|||||||
import java.security.DigestOutputStream;
|
import java.security.DigestOutputStream;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
@@ -46,6 +45,8 @@ import java.util.Map;
|
|||||||
import java.util.MissingFormatArgumentException;
|
import java.util.MissingFormatArgumentException;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.ServiceConfigurationError;
|
||||||
|
import java.util.ServiceLoader;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
@@ -293,27 +294,96 @@ public class JWO {
|
|||||||
* "This template was created by John Doe."
|
* "This template was created by John Doe."
|
||||||
*/
|
*/
|
||||||
public static String renderTemplate(String template, Map<String, Object> valuesMap) {
|
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();
|
StringBuilder sb = new StringBuilder();
|
||||||
Object absent = new Object();
|
Object absent = new Object();
|
||||||
|
|
||||||
int cursor = 0;
|
int cursor = 0;
|
||||||
while (cursor < template.length()) {
|
while(cursor < template.length()) {
|
||||||
String key;
|
int nextPlaceHolder = indexOfWithEscape(template, '$', '$', cursor, template.length());
|
||||||
char ch = template.charAt(cursor);
|
if (nextPlaceHolder < 0) {
|
||||||
if (ch != '$' || (cursor > 0 && template.charAt(cursor - 1) == '\\')) {
|
nextPlaceHolder = template.length();
|
||||||
sb.append(template.charAt(cursor++));
|
}
|
||||||
} else if (cursor + 1 < template.length() && template.charAt(cursor + 1) == '{') {
|
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);
|
int end = template.indexOf('}', cursor + 1);
|
||||||
key = template.substring(cursor + 2, end);
|
int colon;
|
||||||
Object value = valuesMap.getOrDefault(key, absent);
|
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) {
|
if (value != absent) {
|
||||||
sb.append(value.toString());
|
sb.append(value.toString());
|
||||||
} else {
|
} 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;
|
cursor = end + 1;
|
||||||
} else {
|
|
||||||
sb.append(template.charAt(cursor++));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
@@ -329,14 +399,20 @@ public class JWO {
|
|||||||
* "This template was created by John Doe."
|
* "This template was created by John Doe."
|
||||||
*/
|
*/
|
||||||
@SneakyThrows
|
@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();
|
StringBuilder sb = new StringBuilder();
|
||||||
char[] buf = new char[1024];
|
char[] buf = new char[1024];
|
||||||
int read;
|
int read;
|
||||||
while (!((read = reader.read(buf)) < 0)) {
|
while (!((read = reader.read(buf)) < 0)) {
|
||||||
sb.append(buf, 0, read);
|
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
|
@SneakyThrows
|
||||||
@@ -871,4 +947,19 @@ public class JWO {
|
|||||||
public static <T> Runnable compose(Supplier<T> sup, Consumer<T> con) {
|
public static <T> Runnable compose(Supplier<T> sup, Consumer<T> con) {
|
||||||
return () -> con.accept(sup.get());
|
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()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package net.woggioni.jwo;
|
package net.woggioni.jwo;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Disabled;
|
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.EnabledOnOs;
|
||||||
import org.junit.jupiter.api.condition.OS;
|
import org.junit.jupiter.api.condition.OS;
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
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.io.*;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
@@ -20,6 +23,9 @@ import java.util.regex.Pattern;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static net.woggioni.jwo.CollectionUtils.immutableList;
|
||||||
|
import static net.woggioni.jwo.CollectionUtils.newArrayList;
|
||||||
|
|
||||||
public class JWOTest {
|
public class JWOTest {
|
||||||
|
|
||||||
@Test
|
@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
|
@Test
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public void testRenderTemplate() {
|
public void testRenderTemplate() {
|
||||||
@@ -50,15 +97,30 @@ public class JWOTest {
|
|||||||
valuesMap.put("author", "John Doe");
|
valuesMap.put("author", "John Doe");
|
||||||
valuesMap.put("date", "2020-03-25 16:22");
|
valuesMap.put("date", "2020-03-25 16:22");
|
||||||
valuesMap.put("adjective", "simple");
|
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(
|
try (Reader reader = new InputStreamReader(
|
||||||
JWOTest.class.getResourceAsStream("/render_template_test.txt"))) {
|
JWOTest.class.getResourceAsStream("/render_template_test.txt"))) {
|
||||||
String rendered = JWO.renderTemplate(reader, valuesMap);
|
String rendered = JWO.renderTemplate(reader, valuesMap, contextMap);
|
||||||
Assertions.assertEquals(expected, rendered);
|
Assertions.assertEquals(expected, rendered);
|
||||||
}
|
}
|
||||||
try (Reader reader = new InputStreamReader(
|
try (Reader reader = new InputStreamReader(
|
||||||
JWOTest.class.getResourceAsStream("/render_template_test.txt"))) {
|
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);
|
Assertions.assertEquals(expected, rendered);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1 +1,4 @@
|
|||||||
This is a ${adjective} test made by ${author} on ${date}. It's really ${adjective}!
|
This is a ${adjective} test made by ${author} on ${date}. It's really ${adjective}!
|
||||||
|
${sys:user.home}
|
||||||
|
${env:HOME}
|
||||||
|
${env:SOME_STRANGE_VAR:defaultValue}
|
||||||
|
Reference in New Issue
Block a user