Skip to content

Commit 4bf1034

Browse files
committed
adds exception handling, audit columns, and fixes localization.
1 parent 09c548a commit 4bf1034

File tree

11 files changed

+196
-29
lines changed

11 files changed

+196
-29
lines changed

pom.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
</dependency>
3535

3636
<!-- r2dbc Connection pool -->
37-
3837
<dependency>
3938
<groupId>io.r2dbc</groupId>
4039
<artifactId>r2dbc-pool</artifactId>

src/main/java/com/github/michaelsteven/archetype/springboot/webflux/items/Application.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
@OpenAPIDefinition(info = @Info(title = "Spring Boot Items API webflux", version = "0.0.1", contact = @Contact(name = "Mike Hepfer", email = "michaelsteven@hepfer.org")))
2121
@SpringBootApplication
2222
@EnableR2dbcRepositories
23-
//@EnableR2dbcAuditing
23+
@EnableR2dbcAuditing
2424
public class Application {
2525

2626
/**

src/main/java/com/github/michaelsteven/archetype/springboot/webflux/items/configuration/ItemDtoMapper.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.List;
44

55
import org.mapstruct.Mapper;
6+
import org.mapstruct.Mapping;
67
import org.mapstruct.factory.Mappers;
78

89
import com.github.michaelsteven.archetype.springboot.webflux.items.model.ItemDto;
@@ -19,7 +20,7 @@ public interface ItemDtoMapper {
1920
//@Mapping(source = "dto.id", target = "id")
2021
ItemEntity mapToEntity(ItemDto dto);
2122

22-
//@Mapping(source = "entity.id", target = "id")
23+
@Mapping(source = "createdTimestamp", target = "dateSubmitted")
2324
ItemDto mapToDto(ItemEntity entity);
2425

2526
List<ItemEntity> mapListToEntity(List<ItemDto> dtoList);

src/main/java/com/github/michaelsteven/archetype/springboot/webflux/items/configuration/MessageLocalizationConfig.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package com.github.michaelsteven.archetype.springboot.webflux.items.configuration;
22

3-
import java.util.Locale;
4-
53
import org.springframework.context.MessageSource;
64
import org.springframework.context.annotation.Bean;
75
import org.springframework.context.support.ResourceBundleMessageSource;
@@ -13,9 +11,6 @@
1311
@Component
1412
public class MessageLocalizationConfig {
1513

16-
/** The default locale. */
17-
private final Locale DEFAULT_LOCALE = Locale.US;
18-
1914
/** The message bundle base name. */
2015
private final String MESSAGE_BUNDLE_BASE_NAME = "messages";
2116

@@ -40,9 +35,6 @@ public MessageSource messageSource() {
4035
@Bean
4136
public LocaleResolver localeResolver() {
4237
return new LocaleResolver();
43-
//AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver();
44-
//acceptHeaderLocaleResolver.setDefaultLocale(DEFAULT_LOCALE);
45-
//return acceptHeaderLocaleResolver;
4638
}
4739

4840
}

src/main/java/com/github/michaelsteven/archetype/springboot/webflux/items/controller/ItemsController.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import org.springframework.context.i18n.LocaleContextHolder;
99
import org.springframework.data.domain.Page;
1010
import org.springframework.data.domain.PageRequest;
11-
import org.springframework.data.domain.Pageable;
1211
import org.springframework.data.domain.Sort;
1312
import org.springframework.http.MediaType;
1413
import org.springframework.validation.annotation.Validated;
@@ -24,7 +23,6 @@
2423
import com.github.michaelsteven.archetype.springboot.webflux.items.model.ApiError;
2524
import com.github.michaelsteven.archetype.springboot.webflux.items.model.ConfirmationDto;
2625
import com.github.michaelsteven.archetype.springboot.webflux.items.model.ItemDto;
27-
import com.github.michaelsteven.archetype.springboot.webflux.items.model.ItemEntity;
2826
import com.github.michaelsteven.archetype.springboot.webflux.items.service.ItemsService;
2927

3028
import io.swagger.v3.oas.annotations.Operation;
@@ -98,7 +96,7 @@ public Flux<ItemDto> getItems(@RequestParam(required=false,defaultValue="0") int
9896
@ApiResponse(responseCode = "503", description = "Service unavailable", content = @Content(schema = @Schema(implementation = ApiError.class))) })
9997
@PostMapping(API_PATH)
10098
public Mono<ConfirmationDto> saveItem(@Valid @RequestBody @Parameter(description = "A new item", required = true) ItemDto itemDto){
101-
return itemsService.saveItem(itemDto);
99+
return Mono.just(itemDto).flatMap(itemsService::saveItem);
102100
}
103101

