Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions boot-mongodb-elasticsearch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ docker compose -f docker/docker-compose.yml up -d
## Configuration Properties
Key application properties:
```properties
spring.data.mongodb.database=mongoes
spring.data.mongodb.uri=mongodb://localhost:27017/mongoes?replicaSet=rs0
spring.mongodb.database=mongoes
spring.mongodb.uri=mongodb://localhost:27017/mongoes?replicaSet=rs0&readPreference=primary&directConnection=true
spring.elasticsearch.uris=localhost:9200
spring.elasticsearch.socket-timeout=10s
```
Expand Down
17 changes: 6 additions & 11 deletions boot-mongodb-elasticsearch/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.0-M1</version>
<version>4.0.0</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.example.mongoes</groupId>
Expand Down Expand Up @@ -48,7 +48,7 @@
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down Expand Up @@ -89,32 +89,27 @@

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<artifactId>spring-boot-starter-webflux-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mongodb</artifactId>
<artifactId>testcontainers-mongodb</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>elasticsearch</artifactId>
<artifactId>testcontainers-elasticsearch</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,50 @@
import com.example.mongoes.web.exception.DuplicateRestaurantException;
import com.example.mongoes.web.exception.RestaurantNotFoundException;
import jakarta.validation.ConstraintViolationException;
import java.net.URI;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import org.springframework.http.HttpStatusCode;
import org.jspecify.annotations.NonNull;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.server.MissingRequestValueException;
import reactor.core.publisher.Mono;

