From 1dae396d99c002fc58ae0a33eda610d491cbc6c7 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 23 Apr 2019 06:17:55 +0200 Subject: [PATCH] JSonDeserializerWithAccessFilter --- package-lock.json | 41 ++++++++--- .../JSonDeserializerWithAccessFilter.java | 53 +++++++++++---- .../JSonSerializerWithAccessFilter.java | 2 +- .../hsadminng/service/dto/CustomerDTO.java | 4 +- .../service/util/ReflectionUtil.java | 22 ++++++ ...nDeserializerWithAccessFilterUnitTest.java | 68 +++++++++---------- ...SonSerializerWithAccessFilterUnitTest.java | 2 +- 7 files changed, 130 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index 544d36a9..b13a9116 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6025,7 +6025,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -6046,12 +6047,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6066,17 +6069,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -6193,7 +6199,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -6205,6 +6212,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6219,6 +6227,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6226,12 +6235,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -6250,6 +6261,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -6330,7 +6342,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -6342,6 +6355,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -6427,7 +6441,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -6463,6 +6478,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6482,6 +6498,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6525,12 +6542,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java index 3ab6f061..40d02455 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java @@ -3,6 +3,13 @@ package org.hostsharing.hsadminng.service.accessfilter; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.LongNode; +import com.fasterxml.jackson.databind.node.TextNode; +import org.apache.commons.lang3.NotImplementedException; +import org.hostsharing.hsadminng.service.util.ReflectionUtil; + +import java.lang.reflect.Field; import static org.hostsharing.hsadminng.service.util.ReflectionUtil.unchecked; @@ -17,18 +24,40 @@ public class JSonDeserializerWithAccessFilter { } public T deserialize() { -// -// CustomerDTO dto = new CustomerDTO(); -// dto.setId(((IntNode) treeNode.get("id")).asLong()); -// dto.setReference(((IntNode) treeNode.get("reference")).asInt()); -// dto.setPrefix(((TextNode) treeNode.get("prefix")).asText()); -// dto.setName(((TextNode) treeNode.get("name")).asText()); -// dto.setContractualAddress(((TextNode) treeNode.get("contractualAddress")).asText()); -// dto.setContractualSalutation(((TextNode) treeNode.get("contractualSalutation")).asText()); -// dto.setBillingAddress(((TextNode) treeNode.get("billingAddress")).asText()); -// dto.setBillingSalutation(((TextNode) treeNode.get("billingSalutation")).asText()); -// dto.setRemark(((TextNode) treeNode.get("remark")).asText()); - + treeNode.fieldNames().forEachRemaining(fieldName -> { + try { + final Field field = dto.getClass().getDeclaredField(fieldName); + final Object value = readValue(treeNode, field); + writeValue(dto, field, value); + } catch (NoSuchFieldException e) { + throw new RuntimeException("setting field " + fieldName + " failed", e); + } + }); return dto; } + + private Object readValue(final TreeNode treeNode, final Field field) { + final TreeNode fieldNode = treeNode.get(field.getName()); + if (fieldNode instanceof TextNode) { + return ((TextNode)fieldNode).asText(); + } else if (fieldNode instanceof IntNode) { + return ((IntNode)fieldNode).asInt(); + } else if (fieldNode instanceof LongNode) { + return ((LongNode)fieldNode).asLong(); + } else { + throw new NotImplementedException("property type not yet implemented: " + field); + } + } + + private void writeValue(final T dto, final Field field, final Object value) { + if ( field.getType().isAssignableFrom(value.getClass()) ) { + ReflectionUtil.setValue(dto, field, value); + } else if (Integer.class.isAssignableFrom(field.getType()) || int.class.isAssignableFrom(field.getType())) { + ReflectionUtil.setValue(dto, field, ((Number)value).intValue()); + } else if (Long.class.isAssignableFrom(field.getType()) || long.class.isAssignableFrom(field.getType())) { + ReflectionUtil.setValue(dto, field, ((Number)value).longValue()); + } else { + throw new NotImplementedException("property type not yet implemented: " + field); + } + } } diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java index 2fd08c51..2e6ce295 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java @@ -40,7 +40,7 @@ public class JSonSerializerWithAccessFilter extends JsonSerializer { } else if (String.class.isAssignableFrom(prop.getType())) { jsonGenerator.writeStringField(fieldName, (String) get(dto, prop)); } else { - throw new NotImplementedException("property type not yet implemented" + prop); + throw new NotImplementedException("property type not yet implemented: " + prop); } } } diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java index 58abb893..f789c4be 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java @@ -175,9 +175,9 @@ public class CustomerDTO implements Serializable { @Override public CustomerDTO deserialize(final JsonParser jsonParser, - final DeserializationContext deserializationContext) throws IOException { + final DeserializationContext deserializationContext) { - return new JSonDeserializerWithAccessFilter(jsonParser, deserializationContext, CustomerDTO.class).deserialize(); + return new JSonDeserializerWithAccessFilter<>(jsonParser, deserializationContext, CustomerDTO.class).deserialize(); } } } diff --git a/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java b/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java index ff5c8de3..9492a28c 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java +++ b/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java @@ -1,9 +1,31 @@ package org.hostsharing.hsadminng.service.util; +import com.fasterxml.jackson.core.TreeNode; + +import java.lang.reflect.Field; import java.util.function.Supplier; public class ReflectionUtil { + public static void setValue(final T dto, final String fieldName, final Object value) { + try { + final Field field = dto.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(dto, value); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public static void setValue(final T dto, final Field field, final Object value) { + try { + field.setAccessible(true); + field.set(dto, value); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + @FunctionalInterface public interface ThrowingSupplier { T get() throws Exception; diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java index c0c93a47..2e3bcaa4 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java @@ -1,8 +1,14 @@ package org.hostsharing.hsadminng.service.accessfilter; import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.tuple.ImmutablePair; +import org.hostsharing.hsadminng.service.dto.CustomerDTO; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -15,6 +21,7 @@ import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenLoginUserWithRole; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -24,68 +31,55 @@ public class JSonDeserializerWithAccessFilterUnitTest { public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock - public JsonGenerator jsonGenerator; + public JsonParser jsonParser; + + @Mock + public ObjectCodec codec; + + @Mock + public TreeNode treeNode; @Before - public void init() { + public void init() throws IOException { givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); + + given(jsonParser.getCodec()).willReturn(codec); } @Test public void shouldDeserializeStringField() throws IOException { // given - final String givenJSon = asJSon(ImmutablePair.of("stringField", "String Value")); + givenJSonTree(asJSon(ImmutablePair.of("openStringField", "String Value"))); // when - new JSonDeserializerWithAccessFilter().deserialize(givenJSon, jsonGenerator, null); + GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize(); // then - verify(jsonGenerator).writeStringField("openStringField", givenDTO.openStringField); + assertThat(actualDto.openStringField).isEqualTo("String Value"); } @Test - public void shouldSerializeRestrictedFieldIfRequiredRoleIsCoveredByUser() throws IOException { - + public void shouldDeserializeIntegerField() throws IOException { // given - givenLoginUserWithRole(Role.FINANCIAL_CONTACT); + givenJSonTree(asJSon(ImmutablePair.of("openIntegerField", 1234))); // when - new JSonSerializerWithAccessFilter().serialize(givenDTO, jsonGenerator, null); + GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize(); // then - verify(jsonGenerator).writeStringField("restrictedField", givenDTO.restrictedField); + assertThat(actualDto.openIntegerField).isEqualTo(1234); } @Test - public void shouldNotSerializeRestrictedFieldIfRequiredRoleIsNotCoveredByUser() throws IOException { - + public void shouldDeserializeLongField() throws IOException { // given - givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); + givenJSonTree(asJSon(ImmutablePair.of("openLongField", 1234L))); // when - new JSonSerializerWithAccessFilter().serialize(givenDTO, jsonGenerator, null); + GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize(); // then - verify(jsonGenerator, never()).writeStringField("restrictedField", givenDTO.restrictedField); - } - - @Test - public void shouldThrowExceptionForUnimplementedFieldType() throws IOException { - - // given - class Arbitrary { - } - class GivenDtoWithUnimplementedFieldType { - @AccessFor(read = Role.ANYBODY) - Arbitrary fieldWithUnimplementedType; - } - final GivenDtoWithUnimplementedFieldType givenDto = new GivenDtoWithUnimplementedFieldType(); - - // when - Throwable actual = catchThrowable(() -> new JSonSerializerWithAccessFilter().serialize(givenDto, jsonGenerator, null)); - - // then - assertThat(actual).isInstanceOf(NotImplementedException.class); + assertThat(actualDto.openLongField).isEqualTo(1234L); } // --- fixture code below --- @@ -105,11 +99,15 @@ public class JSonDeserializerWithAccessFilterUnitTest { return "{\n" + json.substring(0, json.length()-2) + "\n}"; } + private void givenJSonTree(String givenJSon) throws IOException { + given(codec.readTree(jsonParser)).willReturn(new ObjectMapper().readTree(givenJSon)); + } + private String inQuotes(Object value) { return "\"" + value.toString() + "\""; } - private static class GivenDto { + public static class GivenDto { @AccessFor(update = {Role.TECHNICAL_CONTACT, Role.FINANCIAL_CONTACT}) String restrictedField; diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java index 9a5e838f..2e7be2fb 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java @@ -36,7 +36,7 @@ public class JSonSerializerWithAccessFilterUnitTest { @Test public void shouldSerializeStringField() throws IOException { - // when + // when new JSonSerializerWithAccessFilter().serialize(givenDTO, jsonGenerator, null); // then