From 5287d78fcb384e981c90dc8b4f4195d14c196671 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 18 Apr 2019 17:12:24 +0200 Subject: [PATCH] JSON serializer generalized --- package-lock.json | 41 +++-- .../hsadminng/service/dto/CustomerDTO.java | 166 ++++++++++-------- .../service/dto/CustomerDTOUnitTest.java | 42 +++-- 3 files changed, 150 insertions(+), 99 deletions(-) diff --git a/package-lock.json b/package-lock.json index 96002d14..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/dto/CustomerDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java index ce713409..e56b5ac3 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java @@ -14,60 +14,55 @@ import org.apache.commons.lang3.NotImplementedException; import org.hostsharing.hsadminng.security.SecurityUtils; import org.springframework.boot.jackson.JsonComponent; -import javax.annotation.PostConstruct; import javax.validation.constraints.*; -import java.beans.IntrospectionException; -import java.beans.Introspector; -import java.beans.PropertyDescriptor; import java.io.IOException; import java.io.Serializable; import java.lang.annotation.*; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Objects; -import java.util.Optional; /** * A DTO for the Customer entity. */ -@ReadableFor(Role.ANY_CUSTOMER_USER) -@WritableFor(Role.SUPPORTER) public class CustomerDTO implements Serializable { - @WritableFor(Role.NOBODY) + @AccessFor(read = Role.ANY_CUSTOMER_USER) private Long id; @NotNull @Min(value = 10000) @Max(value = 99999) + @AccessFor(init = Role.ADMIN, read = Role.ANY_CUSTOMER_USER) private Integer number; @NotNull @Pattern(regexp = "[a-z][a-z0-9]+") + @AccessFor(init = Role.ADMIN, read = Role.ANY_CUSTOMER_USER) private String prefix; @NotNull @Size(max = 80) + @AccessFor(init = Role.ADMIN, read = Role.ANY_CUSTOMER_USER) private String name; @NotNull @Size(max = 400) - @ReadableFor(Role.CONTRACTUAL_CONTACT) + @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = Role.CONTRACTUAL_CONTACT) private String contractualAddress; @Size(max = 80) - @ReadableFor(Role.CONTRACTUAL_CONTACT) - @WritableFor(Role.CONTRACTUAL_CONTACT) + @AccessFor(init = Role.ADMIN, update = Role.CONTRACTUAL_CONTACT, read = Role.ANY_CUSTOMER_CONTACT) private String contractualSalutation; @Size(max = 400) - @ReadableFor(Role.CONTRACTUAL_CONTACT) + @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = Role.CONTRACTUAL_CONTACT) private String billingAddress; @Size(max = 80) - @ReadableFor(Role.CONTRACTUAL_CONTACT) - @WritableFor(Role.CONTRACTUAL_CONTACT) + @AccessFor(init = Role.ADMIN, update = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}, read = Role.CONTRACTUAL_CONTACT) private String billingSalutation; public Long getId() { @@ -172,79 +167,54 @@ public class CustomerDTO implements Serializable { @JsonComponent public static class CustomerJsonSerializer extends JsonSerializer { - private Optional login; - - @PostConstruct - public void getLoginUser() { - this.login = SecurityUtils.getCurrentUserLogin(); - } - @Override public void serialize(CustomerDTO dto, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { jsonGenerator.writeStartObject(); - try { - for (PropertyDescriptor prop : Introspector.getBeanInfo(CustomerDTO.class).getPropertyDescriptors()) { - if (isRealProprety(prop)) { - toJSon(dto, jsonGenerator, prop); - } - } - } catch (IntrospectionException e) { - throw new RuntimeException(e); + for (Field prop : CustomerDTO.class.getDeclaredFields()) { + toJSon(dto, jsonGenerator, prop); } -// -// jsonGenerator.writeNumberField("number", dto.getNumber()); -// jsonGenerator.writeStringField("prefix", dto.getPrefix()); -// jsonGenerator.writeStringField("name", dto.getName()); -// toJSonString(dto, jsonGenerator,"contractualAddress"); -// jsonGenerator.writeStringField("contractualSalutation", dto.getContractualSalutation()); -// jsonGenerator.writeStringField("billingAddress", dto.getBillingAddress()); -// jsonGenerator.writeStringField("billingSalutation", dto.getBillingSalutation()); - jsonGenerator.writeEndObject(); } - private boolean isRealProprety(PropertyDescriptor prop) { - return prop.getWriteMethod() != null; - } - - private void toJSonString(CustomerDTO user, JsonGenerator jsonGenerator, String fieldName) throws IOException { - if (isReadAllowed(fieldName)) { - jsonGenerator.writeStringField(fieldName, user.getContractualAddress()); - } - } - - private void toJSon(CustomerDTO dto, JsonGenerator jsonGenerator, PropertyDescriptor prop) throws IOException { - final String fieldName = prop.getName(); - if (isReadAllowed(fieldName)) { - if (Integer.class.isAssignableFrom(prop.getPropertyType()) || int.class.isAssignableFrom(prop.getPropertyType())) { - jsonGenerator.writeNumberField(fieldName, (int) invoke(dto, prop.getReadMethod())); - } else if (Long.class.isAssignableFrom(prop.getPropertyType()) || long.class.isAssignableFrom(prop.getPropertyType())) { - jsonGenerator.writeNumberField(fieldName, (long) invoke(dto, prop.getReadMethod())); - } else if (String.class.isAssignableFrom(prop.getPropertyType())) { - jsonGenerator.writeStringField(fieldName, (String) invoke(dto, prop.getReadMethod())); + private void toJSon(CustomerDTO dto, JsonGenerator jsonGenerator, Field prop) throws IOException { + if (getLoginUserRole().isAllowedToRead(prop)) { + final String fieldName = prop.getName(); + if (Integer.class.isAssignableFrom(prop.getType()) || int.class.isAssignableFrom(prop.getType())) { + jsonGenerator.writeNumberField(fieldName, (int) get(dto, prop)); + } else if (Long.class.isAssignableFrom(prop.getType()) || long.class.isAssignableFrom(prop.getType())) { + jsonGenerator.writeNumberField(fieldName, (long) get(dto, prop)); + } else if (String.class.isAssignableFrom(prop.getType())) { + jsonGenerator.writeStringField(fieldName, (String) get(dto, prop)); } else { throw new NotImplementedException("property type not yet implemented" + prop); } } } - private Object invoke(Object dto, Method method) { + private Object get(CustomerDTO dto, Field field) { try { - return method.invoke(dto); - } catch (IllegalAccessException|InvocationTargetException e) { + field.setAccessible(true); + return field.get(dto); + } catch (IllegalAccessException e) { throw new RuntimeException(e); } } - private boolean isReadAllowed(String fieldName) { - if ( fieldName.equals("contractualAddress") ) { - return login.map(user -> user.equals("admin")).orElse(false); - } - return true; + private Role getLoginUserRole() { + return SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); } + + private Object invoke(Object dto, Method method) { + try { + return method.invoke(dto); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + } @JsonComponent @@ -274,27 +244,73 @@ public class CustomerDTO implements Serializable { enum Role { NOBODY(0), HOSTMASTER(1), ADMIN(2), SUPPORTER(3), - ANY_CUSTOMER_CONTACT(10), CONTRACTUAL_CONTACT(11), - ANY_CUSTOMER_USER(30); + ANY_CUSTOMER_CONTACT(20), CONTRACTUAL_CONTACT(21), FINANCIAL_CONTACT(22), TECHNICAL_CONTACT(22), + ANY_CUSTOMER_USER(80), + ANYBODY(99); private final int level; Role(final int level) { this.level = level; } + + boolean covers(final Role role) { + return this == role || this.level < role.level; + } + + public boolean isAllowedToInit(Field field) { + + final AccessFor accessFor = field.getAnnotation(AccessFor.class); + if (accessFor == null) { + return false; + } + + return isRoleCovered(accessFor.init()); + } + + public boolean isAllowedToUpdate(Field field) { + + final Role loginUserRole = SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); + + final AccessFor accessFor = field.getAnnotation(AccessFor.class); + if (accessFor == null) { + return false; + } + + return isRoleCovered(accessFor.update()); + } + + public boolean isAllowedToRead(Field field) { + + final Role loginUserRole = SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); + + final AccessFor accessFor = field.getAnnotation(AccessFor.class); + if (accessFor == null) { + return false; + } + + return isRoleCovered(accessFor.read()); + } + + private boolean isRoleCovered(Role[] requiredRoles) { + for (Role accessAllowedForRole : requiredRoles) { + if (this.covers(accessAllowedForRole)) { + return true; + } + } + return false; + } + } @Target({ElementType.FIELD, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) @Documented -@interface ReadableFor { +@interface AccessFor { + Role[] init() default Role.NOBODY; + Role[] update() default Role.NOBODY; + + Role[] read() default Role.NOBODY; } - -@Target({ElementType.FIELD, ElementType.TYPE_USE}) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@interface WritableFor { - -} diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java index 472a235e..b6be38c9 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java @@ -27,26 +27,42 @@ public class CustomerDTOUnitTest { private ObjectMapper objectMapper; @Test - public void testSerializationAsCustomer() throws JsonProcessingException { + public void testSerializationAsContractualCustomerContact() throws JsonProcessingException { // given CustomerDTO given = createSomeCustomerDTO(); - givenLoginUser("customer"); + givenLoginUserWithRole("ANY_CUSTOMER_USER"); // when String actual = objectMapper.writeValueAsString(given); // then given.setContractualAddress(null); - //given.setContractualSalutation(null); + given.setContractualSalutation(null); + given.setBillingAddress(null); + given.setBillingSalutation(null); assertEquals(createExpectedJSon(given), actual); } @Test - public void testDeserializeAsCustomer() throws IOException { + public void testSerializationAsSupporter() throws JsonProcessingException { + + // given + CustomerDTO given = createSomeCustomerDTO(); + givenLoginUserWithRole("SUPPORTER"); + + // when + String actual = objectMapper.writeValueAsString(given); + + // then + assertEquals(createExpectedJSon(given), actual); + } + + @Test + public void testDeserializeAsContractualCustomerContact() throws IOException { // given String json = "{\"id\":1234,\"number\":10001,\"prefix\":\"abc\",\"name\":\"Mein Name\",\"contractualAddress\":\"Eine Adresse\",\"contractualSalutation\":\"Hallo\",\"billingAddress\":\"Noch eine Adresse\",\"billingSalutation\":\"Moin\"}"; - givenLoginUser("customer"); + givenLoginUserWithRole("CONTRACTUAL_CONTACT"); // when CustomerDTO actual = objectMapper.readValue(json, CustomerDTO.class); @@ -66,14 +82,14 @@ public class CustomerDTOUnitTest { private String createExpectedJSon(CustomerDTO dto) { String json = // the fields in alphanumeric order: - toJSonFieldDefinitionIfPresent("billingAddress", dto.getBillingAddress()) + - toJSonFieldDefinitionIfPresent("billingSalutation", dto.getBillingSalutation()) + + toJSonFieldDefinitionIfPresent("id", dto.getId()) + + toJSonFieldDefinitionIfPresent("number", dto.getNumber()) + + toJSonFieldDefinitionIfPresent("prefix", dto.getPrefix()) + + toJSonFieldDefinitionIfPresent("name", dto.getName()) + toJSonFieldDefinitionIfPresent("contractualAddress", dto.getContractualAddress()) + toJSonFieldDefinitionIfPresent("contractualSalutation", dto.getContractualSalutation()) + - toJSonFieldDefinitionIfPresent("id", dto.getId()) + - toJSonFieldDefinitionIfPresent("name", dto.getName()) + - toJSonFieldDefinitionIfPresent("number", dto.getNumber()) + - toJSonFieldDefinitionIfPresent("prefix", dto.getPrefix()); + toJSonFieldDefinitionIfPresent("billingAddress", dto.getBillingAddress()) + + toJSonFieldDefinitionIfPresent("billingSalutation", dto.getBillingSalutation()); return "{" + json.substring(0, json.length() - 1) + "}"; } @@ -99,11 +115,11 @@ public class CustomerDTOUnitTest { given.setContractualSalutation("Hallo"); given.setBillingAddress("Noch eine Adresse"); given.setBillingSalutation("Moin"); - givenLoginUser("admin"); + givenLoginUserWithRole("admin"); return given; } - private void givenLoginUser(String userName) { + private void givenLoginUserWithRole(String userName) { SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); securityContext.setAuthentication(new UsernamePasswordAuthenticationToken(userName, userName)); SecurityContextHolder.setContext(securityContext);