@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler {

@ExceptionHandler(DuplicateRestaurantException.class)
public Mono<ProblemDetail> handleDuplicateRestaurantException(DuplicateRestaurantException ex) {
public Mono<@NonNull ProblemDetail> handleDuplicateRestaurantException(
DuplicateRestaurantException ex) {
ProblemDetail problemDetail =
ProblemDetail.forStatusAndDetail(ex.getHttpStatus(), ex.getMessage());
problemDetail.setType(URI.create("https://api.mongoes.com/errors/duplicate-restaurant"));
return Mono.just(problemDetail);
}

@ExceptionHandler(RestaurantNotFoundException.class)
Mono<ProblemDetail> handleRestaurantNotFoundException(RestaurantNotFoundException ex) {
Mono<@NonNull ProblemDetail> handleRestaurantNotFoundException(RestaurantNotFoundException ex) {
ProblemDetail problemDetail =
ProblemDetail.forStatusAndDetail(ex.getHttpStatus(), ex.getMessage());
problemDetail.setType(URI.create("https://api.mongoes.com/errors/restaurant-not-found"));
return Mono.just(problemDetail);
}

@ExceptionHandler(WebExchangeBindException.class)
Mono<ProblemDetail> handleValidationErrors(WebExchangeBindException ex) {
Mono<@NonNull ProblemDetail> handleValidationErrors(WebExchangeBindException ex) {
ProblemDetail problemDetail =
ProblemDetail.forStatusAndDetail(
HttpStatusCode.valueOf(400), "Request failed validation checks.");
problemDetail.setTitle("Constraint Violation");
HttpStatus.BAD_REQUEST, "Invalid request content.");
problemDetail.setTitle("Bad Request");
problemDetail.setType(URI.create("https://api.mongoes.com/errors/validation-error"));
List<ApiValidationError> validationErrorsList =
ex.getAllErrors().stream()
.map(
Expand All @@ -56,11 +66,12 @@ Mono<ProblemDetail> handleValidationErrors(WebExchangeBindException ex) {
}

@ExceptionHandler(ConstraintViolationException.class)
Mono<ProblemDetail> handleConstraintViolation(ConstraintViolationException ex) {
Mono<@NonNull ProblemDetail> handleConstraintViolation(ConstraintViolationException ex) {

ProblemDetail problemDetail =
ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(400), "Validation failed");
ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Validation failed");
problemDetail.setTitle("Constraint Violation");
problemDetail.setType(URI.create("https://api.mongoes.com/errors/validation-error"));
List<ApiValidationError> validationErrorsList =
ex.getConstraintViolations().stream()
.map(
Expand All @@ -76,5 +87,13 @@ Mono<ProblemDetail> handleConstraintViolation(ConstraintViolationException ex) {
return Mono.just(problemDetail);
}

@ExceptionHandler(MissingRequestValueException.class)
Mono<@NonNull ProblemDetail> handleException(MissingRequestValueException ex) {
ProblemDetail problemDetail =
ProblemDetail.forStatusAndDetail(ex.getStatusCode(), ex.getBody().getDetail());
problemDetail.setType(URI.create("https://api.mongoes.com/errors/validation-error"));
return Mono.just(problemDetail);
}

record ApiValidationError(String object, String field, Object rejectedValue, String message) {}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.example.mongoes.config;

import org.jspecify.annotations.NonNull;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.geo.GeoModule;
import org.springframework.data.geo.GeoJacksonModule;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;

Expand All @@ -16,16 +17,16 @@ public WebFluxConfig(ApplicationProperties properties) {
}

@Override
public void addCorsMappings(CorsRegistry registry) {
public void addCorsMappings(@NonNull CorsRegistry registry) {
registry.addMapping(properties.getCors().getPathPattern())
.allowedMethods(properties.getCors().getAllowedMethods())
.allowedHeaders(properties.getCors().getAllowedHeaders())
.allowedOriginPatterns(properties.getCors().getAllowedOriginPatterns())
.allowedMethods(properties.getCors().getAllowedMethods().split(","))
.allowedHeaders(properties.getCors().getAllowedHeaders().split(","))
.allowedOriginPatterns(properties.getCors().getAllowedOriginPatterns().split(","))
.allowCredentials(properties.getCors().isAllowCredentials());
}

@Bean
GeoModule jacksonGeoModule() {
return new GeoModule();
GeoJacksonModule geoJacksonModule() {
return new GeoJacksonModule();
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.example.mongoes.document;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import java.time.LocalDateTime;
import java.util.StringJoiner;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import tools.jackson.databind.annotation.JsonDeserialize;
import tools.jackson.databind.ext.javatime.deser.LocalDateTimeDeserializer;

public class Grades {
private String grade;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#clusterURL
spring.data.mongodb.uri=mongodb://localhost:27017,localhost:27018,localhost:27019/mongoes?replicaSet=myReplicaSet
spring.mongodb.uri=mongodb://localhost:27017,localhost:27018,localhost:27019/mongoes?replicaSet=myReplicaSet
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ spring.jackson.serialization.fail-on-empty-beans=false
#spring.data.mongodb.authentication-database=admin
#spring.data.mongodb.username=admin
#spring.data.mongodb.password=passcode
spring.data.mongodb.database=mongoes
spring.data.mongodb.uri=mongodb://localhost:27017/mongoes?replicaSet=rs0&readPreference=primary&directConnection=true
spring.mongodb.database=mongoes
spring.mongodb.uri=mongodb://localhost:27017/mongoes?replicaSet=rs0&readPreference=primary&directConnection=true

spring.elasticsearch.uris=localhost:9200
spring.elasticsearch.socket-timeout=10s
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

import com.example.mongoes.repository.elasticsearch.RestaurantESRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.reactive.server.WebTestClient;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.elasticsearch.ElasticsearchContainer;
import org.testcontainers.mongodb.MongoDBContainer;
import org.testcontainers.utility.DockerImageName;

@TestConfiguration(proxyBeanMethods = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
import org.springframework.boot.test.json.JsonContent;
import org.springframework.context.annotation.Import;
import org.springframework.data.geo.Point;
import org.springframework.data.web.config.SpringDataJacksonConfiguration;
import org.springframework.data.web.config.SpringDataJackson3Configuration;

@JsonTest
@Import(SpringDataJacksonConfiguration.class)
@Import(SpringDataJackson3Configuration.class)
class RestaurantRequestTest {

@Autowired private JacksonTester<RestaurantRequest> jacksonTester;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ void createRestaurant_WithDuplicateName_ShouldReturnConflict() {
.json(
"""
{
"type":"about:blank",
"type":"https://api.mongoes.com/errors/duplicate-restaurant",
"title":"Conflict",
"status":409,
"detail":"Restaurant with name 'Restaurant2' already exists",
Expand Down Expand Up @@ -243,7 +243,7 @@ void findRestaurantById_WithNonExistentId_ShouldReturnNotFound() {
.json(
"""
{
"type":"about:blank",
"type":"https://api.mongoes.com/errors/restaurant-not-found",
"title":"Not Found",
"status":404,
"detail":"Restaurant not found with id: 999999",
Expand All @@ -265,7 +265,7 @@ void findRestaurantByName_WithNonExistentName_ShouldReturnNotFound() {
.json(
"""
{
"type":"about:blank",
"type":"https://api.mongoes.com/errors/restaurant-not-found",
"title":"Not Found",
"status":404,
"detail":"Restaurant not found with name: Non Existent Restaurant",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.webflux.test.autoconfigure.WebFluxTest;
import org.springframework.data.geo.Point;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
Expand Down Expand Up @@ -79,7 +79,7 @@ void findAllRestaurants_WithInvalidLimit_ShouldReturnBadRequest() {
.json(
"""
{
"type": "about:blank",
"type": "https://api.mongoes.com/errors/validation-error",
"title": "Constraint Violation",
"status": 400,
"detail": "Validation failed",
Expand Down Expand Up @@ -114,7 +114,7 @@ void whenRestaurantRequestWithNullName_thenBadRequest() {
.expectBody()
.json(
"""
{"type":"about:blank","title":"Bad Request","status":400,"detail":"Invalid request content.","instance":"/api/restaurant"}
{"type":"https://api.mongoes.com/errors/validation-error","title":"Bad Request","status":400,"detail":"Invalid request content.","instance":"/api/restaurant"}
""");
}

Expand All @@ -136,7 +136,7 @@ void whenRestaurantRequestWithEmptyBorough_thenBadRequest() {
.expectBody()
.json(
"""
{"type":"about:blank","title":"Bad Request","status":400,"detail":"Invalid request content.","instance":"/api/restaurant"}
{"type":"https://api.mongoes.com/errors/validation-error","title":"Bad Request","status":400,"detail":"Invalid request content.","instance":"/api/restaurant"}
""");
}

Expand Down Expand Up @@ -180,7 +180,7 @@ void whenInvalidGrade_willReturns400() {
.expectBody()
.json(
"""
{"type":"about:blank","title":"Bad Request","status":400,"detail":"Invalid request content.","instance":"/api/restaurant/1/grade"}
{"type":"https://api.mongoes.com/errors/validation-error","title":"Bad Request","status":400,"detail":"Invalid request content.","instance":"/api/restaurant/1/grade"}
""");
}

Expand All @@ -204,7 +204,7 @@ void whenNegativeScore_willReturns400() {
.expectBody()
.json(
"""
{"type":"about:blank","title":"Bad Request","status":400,"detail":"Invalid request content.","instance":"/api/restaurant/1/grade"}
{"type":"https://api.mongoes.com/errors/validation-error","title":"Bad Request","status":400,"detail":"Invalid request content.","instance":"/api/restaurant/1/grade"}
""");
}

Expand Down Expand Up @@ -281,7 +281,7 @@ void findRestaurantByName_WithTooLongName_ShouldReturnBadRequest() {
.expectBody()
.json(
"""
{"type":"about:blank","title":"Constraint Violation","status":400,"detail":"Validation failed","instance":"/api/restaurant/name/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","violations":[{"object":"RestaurantController","field":"findRestaurantByName.restaurantName","rejectedValue":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","message":"size must be between 0 and 255"}]}
{"type":"https://api.mongoes.com/errors/validation-error","title":"Constraint Violation","status":400,"detail":"Validation failed","instance":"/api/restaurant/name/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","violations":[{"object":"RestaurantController","field":"findRestaurantByName.restaurantName","rejectedValue":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","message":"size must be between 0 and 255"}]}
""");
}

Expand Down Expand Up @@ -316,7 +316,7 @@ void findRestaurantByName_WithInvalidCharacters_ShouldReturnBadRequest(
.isBadRequest()
.expectBody()
.jsonPath("$.type")
.isEqualTo("about:blank")
.isEqualTo("https://api.mongoes.com/errors/validation-error")
.jsonPath("$.title")
.isEqualTo("Constraint Violation")
.jsonPath("$.status")
Expand Down Expand Up @@ -451,7 +451,7 @@ void createRestaurant_WithInvalidRequest_ShouldReturnBadRequest() {
.expectBody()
.json(
"""
{"type":"about:blank","title":"Bad Request","status":400,"detail":"Invalid request content.","instance":"/api/restaurant"}
{"type":"https://api.mongoes.com/errors/validation-error","title":"Bad Request","status":400,"detail":"Invalid request content.","instance":"/api/restaurant"}
""");
}

Expand Down Expand Up @@ -493,7 +493,7 @@ void updateGradesOfRestaurant_WithInvalidRequest_ShouldReturnBadRequest() {
.expectBody()
.json(
"""
{"type":"about:blank","title":"Constraint Violation","status":400,"detail":"Validation failed","instance":"/api/restaurant/1/grades","violations":[{"object":"RestaurantController","field":"updateGradesOfRestaurant.grades[0].date","rejectedValue":null,"message":"Date cannot be null"},{"object":"RestaurantController","field":"updateGradesOfRestaurant.grades[0].grade","rejectedValue":null,"message":"Grade cannot be blank"},{"object":"RestaurantController","field":"updateGradesOfRestaurant.grades[0].score","rejectedValue":null,"message":"Score cannot be null"}]}
{"type":"https://api.mongoes.com/errors/validation-error","title":"Constraint Violation","status":400,"detail":"Validation failed","instance":"/api/restaurant/1/grades","violations":[{"object":"RestaurantController","field":"updateGradesOfRestaurant.grades[0].date","rejectedValue":null,"message":"Date cannot be null"},{"object":"RestaurantController","field":"updateGradesOfRestaurant.grades[0].grade","rejectedValue":null,"message":"Grade cannot be blank"},{"object":"RestaurantController","field":"updateGradesOfRestaurant.grades[0].score","rejectedValue":null,"message":"Score cannot be null"}]}
""");
}

Expand Down
Loading