Skip to content

Commit

Permalink
Merge pull request #1 from kokorin/ADD_EXAMPLE
Browse files Browse the repository at this point in the history
Add example
  • Loading branch information
kokorin authored Dec 15, 2020
2 parents a0c933d + 6c5053c commit 3aec349
Show file tree
Hide file tree
Showing 10 changed files with 411 additions and 36 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/maven-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
109 changes: 105 additions & 4 deletions README.md
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`

97 changes: 97 additions & 0 deletions lombok-presence-checker-example/pom.xml
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>
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.
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());
}

}
Loading

0 comments on commit 3aec349

Please sign in to comment.