From c4f3f955179cd217c6e7273561d06468b33e8973 Mon Sep 17 00:00:00 2001 From: Paul Duncan Date: Thu, 6 Sep 2018 18:08:34 -0400 Subject: update errors, fix filters, add tests --- java/src/main/java/org/pablotron/luigi/Cache.java | 27 ++-- java/src/main/java/org/pablotron/luigi/Filter.java | 173 +++++++++++++++++---- .../main/java/org/pablotron/luigi/FilterError.java | 7 + .../main/java/org/pablotron/luigi/Template.java | 34 ++++ .../org/pablotron/luigi/UnknownEntryError.java | 13 ++ .../org/pablotron/luigi/UnknownFilterError.java | 7 + .../java/org/pablotron/luigi/UnknownKeyError.java | 7 + .../org/pablotron/luigi/UnknownTemplateError.java | 7 + .../org/pablotron/luigi/actions/FilterAction.java | 6 +- .../java/org/pablotron/luigi/tests/CacheTest.java | 46 ++++++ .../pablotron/luigi/tests/DefaultFiltersTest.java | 53 +++++++ .../org/pablotron/luigi/tests/FiltersTest.java | 88 +++++++++++ .../org/pablotron/luigi/tests/TemplateTest.java | 18 +++ 13 files changed, 439 insertions(+), 47 deletions(-) create mode 100644 java/src/main/java/org/pablotron/luigi/FilterError.java create mode 100644 java/src/main/java/org/pablotron/luigi/UnknownEntryError.java create mode 100644 java/src/main/java/org/pablotron/luigi/UnknownFilterError.java create mode 100644 java/src/main/java/org/pablotron/luigi/UnknownKeyError.java create mode 100644 java/src/main/java/org/pablotron/luigi/UnknownTemplateError.java create mode 100644 java/src/test/java/org/pablotron/luigi/tests/CacheTest.java create mode 100644 java/src/test/java/org/pablotron/luigi/tests/DefaultFiltersTest.java create mode 100644 java/src/test/java/org/pablotron/luigi/tests/FiltersTest.java diff --git a/java/src/main/java/org/pablotron/luigi/Cache.java b/java/src/main/java/org/pablotron/luigi/Cache.java index 97fa0ab..4a591bc 100644 --- a/java/src/main/java/org/pablotron/luigi/Cache.java +++ b/java/src/main/java/org/pablotron/luigi/Cache.java @@ -28,24 +28,25 @@ public final class Cache { final String key, final Map args ) throws LuigiError { - Template t; + // run template with args + return get(key).run(args); + } + + public boolean containsKey(final String key) { + return strings.containsKey(key); + } - if (templates.containsKey(key)) { - // get template - t = templates.get(key); - } else { + public Template get(final String key) throws LuigiError { + if (!templates.containsKey(key)) { // make sure template exists if (!strings.containsKey(key)) - throw new LuigiError("unknown template: " + key); - - // create template - t = new Template(strings.get(key), filters); + throw new UnknownTemplateError(key); - // cache template - templates.put(key, t); + // create and cache template + templates.put(key, new Template(strings.get(key), filters)); } - // run template with args - return t.run(args); + // get template + return templates.get(key); } }; diff --git a/java/src/main/java/org/pablotron/luigi/Filter.java b/java/src/main/java/org/pablotron/luigi/Filter.java index b00491f..d2cd432 100644 --- a/java/src/main/java/org/pablotron/luigi/Filter.java +++ b/java/src/main/java/org/pablotron/luigi/Filter.java @@ -2,12 +2,26 @@ package org.pablotron.luigi; import java.util.Map; import java.util.HashMap; +import java.nio.charset.Charset; public final class Filter { public interface Handler { - public String filter(String val, String args[], Map row); + public String filter( + String val, + String args[], + Map row + ) throws FilterError; }; + protected static byte[] getBytes(final String val, final String args[]) { + final Charset charset = (args.length > 0) ? Charset.forName(args[0]) : Charset.defaultCharset(); + return val.getBytes(charset); + } + + protected static int toUInt(final byte b) { + return (b < 0) ? (256 + b) : b; + } + public static Map FILTERS = new HashMap() {{ put("null", new Handler() { public String filter(String val, String args[], Map row) { @@ -48,41 +62,136 @@ public final class Filter { put("h", new Handler() { public String filter(String val, String args[], Map row) { - StringBuilder r = new StringBuilder(val.length()); - - for (int i = 0, l = val.length(); i < l; i++) { - char c = val.charAt(i); - - switch (c) { - case '&': - r.append("&"); - break; - case '<': - r.append("<"); - break; - case '>': - r.append(">"); - break; - case '\'': - r.append("'"); - break; - case '"': - r.append("""); - break; - default: - r.append(c); + StringBuilder r = new StringBuilder(); + final byte bytes[] = getBytes(val, args); + + for (int i = 0, l = bytes.length; i < l; i++) { + final byte b = bytes[i]; + + switch (b) { + case '&': + r.append("&"); + break; + case '<': + r.append("<"); + break; + case '>': + r.append(">"); + break; + case '\'': + r.append("'"); + break; + case '"': + r.append("""); + break; + default: + if (b < 32 || b > 126) { + r.append(String.format("&#%d;", toUInt(b))); + } else { + final byte bs[] = {b}; + r.append(new String(bs)); + } } } return r.toString(); -/* - * return val - * .replace("&", "&") - * .replace("<", "<") - * .replace(">", ">") - * .replace("'", "'") - * .replace("\"", """); - */ + } + }); + + put("u", new Handler() { + public String filter(String val, String args[], Map row) { + final StringBuilder r = new StringBuilder(); + final byte bytes[] = getBytes(val, args); + + for (int i = 0, l = bytes.length; i < l; i++) { + final byte b = bytes[i]; + + switch (b) { + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + case 'G': + case 'H': + case 'I': + case 'J': + case 'K': + case 'L': + case 'M': + case 'N': + case 'O': + case 'P': + case 'Q': + case 'R': + case 'S': + case 'T': + case 'U': + case 'V': + case 'W': + case 'X': + case 'Y': + case 'Z': + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + case 'g': + case 'h': + case 'i': + case 'j': + case 'k': + case 'l': + case 'm': + case 'n': + case 'o': + case 'p': + case 'q': + case 'r': + case 's': + case 't': + case 'u': + case 'v': + case 'w': + case 'x': + case 'y': + case 'z': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + case '_': + case '.': + case '~': + // unreserved character + final byte bs[] = {b}; + r.append(new String(bs)); + break; + case ' ': + r.append("+"); + break; + default: + r.append(String.format("%%%02X", toUInt(b))); + } + } + + return r.toString(); + } + }); + + put("trim", new Handler() { + public String filter(String val, String args[], Map row) { + return val.replaceAll("\\A\\s+|\\s+\\Z", ""); } }); }}; diff --git a/java/src/main/java/org/pablotron/luigi/FilterError.java b/java/src/main/java/org/pablotron/luigi/FilterError.java new file mode 100644 index 0000000..cb631a7 --- /dev/null +++ b/java/src/main/java/org/pablotron/luigi/FilterError.java @@ -0,0 +1,7 @@ +package org.pablotron.luigi; + +public class FilterError extends LuigiError { + public FilterError(final String message) { + super(message); + } +}; diff --git a/java/src/main/java/org/pablotron/luigi/Template.java b/java/src/main/java/org/pablotron/luigi/Template.java index 9af3a74..e5f25a9 100644 --- a/java/src/main/java/org/pablotron/luigi/Template.java +++ b/java/src/main/java/org/pablotron/luigi/Template.java @@ -47,4 +47,38 @@ public final class Template { public String toString() { return this.template; } + + public static String run( + final String template, + final Map args + ) throws LuigiError { + return Template.run(template, args, Filter.FILTERS); + } + + public static void run( + final String template, + final Map args, + final ResultHandler r + ) throws LuigiError { + run(template, args, Filter.FILTERS, r); + } + + public static String run( + final String template, + final Map args, + final Map filters + ) throws LuigiError { + final Template t = new Template(template, filters); + return t.run(args); + } + + public static void run( + final String template, + final Map args, + final Map filters, + final ResultHandler r + ) throws LuigiError { + final Template t = new Template(template, filters); + t.run(args, r); + } }; diff --git a/java/src/main/java/org/pablotron/luigi/UnknownEntryError.java b/java/src/main/java/org/pablotron/luigi/UnknownEntryError.java new file mode 100644 index 0000000..8b8a0fd --- /dev/null +++ b/java/src/main/java/org/pablotron/luigi/UnknownEntryError.java @@ -0,0 +1,13 @@ +package org.pablotron.luigi; + +public class UnknownEntryError extends LuigiError { + public final String type; + public final String name; + + public UnknownEntryError(final String type, final String name) { + super(String.format("unknown %s: %s", type, name)); + + this.type = type; + this.name = name; + } +}; diff --git a/java/src/main/java/org/pablotron/luigi/UnknownFilterError.java b/java/src/main/java/org/pablotron/luigi/UnknownFilterError.java new file mode 100644 index 0000000..a56a6c1 --- /dev/null +++ b/java/src/main/java/org/pablotron/luigi/UnknownFilterError.java @@ -0,0 +1,7 @@ +package org.pablotron.luigi; + +public class UnknownFilterError extends UnknownEntryError { + public UnknownFilterError(final String name) { + super("filter", name); + } +}; diff --git a/java/src/main/java/org/pablotron/luigi/UnknownKeyError.java b/java/src/main/java/org/pablotron/luigi/UnknownKeyError.java new file mode 100644 index 0000000..88da21a --- /dev/null +++ b/java/src/main/java/org/pablotron/luigi/UnknownKeyError.java @@ -0,0 +1,7 @@ +package org.pablotron.luigi; + +public class UnknownKeyError extends UnknownEntryError { + public UnknownKeyError(final String name) { + super("key", name); + } +}; diff --git a/java/src/main/java/org/pablotron/luigi/UnknownTemplateError.java b/java/src/main/java/org/pablotron/luigi/UnknownTemplateError.java new file mode 100644 index 0000000..15dae97 --- /dev/null +++ b/java/src/main/java/org/pablotron/luigi/UnknownTemplateError.java @@ -0,0 +1,7 @@ +package org.pablotron.luigi; + +public class UnknownTemplateError extends UnknownEntryError { + public UnknownTemplateError(final String name) { + super("template", name); + } +}; diff --git a/java/src/main/java/org/pablotron/luigi/actions/FilterAction.java b/java/src/main/java/org/pablotron/luigi/actions/FilterAction.java index 9115a68..19565dc 100644 --- a/java/src/main/java/org/pablotron/luigi/actions/FilterAction.java +++ b/java/src/main/java/org/pablotron/luigi/actions/FilterAction.java @@ -5,6 +5,8 @@ import org.pablotron.luigi.actions.Action; import org.pablotron.luigi.FilterReference; import org.pablotron.luigi.Filter; import org.pablotron.luigi.LuigiError; +import org.pablotron.luigi.UnknownFilterError; +import org.pablotron.luigi.UnknownKeyError; public final class FilterAction implements Action { private final String key; @@ -21,7 +23,7 @@ public final class FilterAction implements Action { ) throws LuigiError { // check for key if (!args.containsKey(key)) - throw new LuigiError("unknown key: " + key); + throw new UnknownKeyError(key); // reduce value to result String r = args.get(key); @@ -29,7 +31,7 @@ public final class FilterAction implements Action { // get/check filter Filter.Handler f = filters.get(this.filters[i].name); if (f == null) - throw new LuigiError("unknown filter: " + this.filters[i].name); + throw new UnknownFilterError(this.filters[i].name); // run filter r = f.filter(r, this.filters[i].args, args); diff --git a/java/src/test/java/org/pablotron/luigi/tests/CacheTest.java b/java/src/test/java/org/pablotron/luigi/tests/CacheTest.java new file mode 100644 index 0000000..c91b727 --- /dev/null +++ b/java/src/test/java/org/pablotron/luigi/tests/CacheTest.java @@ -0,0 +1,46 @@ +import java.util.Map; +import java.util.HashMap; + +import org.pablotron.luigi.Template; +import org.pablotron.luigi.Filter; +import org.pablotron.luigi.Cache; +import org.pablotron.luigi.LuigiError; +import org.pablotron.luigi.UnknownKeyError; +import org.pablotron.luigi.UnknownFilterError; +import org.pablotron.luigi.UnknownTemplateError; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; + +public final class CacheTest { + private static Map TEST_ARGS = new HashMap() {{ + put("bar", "foo"); + }}; + + private static Map TEST_TEMPLATES = new HashMap() {{ + put("foo", "foo%{bar}foo"); + put("foo-custom", "foo%{bar | custom-filter}foo"); + }}; + + private static Map TEST_FILTERS = new HashMap() {{ + put("custom-filter", new Filter.Handler() { + public String filter(String val, String args[], Map row) { + return String.format("-custom-%s-filter-", val); + } + }); + }}; + + @Test + public void testCache() throws LuigiError { + final Cache cache = new Cache(TEST_TEMPLATES); + + assertEquals("foofoofoo", cache.run("foo", TEST_ARGS)); + } + + @Test + public void testCacheWithCustomFilters() throws LuigiError { + final Cache cache = new Cache(TEST_TEMPLATES, TEST_FILTERS); + assertEquals("foo-custom-foo-filter-foo", cache.run("foo-custom", TEST_ARGS)); + } +}; diff --git a/java/src/test/java/org/pablotron/luigi/tests/DefaultFiltersTest.java b/java/src/test/java/org/pablotron/luigi/tests/DefaultFiltersTest.java new file mode 100644 index 0000000..c839ded --- /dev/null +++ b/java/src/test/java/org/pablotron/luigi/tests/DefaultFiltersTest.java @@ -0,0 +1,53 @@ +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; + +import org.pablotron.luigi.LuigiError; +import org.pablotron.luigi.Template; +import org.pablotron.luigi.Filter; +import org.pablotron.luigi.FilterError; +import org.pablotron.luigi.ResultHandler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.Test; + +public final class DefaultFiltersTest { + private static final class TestCase { + private final String name; + private final String arg; + public final String expect; + + public TestCase(final String name, final String arg, final String expect) { + this.name = name; + this.arg = arg; + this.expect = expect; + } + + public String run() throws LuigiError { + final Map args = new HashMap(); + args.put("val", arg); + + return Template.run(String.format("%%{val|%s}", name), args); + } + }; + + private static final List TEST_CASES = new ArrayList() {{ + add(new TestCase("uc", "bar", "BAR")); + add(new TestCase("lc", "BAR", "bar")); + add(new TestCase("h", "asdf<>&\"'\u000f", "asdf<>&"'")); + add(new TestCase("u", "asdf<>&\"' \u000f", "asdf%3C%3E%26%22%27+%0F")); + add(new TestCase("trim", " \r\n\tfoo", "foo")); + add(new TestCase("trim", " \r\n\tfoo \r\n\t", "foo")); + add(new TestCase("trim", "foo \r\n\t", "foo")); + }}; + + @Test + public void testDefaultFilters() throws LuigiError { + for (final TestCase t: TEST_CASES) { + assertEquals(t.expect, t.run()); + } + } +}; diff --git a/java/src/test/java/org/pablotron/luigi/tests/FiltersTest.java b/java/src/test/java/org/pablotron/luigi/tests/FiltersTest.java new file mode 100644 index 0000000..9168529 --- /dev/null +++ b/java/src/test/java/org/pablotron/luigi/tests/FiltersTest.java @@ -0,0 +1,88 @@ +import java.util.Map; +import java.util.HashMap; + +import org.pablotron.luigi.LuigiError; +import org.pablotron.luigi.Template; +import org.pablotron.luigi.Filter; +import org.pablotron.luigi.FilterError; +import org.pablotron.luigi.ResultHandler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.Test; + + +public final class FiltersTest { + private static final Map TEST_ARGS = new HashMap() {{ + put("bar", "foo"); + }}; + + private static final Map TEST_MULTIPLE_ARGS = new HashMap() {{ + put("bar", "foo"); + put("baz", "bar"); + }}; + + private static final Map TEST_FILTERS = new HashMap() {{ + put("barify", new Filter.Handler() { + public String filter(String val, String args[], Map row) { + return String.format("bar-%s-bar", val); + } + }); + + put("wrap", new Filter.Handler() { + public String filter( + String val, + String args[], + Map row + ) throws FilterError { + switch (args.length) { + case 2: + return String.format("(%s, %s, %s)", args[0], val, args[1]); + case 1: + return String.format("(%s in %s)", val, args[0], val); + case 0: + return val; + default: + throw new FilterError("invalid filter argument count"); + } + } + }); + }}; + + @Test + public void testFilter() throws LuigiError { + final String r = Template.run("foo%{bar | lc}", TEST_ARGS); + + assertEquals("foofoo", r); + } + + @Test + public void testFilterChain() throws LuigiError { + final String r = Template.run("foo%{bar | lc | uc}", TEST_ARGS); + + assertEquals("fooFOO", r); + } + + @Test + public void testCustomFilter() throws LuigiError { + final String r = Template.run("foo%{bar | barify}", TEST_ARGS, TEST_FILTERS); + + assertEquals("foobar-foo-bar", r); + } + + @Test + public void testCustomFilterWithArgs() throws LuigiError { + // test two arguments + final String plain = Template.run("%{bar | wrap}", TEST_ARGS, TEST_FILTERS); + assertEquals("foo", plain); + + // test one argument + final String sandwich = Template.run("%{bar | wrap bread}", TEST_ARGS, TEST_FILTERS); + assertEquals("(foo in bread)", sandwich); + + // test two arguments + final String pizza = Template.run("%{bar | wrap crust cheese}", TEST_ARGS, TEST_FILTERS); + assertEquals("(crust, foo, cheese)", pizza); + } +}; diff --git a/java/src/test/java/org/pablotron/luigi/tests/TemplateTest.java b/java/src/test/java/org/pablotron/luigi/tests/TemplateTest.java index 94e259e..eadaab1 100644 --- a/java/src/test/java/org/pablotron/luigi/tests/TemplateTest.java +++ b/java/src/test/java/org/pablotron/luigi/tests/TemplateTest.java @@ -36,6 +36,13 @@ public final class TemplateTest { assertEquals("foofoo", r); } + @Test + public void testStaticRun() throws LuigiError { + final String r = Template.run("foo%{bar}", TEST_ARGS); + + assertEquals("foofoo", r); + } + private static final class TestResultHandler implements ResultHandler { private final StringBuilder sb; public TestResultHandler(final StringBuilder sb) { @@ -59,6 +66,17 @@ public final class TemplateTest { assertEquals("foofoo", sb.toString()); } + @Test + public void testStaticResultHandler() throws LuigiError { + final StringBuilder sb = new StringBuilder(); + final TestResultHandler rh = new TestResultHandler(sb); + + Template.run("foo%{bar}", TEST_ARGS, rh); + final String r = sb.toString(); + + assertEquals("foofoo", sb.toString()); + } + @Test public void testMultipleKeys() throws LuigiError { final Template t = new Template("foo%{bar}%{baz}"); -- cgit v1.2.3