Skip to content

Commit

Permalink
Spring Boot Example & Documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
kokorin authored and kokorin committed Dec 15, 2020
1 parent 3d1971a commit 6c5053c
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 33 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
90 changes: 85 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,15 +12,16 @@

import java.util.concurrent.atomic.AtomicReference;

@SpringBootApplication
@SpringBootApplication(scanBasePackageClasses = LombokPresenceCheckerExampleApplication.class)
@EnableWebMvc
public class LombokPresenceCheckerExampleApplication {

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<User> userRef = new AtomicReference<>(
Expand All @@ -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 {
Expand All @@ -64,15 +65,17 @@ public static class User {
private String nickname;
}

@Data
@Getter
@Setter
public static class UserDto {
private String name;
private String patronymic;
private String surname;
private String nickname;
}

@Data
@Getter
@Setter
@PresenceChecker
public static class UserUpdateDto {
private String name;
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@

Original file line number Diff line number Diff line change
@@ -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<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());
}

}
1 change: 0 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
<module>lombok-unshaded</module>
<module>lombok-presence-checker</module>
<module>lombok-presence-checker-test</module>
<module>lombok-presence-checker-example</module>
</modules>

<properties>
Expand Down

0 comments on commit 6c5053c

Please sign in to comment.