-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from kokorin/ADD_EXAMPLE
Add example
- Loading branch information
Showing
10 changed files
with
411 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,112 @@ | ||
# lombok-presence-checker | ||
Lombok extension which generates Presence Check methods | ||
# @PresenceChecker | ||
|
||
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 | ||
@Getter | ||
@Setter | ||
public class User { | ||
private String name; | ||
} | ||
|
||
@Getter | ||
@Setter | ||
@PresenceChecker | ||
public class UserUpdateDto { | ||
private String name; | ||
} | ||
|
||
//MapStruct Mapper interface declaration | ||
@Mapper | ||
public interface UserMapper { | ||
void updateUser(UserUpdateDto dto, @MappingTarget User user); | ||
} | ||
``` | ||
|
||
## 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 | ||
|
||
Some dependencies have to be installed to local Maven repository before compilation | ||
|
||
`mvn initialize` This command will download & install | ||
First initialize everything: | ||
|
||
`mvn initialize` | ||
|
||
Then build: | ||
|
||
`mvn` To build project | ||
`mvn clean install` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
<parent> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter-parent</artifactId> | ||
<version>2.4.0</version> | ||
<relativePath/> <!-- lookup parent from repository --> | ||
</parent> | ||
|
||
<groupId>com.github.kokorin.lombok</groupId> | ||
<artifactId>lombok-presence-checker-example</artifactId> | ||
<version>0.0.1-SNAPSHOT</version> | ||
|
||
<description>Lombok Presence Checker example</description> | ||
|
||
<properties> | ||
<java.version>1.8</java.version> | ||
<lombok.version>1.18.16</lombok.version> | ||
<mapstruct.version>1.4.1.Final</mapstruct.version> | ||
</properties> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter-web</artifactId> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>org.projectlombok</groupId> | ||
<artifactId>lombok</artifactId> | ||
<version>${lombok.version}</version> | ||
<scope>provided</scope> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>com.github.kokorin.lombok</groupId> | ||
<artifactId>lombok-presence-checker</artifactId> | ||
<version>0.0.1-SNAPSHOT</version> | ||
<scope>provided</scope> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>org.mapstruct</groupId> | ||
<artifactId>mapstruct</artifactId> | ||
<version>${mapstruct.version}</version> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter-test</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>junit</groupId> | ||
<artifactId>junit</artifactId> | ||
<version>4.13.1</version> | ||
<scope>test</scope> | ||
</dependency> | ||
</dependencies> | ||
|
||
<build> | ||
<plugins> | ||
<plugin> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-maven-plugin</artifactId> | ||
</plugin> | ||
|
||
<plugin> | ||
<groupId>org.apache.maven.plugins</groupId> | ||
<artifactId>maven-compiler-plugin</artifactId> | ||
<configuration> | ||
<annotationProcessorPaths> | ||
<path> | ||
<groupId>org.projectlombok</groupId> | ||
<artifactId>lombok</artifactId> | ||
<version>${lombok.version}</version> | ||
</path> | ||
<path> | ||
<groupId>com.github.kokorin.lombok</groupId> | ||
<artifactId>lombok-presence-checker</artifactId> | ||
<version>0.0.1-SNAPSHOT</version> | ||
</path> | ||
<path> | ||
<groupId>org.mapstruct</groupId> | ||
<artifactId>mapstruct-processor</artifactId> | ||
<version>${mapstruct.version}</version> | ||
</path> | ||
</annotationProcessorPaths> | ||
</configuration> | ||
</plugin> | ||
</plugins> | ||
</build> | ||
|
||
</project> |
93 changes: 93 additions & 0 deletions
93
.../main/java/com/github/kokorin/lombok/example/LombokPresenceCheckerExampleApplication.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package com.github.kokorin.lombok.example; | ||
|
||
import com.github.kokorin.lombok.PresenceChecker; | ||
import lombok.*; | ||
import org.mapstruct.Mapper; | ||
import org.mapstruct.MappingTarget; | ||
import org.mapstruct.factory.Mappers; | ||
import org.springframework.boot.SpringApplication; | ||
import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
import org.springframework.web.bind.annotation.*; | ||
import org.springframework.web.servlet.config.annotation.EnableWebMvc; | ||
|
||
import java.util.concurrent.atomic.AtomicReference; | ||
|
||
@SpringBootApplication(scanBasePackageClasses = LombokPresenceCheckerExampleApplication.class) | ||
@EnableWebMvc | ||
public class LombokPresenceCheckerExampleApplication { | ||
|
||
public static void main(String[] args) { | ||
SpringApplication.run(LombokPresenceCheckerExampleApplication.class, args); | ||
} | ||
|
||
@RestController | ||
@RequestMapping("user") | ||
public static class UserRestController { | ||
private final UserMapper userMapper = Mappers.getMapper(UserMapper.class); | ||
private final AtomicReference<User> userRef = new AtomicReference<>( | ||
new User("Иван", "Фёдорович", "Крузенштерн", "Человек и пароход") | ||
); | ||
|
||
@GetMapping | ||
public UserDto getUser() { | ||
return userMapper.toDto(userRef.get()); | ||
} | ||
|
||
@PostMapping | ||
public UserDto setUser(@RequestBody UserDto userDto) { | ||
userRef.set(userMapper.fromDto(userDto)); | ||
return userDto; | ||
} | ||
|
||
@PutMapping | ||
public UserDto updateUser(@RequestBody UserUpdateDto userUpdateDto) { | ||
// 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(updated); | ||
} | ||
} | ||
|
||
@Getter | ||
@Setter | ||
@Builder(toBuilder = true) | ||
@NoArgsConstructor | ||
@AllArgsConstructor | ||
public static class User { | ||
private String name; | ||
private String patronymic; | ||
private String surname; | ||
private String nickname; | ||
} | ||
|
||
@Getter | ||
@Setter | ||
public static class UserDto { | ||
private String name; | ||
private String patronymic; | ||
private String surname; | ||
private String nickname; | ||
} | ||
|
||
@Getter | ||
@Setter | ||
@PresenceChecker | ||
public static class UserUpdateDto { | ||
private String name; | ||
private String patronymic; | ||
private String surname; | ||
private String nickname; | ||
} | ||
|
||
@Mapper | ||
public static interface UserMapper { | ||
UserDto toDto(User user); | ||
User fromDto(UserDto dto); | ||
void updateUser(UserUpdateDto dto, @MappingTarget User user); | ||
} | ||
} |
Empty file.
71 changes: 71 additions & 0 deletions
71
.../java/com/github/kokorin/lombok/example/LombokPresenceCheckerExampleApplicationTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +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(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) | ||
class LombokPresenceCheckerExampleApplicationTests { | ||
@LocalServerPort | ||
private int port; | ||
|
||
@Autowired | ||
private TestRestTemplate restTemplate; | ||
|
||
@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<UserDto> 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()); | ||
} | ||
|
||
} |
Oops, something went wrong.