From b9b8203f0ed49c9ed903e462ac299dcfa44fe77d Mon Sep 17 00:00:00 2001 From: Joe Lauer Date: Wed, 25 Sep 2024 16:07:16 -0400 Subject: [PATCH] Jackson utility for serializing to/from map (which can represent a query string) --- .../fizzed/crux/jackson/QueryParamMapper.java | 82 +++++++++++ .../crux/jackson/QueryParamMapperTest.java | 139 ++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 crux-jackson/src/main/java/com/fizzed/crux/jackson/QueryParamMapper.java create mode 100644 crux-jackson/src/test/java/com/fizzed/crux/jackson/QueryParamMapperTest.java diff --git a/crux-jackson/src/main/java/com/fizzed/crux/jackson/QueryParamMapper.java b/crux-jackson/src/main/java/com/fizzed/crux/jackson/QueryParamMapper.java new file mode 100644 index 0000000..d522101 --- /dev/null +++ b/crux-jackson/src/main/java/com/fizzed/crux/jackson/QueryParamMapper.java @@ -0,0 +1,82 @@ +package com.fizzed.crux.jackson; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +public class QueryParamMapper { + + static public Map toQueryParams(ObjectMapper objectMapper, Object value) throws IOException { + + final JsonNode node = objectMapper.valueToTree(value); + + // node MUST be an object + if (!node.isObject()) { + throw new IOException("Only objects are supported"); + } + + // build a Map ourselves of this object + final Map params = new LinkedHashMap<>(); + + Iterator> fieldIterator = node.fields(); + + while (fieldIterator.hasNext()) { + Map.Entry field = fieldIterator.next(); + String fieldName = field.getKey(); + JsonNode fieldNode = field.getValue(); + + if (fieldNode.isValueNode()) { + params.put(fieldName, fieldNode.asText()); + } else if (fieldNode.isArray()) { + ArrayNode arrayNode = (ArrayNode)fieldNode; + Iterator arrayIterator = arrayNode.elements(); + StringBuilder sb = new StringBuilder(); + + while (arrayIterator.hasNext()) { + JsonNode arrayFieldNode = arrayIterator.next(); + + if (arrayFieldNode.isValueNode()) { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(arrayFieldNode.asText()); + } else { + // we can only serialize arrays 1-level deep! + throw new IOException("Only objects that contain 1-level deep of iterables are supported"); + } + } + + params.put(fieldName, sb.toString()); + } + } + + return params; + } + + static public T fromQueryParams(ObjectMapper objectMapper, Map params, Class type) throws IOException { + + // build a map of key -> values, where comma-delimited values are turned into lists + final Map data = new HashMap<>(); + + for (Map.Entry entry : params.entrySet()) { + final String key = entry.getKey(); + final String value = entry.getValue(); + + if (value != null && value.contains(",")) { + String[] tokens = value.split(","); + data.put(key, tokens); + } else { + data.put(key, value); + } + } + + return objectMapper.convertValue(data, type); + } + +} \ No newline at end of file diff --git a/crux-jackson/src/test/java/com/fizzed/crux/jackson/QueryParamMapperTest.java b/crux-jackson/src/test/java/com/fizzed/crux/jackson/QueryParamMapperTest.java new file mode 100644 index 0000000..155958d --- /dev/null +++ b/crux-jackson/src/test/java/com/fizzed/crux/jackson/QueryParamMapperTest.java @@ -0,0 +1,139 @@ +package com.fizzed.crux.jackson; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +import java.util.*; + +import static java.util.Arrays.asList; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +public class QueryParamMapperTest { + + public enum Animal { + DOG, + CAT, + SHEEP + } + + static public class A { + + private Integer id; + private String name; + private List types; + private List limits; + private Set animals; + + public Integer getId() { + return id; + } + + public A setId(Integer id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public A setName(String name) { + this.name = name; + return this; + } + + public List getTypes() { + return types; + } + + public A setTypes(List types) { + this.types = types; + return this; + } + + public List getLimits() { + return limits; + } + + public A setLimits(List limits) { + this.limits = limits; + return this; + } + + public Set getAnimals() { + return animals; + } + + public A setAnimals(Set animals) { + this.animals = animals; + return this; + } + } + + @Test + public void toQueryParams() throws Exception { + final ObjectMapper objectMapper = new ObjectMapper(); + + A a = new A(); + a.setId(1); + a.setName("Wow"); + a.setTypes(asList("A", "B", "C")); + a.setLimits(asList(1L, 2L, 3L)); + a.setAnimals(new LinkedHashSet<>(asList(Animal.DOG, Animal.CAT))); + + Map values = QueryParamMapper.toQueryParams(objectMapper, a); + + assertThat(values, hasEntry("id", "1")); + assertThat(values, hasEntry("name", "Wow")); + assertThat(values, hasEntry("types", "A,B,C")); + assertThat(values, hasEntry("limits", "1,2,3")); + assertThat(values, hasEntry("animals", "DOG,CAT")); + } + + @Test + public void fromQueryParams() throws Exception { + final ObjectMapper objectMapper = new ObjectMapper(); + + Map values = new HashMap<>(); + values.put("id", "1"); + + final A a1 = QueryParamMapper.fromQueryParams(objectMapper, values, A.class); + + assertThat(a1.getId(), is(1)); + + values.put("name", "Hello!"); + + final A a2 = QueryParamMapper.fromQueryParams(objectMapper, values, A.class); + + assertThat(a2.getId(), is(1)); + assertThat(a2.getName(), is("Hello!")); + + values.put("types", "C,D,F"); + + final A a3 = QueryParamMapper.fromQueryParams(objectMapper, values, A.class); + + assertThat(a3.getId(), is(1)); + assertThat(a3.getName(), is("Hello!")); + assertThat(a3.getTypes(), contains("C", "D", "F")); + + values.put("animals", "SHEEP,DOG"); + + final A a4 = QueryParamMapper.fromQueryParams(objectMapper, values, A.class); + + assertThat(a4.getId(), is(1)); + assertThat(a4.getName(), is("Hello!")); + assertThat(a4.getTypes(), contains("C", "D", "F")); + assertThat(a4.getAnimals(), containsInAnyOrder(Animal.SHEEP, Animal.DOG)); + + // bad enum value? + values.put("animals", "NOTFOUND"); + + try { + QueryParamMapper.fromQueryParams(objectMapper, values, A.class); + } catch (IllegalArgumentException e) { + // expected + } + } + +} \ No newline at end of file