From 6c5053c453209dde63ceb2958f0833341e4005ee Mon Sep 17 00:00:00 2001 From: kokorin <9crqUVAXd6Q17EY354lkbYeB> Date: Sat, 12 Dec 2020 22:52:36 +0200 Subject: [PATCH] Spring Boot Example & Documentation --- .github/workflows/maven-tests.yml | 4 + README.md | 90 +++++++++++++++++-- ...mbokPresenceCheckerExampleApplication.java | 35 ++++---- .../src/main/resources/application.properties | 1 - ...resenceCheckerExampleApplicationTests.java | 67 +++++++++++--- pom.xml | 1 - 6 files changed, 165 insertions(+), 33 deletions(-) diff --git a/.github/workflows/maven-tests.yml b/.github/workflows/maven-tests.yml index 91a1f9b..45fe690 100644 --- a/.github/workflows/maven-tests.yml +++ b/.github/workflows/maven-tests.yml @@ -34,3 +34,7 @@ jobs: - name: Build & Test run: bash mvnw clean install -B + + - name: Test Spring Boot Example + run: bash mvnw clean install -B -f lombok-presence-checker-example/pom.xml + if: matrix.java-version != 7 diff --git a/README.md b/README.md index 3da6d13..9839296 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,102 @@ # @PresenceChecker -Lombok extension which generates Presence Check methods +Lombok extension which generates Presence Checker methods ## Overview +You can annotate any field or type with `@PresenceChecker`, to let lombok together with lombok-presence-checker +generate presence checker methods (`hasFieldName()`) automatically. + +This project allows easy implementation of partial update via REST API. + +MapStruct is aware of [source presence checking](https://mapstruct.org/documentation/stable/reference/html/#source-presence-check) +and uses presence checker methods by default (if present of course) to verify if a field in a target object should be updated with a value in a source +object. Without presence checkers MapStruct by default updates only fields with non-null values. + +### REST & Partial Updates + +After incoming request body get parsed in a typed object in a REST controller one can't distinguish absent property from property with null value. + +Several strategies can be applied to partial update: +1. Treat any DTO property with null value as absent property and *do not* set corresponding entity property to null +2. Treat any DTO property with null value as property explicitly set to null, thus denying partial update +3. Parse request body as JsonObject (or Map), which seems not very suitable for strongly typed languages +4. Use JSON mapper for entity partial update with a price of JSON mapper leaking to REST Controller +5. For every field in DTO add a flag to mark it as present or not + +Lombok-presence-checker aims at last strategy. Check REST controller [example](/kokorin/lombok-presence-checker/lombok-presence-checker-example/src/main/java/com/github/kokorin/lombok/example/LombokPresenceCheckerExampleApplication.java) +and corresponding [tests](/kokorin/lombok-presence-checker/lombok-presence-checker-example/src/test/java/com/github/kokorin/lombok/example/LombokPresenceCheckerExampleApplicationTests.java) + ## With Lombok and Lombok-Presence-Checker ```java -@PresenceChecker @Getter @Setter -public static class UserDto { +public class User { + private String name; +} + +@Getter +@Setter +@PresenceChecker +public class UserUpdateDto { private String name; - private long value; +} + +//MapStruct Mapper interface declaration +@Mapper +public interface UserMapper { + void updateUser(UserUpdateDto dto, @MappingTarget User user); } ``` -## Vanilla Java +## Generated Code + +```java +public class User { + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } +} + +public class UserUpdateDto { + private boolean hasName; + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + this.hasName = true; + } + + public boolean hasName() { + return this.hasName; + } +} + +//MapStruct Mapper implementation +public class UserMapperImpl implements UserMapper { + @Override + public void updateUser(UserUpdateDto dto, User user) { + if ( dto == null ) { + return; + } + + if ( dto.hasName() ) { + user.setName( dto.getName() ); + } + } +} +``` # Build diff --git a/lombok-presence-checker-example/src/main/java/com/github/kokorin/lombok/example/LombokPresenceCheckerExampleApplication.java b/lombok-presence-checker-example/src/main/java/com/github/kokorin/lombok/example/LombokPresenceCheckerExampleApplication.java index 9d57259..4de8891 100644 --- a/lombok-presence-checker-example/src/main/java/com/github/kokorin/lombok/example/LombokPresenceCheckerExampleApplication.java +++ b/lombok-presence-checker-example/src/main/java/com/github/kokorin/lombok/example/LombokPresenceCheckerExampleApplication.java @@ -1,10 +1,7 @@ package com.github.kokorin.lombok.example; import com.github.kokorin.lombok.PresenceChecker; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.RequiredArgsConstructor; +import lombok.*; import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; import org.mapstruct.factory.Mappers; @@ -15,7 +12,7 @@ import java.util.concurrent.atomic.AtomicReference; -@SpringBootApplication +@SpringBootApplication(scanBasePackageClasses = LombokPresenceCheckerExampleApplication.class) @EnableWebMvc public class LombokPresenceCheckerExampleApplication { @@ -23,7 +20,8 @@ public static void main(String[] args) { SpringApplication.run(LombokPresenceCheckerExampleApplication.class, args); } - @RestController("user") + @RestController + @RequestMapping("user") public static class UserRestController { private final UserMapper userMapper = Mappers.getMapper(UserMapper.class); private final AtomicReference userRef = new AtomicReference<>( @@ -41,20 +39,23 @@ public UserDto setUser(@RequestBody UserDto userDto) { return userDto; } - @PatchMapping + @PutMapping public UserDto updateUser(@RequestBody UserUpdateDto userUpdateDto) { - User updatedUser = userRef.updateAndGet(user -> { - // MapStruct will update only those fields which were explicitly passed - // in HTTP request body - userMapper.updateUser(userUpdateDto, user); - return user; + // MapStruct will update only those fields which were explicitly passed + // in HTTP request body + User updated = userRef.updateAndGet(user -> { + User result = user.toBuilder().build(); + userMapper.updateUser(userUpdateDto, result); + return result; }); - return userMapper.toDto(updatedUser); + return userMapper.toDto(updated); } } - @Data + @Getter + @Setter + @Builder(toBuilder = true) @NoArgsConstructor @AllArgsConstructor public static class User { @@ -64,7 +65,8 @@ public static class User { private String nickname; } - @Data + @Getter + @Setter public static class UserDto { private String name; private String patronymic; @@ -72,7 +74,8 @@ public static class UserDto { private String nickname; } - @Data + @Getter + @Setter @PresenceChecker public static class UserUpdateDto { private String name; diff --git a/lombok-presence-checker-example/src/main/resources/application.properties b/lombok-presence-checker-example/src/main/resources/application.properties index 8b13789..e69de29 100644 --- a/lombok-presence-checker-example/src/main/resources/application.properties +++ b/lombok-presence-checker-example/src/main/resources/application.properties @@ -1 +0,0 @@ - diff --git a/lombok-presence-checker-example/src/test/java/com/github/kokorin/lombok/example/LombokPresenceCheckerExampleApplicationTests.java b/lombok-presence-checker-example/src/test/java/com/github/kokorin/lombok/example/LombokPresenceCheckerExampleApplicationTests.java index 999bcd3..8a06551 100644 --- a/lombok-presence-checker-example/src/test/java/com/github/kokorin/lombok/example/LombokPresenceCheckerExampleApplicationTests.java +++ b/lombok-presence-checker-example/src/test/java/com/github/kokorin/lombok/example/LombokPresenceCheckerExampleApplicationTests.java @@ -1,24 +1,71 @@ package com.github.kokorin.lombok.example; +import com.github.kokorin.lombok.example.LombokPresenceCheckerExampleApplication.UserDto; +import org.junit.Assert; +import org.junit.Before; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.http.*; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -@SpringBootTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class LombokPresenceCheckerExampleApplicationTests { - @LocalServerPort - private int port; + @LocalServerPort + private int port; - @Autowired - private TestRestTemplate restTemplate; + @Autowired + private TestRestTemplate restTemplate; - @Test - void contextLoads() { - String url = "http://localhost:" + port + "/api/openapi?group=v1"; - String content = restTemplate.getForObject(url, String.class); + @Test + void partialUpdateWithPresenceChecker() { + String url = "http://localhost:" + port + "/user"; - } + UserDto userDto = restTemplate.getForObject(url, UserDto.class); + + Assert.assertEquals("Иван", userDto.getName()); + Assert.assertEquals("Фёдорович", userDto.getPatronymic()); + Assert.assertEquals("Крузенштерн", userDto.getSurname()); + Assert.assertEquals("Человек и пароход", userDto.getNickname()); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_JSON); + + ResponseEntity updateResponse = restTemplate.exchange( + url, + HttpMethod.PUT, + new HttpEntity<>("{\"name\":\"Jon\"}", httpHeaders), + UserDto.class + ); + + Assert.assertEquals(HttpStatus.OK, updateResponse.getStatusCode()); + userDto = updateResponse.getBody(); + + Assert.assertEquals("Jon", userDto.getName()); + Assert.assertEquals("Фёдорович", userDto.getPatronymic()); + Assert.assertEquals("Крузенштерн", userDto.getSurname()); + Assert.assertEquals("Человек и пароход", userDto.getNickname()); + + updateResponse = restTemplate.exchange( + url, + HttpMethod.PUT, + new HttpEntity<>("{" + + "\"patronymic\":null," + + "\"surname\":\"Snow\"," + + "\"nickname\":\"you know nothing\"" + + "}", httpHeaders), + UserDto.class + ); + + Assert.assertEquals(HttpStatus.OK, updateResponse.getStatusCode()); + userDto = updateResponse.getBody(); + + Assert.assertEquals("Jon", userDto.getName()); + Assert.assertNull(userDto.getPatronymic()); + Assert.assertEquals("Snow", userDto.getSurname()); + Assert.assertEquals("you know nothing", userDto.getNickname()); + } } diff --git a/pom.xml b/pom.xml index 9d64393..0e0e832 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,6 @@ lombok-unshaded lombok-presence-checker lombok-presence-checker-test - lombok-presence-checker-example