104102

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package com.github.michaelsteven.archetype.springboot.webflux.items.controller;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
import javax.validation.ConstraintViolation;
7+
import javax.validation.ConstraintViolationException;
8+
import javax.validation.ValidationException;
9+
10+
import org.springframework.http.HttpHeaders;
11+
import org.springframework.http.HttpStatus;
12+
import org.springframework.http.ResponseEntity;
13+
import org.springframework.stereotype.Component;
14+
import org.springframework.validation.FieldError;
15+
import org.springframework.web.bind.annotation.ControllerAdvice;
16+
import org.springframework.web.bind.annotation.ExceptionHandler;
17+
import org.springframework.web.bind.annotation.RestController;
18+
import org.springframework.web.bind.support.WebExchangeBindException;
19+
import org.springframework.web.client.HttpServerErrorException;
20+
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
21+
import org.springframework.web.reactive.handler.WebFluxResponseStatusExceptionHandler;
22+
23+
import com.github.michaelsteven.archetype.springboot.webflux.items.model.ApiError;
24+
25+
26+
/**
27+
* The Class RestExceptionHandler.
28+
*
29+
* Use this class to define exit error exception handling. The Controller Advice
30+
* annotation will make it available to the controller so that exceptions caught
31+
* below will be converted to a standard format prior to the error response
32+
* being returned.
33+
*
34+
*/
35+
@ControllerAdvice(annotations = RestController.class)
36+
@Component
37+
public class RestExceptionHandler extends WebFluxResponseStatusExceptionHandler
38+
{
39+
40+
/**
41+
* Handle validation exception.
42+
*
43+
* @param exception the exception
44+
* @param request the request
45+
* @return the response entity
46+
*/
47+
@ExceptionHandler({ ValidationException.class })
48+
public ResponseEntity<Object> handleValidationException(ValidationException exception)
49+
{
50+
List<String> errors = new ArrayList<>();
51+
errors.add(exception.getLocalizedMessage());
52+
ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, exception.getLocalizedMessage(), errors);
53+
return new ResponseEntity<>(apiError, new HttpHeaders(), apiError.getStatus());
54+
}
55+
56+
/**
57+
* Handle constraint violation.
58+
*
59+
* @param exception the exception
60+
* @param request the request
61+
* @return the response entity
62+
*/
63+
@ExceptionHandler({ ConstraintViolationException.class })
64+
public ResponseEntity<Object> handleConstraintViolation(ConstraintViolationException exception)
65+
{
66+
List<String> errors = new ArrayList<>();
67+
for (ConstraintViolation<?> violation : exception.getConstraintViolations())
68+
{
69+
errors.add(violation.getPropertyPath() + ": "
70+
+ violation.getMessage());
71+
}
72+
73+
ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, exception.getLocalizedMessage(), errors);
74+
return new ResponseEntity<>(apiError, new HttpHeaders(), apiError.getStatus());
75+
}
76+
77+
78+
/**
79+
* Handle constraint violation.
80+
*
81+
* @param exception the exception
82+
* @param request the request
83+
* @return the response entity
84+
*/
85+
@ExceptionHandler({ WebExchangeBindException.class })
86+
public ResponseEntity<Object> handleWebExchangeBindException(WebExchangeBindException exception)
87+
{
88+
List<String> errors = new ArrayList<>();
89+
for( FieldError fieldError : exception.getFieldErrors()) {
90+
errors.add( fieldError.getField() + ": " + fieldError.getDefaultMessage());
91+
}
92+
ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, "Validation failed.", errors);
93+
return new ResponseEntity<>(apiError, new HttpHeaders(), apiError.getStatus());
94+
}
95+
/**
96+
* Handle method argument type mismatch.
97+
*
98+
* @param exception the exception
99+
* @param request the request
100+
* @return the response entity
101+
*/
102+
@ExceptionHandler({ MethodArgumentTypeMismatchException.class })
103+
public ResponseEntity<Object> handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException exception)
104+
{
105+
String error = exception.getName() + " should be of type " + exception.getRequiredType().getName();
106+
107+
ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, exception.getLocalizedMessage(), error);
108+
return new ResponseEntity<>(apiError, new HttpHeaders(), apiError.getStatus());
109+
}
110+
111+
/**
112+
* Handle HTTP server error exception.
113+
*
114+
* @param exception the exception
115+
* @return the response entity
116+
*/
117+
@ExceptionHandler({ HttpServerErrorException.class })
118+
public ResponseEntity<Object> handleHttpServerErrorException(HttpServerErrorException exception)
119+
{
120+
String error = exception.getMessage();
121+
ApiError apiError = new ApiError(HttpStatus.BAD_GATEWAY, exception.getLocalizedMessage(), error);
122+
return new ResponseEntity<>(apiError, new HttpHeaders(), apiError.getStatus());
123+
}
124+
125+
/**
126+
* Handle all. A catch-all / fallback handler
127+
*
128+
* @param exception the exception
129+
* @param request the request
130+
* @return the response entity
131+
*/
132+
@ExceptionHandler({ Exception.class })
133+
public ResponseEntity<Object> handleAll(Exception exception)
134+
{
135+
ApiError apiError = new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, exception.getLocalizedMessage(),
136+
"error occurred");
137+
return new ResponseEntity<>(apiError, new HttpHeaders(), apiError.getStatus());
138+
}
139+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.github.michaelsteven.archetype.springboot.webflux.items.model;
2+
3+
import java.time.Instant;
4+
5+
import org.springframework.data.annotation.CreatedBy;
6+
import org.springframework.data.annotation.CreatedDate;
7+
import org.springframework.data.annotation.LastModifiedBy;
8+
import org.springframework.data.annotation.LastModifiedDate;
9+
import org.springframework.data.relational.core.mapping.Column;
10+
11+
import lombok.Data;
12+
13+
/**
14+
* Instantiates a new auditable.
15+
*/
16+
@Data
17+
public abstract class Auditable {
18+
19+
/** The created by. */
20+
@CreatedBy
21+
private String createdBy;
22+
23+
/** The created timestamp. */
24+
@CreatedDate
25+
@Column("created_ts")
26+
private Instant createdTimestamp;
27+
28+
/** The updated by. */
29+
@LastModifiedBy
30+
private String updatedBy;
31+
32+
/** The updated timestamp. */
33+
@LastModifiedDate
34+
@Column("updated_ts")
35+
private Instant updatedTimestamp;
36+
37+
}
Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
package com.github.michaelsteven.archetype.springboot.webflux.items.model;
22

3-
import java.time.Instant;
4-
5-
import org.springframework.data.annotation.CreatedDate;
63
import org.springframework.data.annotation.Id;
7-
import org.springframework.data.relational.core.mapping.Column;
84
import org.springframework.data.relational.core.mapping.Table;
95

106
import lombok.Data;
@@ -17,7 +13,7 @@
1713
@NoArgsConstructor
1814
//@Entity
1915
@Table("items")
20-
public class ItemEntity {
16+
public class ItemEntity extends Auditable {
2117

2218
/** The id. */
2319
@Id
@@ -30,9 +26,4 @@ public class ItemEntity {
3026

3127
/** The description. */
3228
private String description;
33-
34-
/** The created timestamp. */
35-
@CreatedDate
36-
@Column("created_ts")
37-
private Instant createdTimestamp;
3829
}

src/main/java/com/github/michaelsteven/archetype/springboot/webflux/items/service/ItemsService.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
package com.github.michaelsteven.archetype.springboot.webflux.items.service;
22

3-
import java.util.Optional;
4-
53
import javax.validation.Valid;
64
import javax.validation.constraints.NotNull;
75

8-
import org.springframework.data.domain.Page;
96
import org.springframework.data.domain.Pageable;
107

118
import com.github.michaelsteven.archetype.springboot.webflux.items.model.ConfirmationDto;

src/main/java/com/github/michaelsteven/archetype/springboot/webflux/items/service/ItemsServiceImpl.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,20 @@ public Flux<ItemDto> getItems(Pageable pageable){
7676
@Override
7777
@Compliance(action = ComplianceAction.read)
7878
public Mono<ItemDto> getItemById(long id){
79-
return itemRepository.findById(id).map(itemDtoMapper::mapToDto);
79+
return itemRepository.findById(id)
80+
.switchIfEmpty(
81+
Mono.error(
82+
new ValidationException(
83+
messageSource.getMessage(
84+
"itemsservice.validationexception.entitynotfoundforid",
85+
new Object[] { String.valueOf(id) },
86+
LocaleContextHolder.getLocale()
87+
)
88+
)
89+
)
90+
)
91+
92+
.map(itemDtoMapper::mapToDto);
8093
}
8194

8295

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
repositoryaspect.persistenceexception.message=An error occurred while persisting or retrieving items(s).
22
itemscontroller.validationexception.pathiddoesnotmatchobject=ID: {0} in path does not match ID {1} in the object provided.
3-
itemsservice.validationexception.entitynotfoundforid=Item not found for id.
3+
itemsservice.validationexception.entitynotfoundforid=Item not found for id {0}.

0 commit comments

Comments
 (0)