Skip to content

Commit

Permalink
Merge branch 'main' into feature/#124-i18n
Browse files Browse the repository at this point in the history
  • Loading branch information
jpraet authored Nov 28, 2024
2 parents 4508118 + b692c45 commit 07b96a1
Show file tree
Hide file tree
Showing 18 changed files with 209 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,24 @@ void issueWithStatusAndInstance() throws JsonProcessingException {
assertSerializationRoundtrip(problem);
}

@Test
void issueWithNullValue() throws JsonProcessingException {
BadRequestProblem problem = new BadRequestProblem(
new InputValidationIssue(InEnum.BODY, "id", null));
String json = mapper.writeValueAsString(problem);
assertThat(json).doesNotContain("null");
assertSerializationRoundtrip(problem);
}

@Test
void issueWithNullInputValue() throws JsonProcessingException {
BadRequestProblem problem = new BadRequestProblem(new InputValidationIssue()
.inputs(Input.body("a", null), Input.body("b", null)));
String json = mapper.writeValueAsString(problem);
assertThat(json).doesNotContain("null");
assertSerializationRoundtrip(problem);
}

void assertSerializationRoundtrip(Problem problem) throws JsonProcessingException {
String json = mapper.writeValueAsString(problem);
print(json);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.20.3</version>
<version>1.20.4</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.20.3</version>
<version>1.20.4</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
5 changes: 5 additions & 0 deletions belgif-rest-problem-spring-boot-2/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.github.belgif.rest.problem.spring;

import java.util.stream.Stream;

import org.springframework.core.ParameterNameDiscoverer;

import io.github.belgif.rest.problem.internal.CachedAnnotationParameterNameSupport;

/**
* ParameterNameDiscoverer that retrieves the parameter name from Spring MVC annotations (if present).
*/
public class AnnotationParameterNameDiscoverer extends CachedAnnotationParameterNameSupport<String[]>
implements ParameterNameDiscoverer {

@Override
protected String[] toResult(Stream<String> parameterNames) {
return parameterNames.toArray(String[]::new);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import javax.validation.ConstraintViolationException;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
import org.springframework.boot.validation.MessageInterpolatorFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
Expand All @@ -21,6 +23,7 @@
* @see org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration
*/
@Configuration
@AutoConfigureBefore(ValidationAutoConfiguration.class)
public class ProblemValidatorConfiguration {

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.github.belgif.rest.problem.spring;

import static org.assertj.core.api.Assertions.*;

import java.util.stream.Stream;

import org.junit.jupiter.api.Test;

class AnnotationParameterNameDiscovererTest {

private final AnnotationParameterNameDiscoverer discoverer = new AnnotationParameterNameDiscoverer();

@Test
void toResult() {
assertThat(discoverer.toResult(Stream.of("A", "B"))).containsExactly("A", "B");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.github.belgif.rest.problem.spring;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import jakarta.validation.ParameterNameProvider;

import io.github.belgif.rest.problem.internal.CachedAnnotationParameterNameSupport;

/**
* ParameterNameProvider that retrieves the parameter name from Spring MVC annotations (if present).
*/
public class AnnotationParameterNameProvider extends CachedAnnotationParameterNameSupport<List<String>>
implements ParameterNameProvider {

@Override
protected List<String> toResult(Stream<String> parameterNames) {
return parameterNames.collect(Collectors.toList());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.github.belgif.rest.problem.spring;

import jakarta.validation.Configuration;

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.validation.ValidationConfigurationCustomizer;
import org.springframework.stereotype.Component;

/**
* ValidationConfigurationCustomizer that registers the AnnotationParameterNameProvider.
*/
@Component
@ConditionalOnClass(Configuration.class)
public class ProblemValidationConfigurationCustomizer implements ValidationConfigurationCustomizer {

@Override
public void customize(Configuration<?> configuration) {
configuration.parameterNameProvider(new AnnotationParameterNameProvider());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.github.belgif.rest.problem.spring;

import static org.assertj.core.api.Assertions.*;

import java.util.stream.Stream;

import org.junit.jupiter.api.Test;

class AnnotationParameterNameProviderTest {

private final AnnotationParameterNameProvider provider = new AnnotationParameterNameProvider();

@Test
void toResult() {
assertThat(provider.toResult(Stream.of("A", "B"))).containsExactly("A", "B");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.github.belgif.rest.problem.spring;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

import jakarta.validation.Configuration;
import jakarta.validation.ParameterNameProvider;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class ProblemValidationConfigurationCustomizerTest {

private final ProblemValidationConfigurationCustomizer customizer = new ProblemValidationConfigurationCustomizer();

@Mock
private Configuration configuration;

@Captor
private ArgumentCaptor<ParameterNameProvider> parameterNameProviderCaptor;

@Test
void customize() {
when(configuration.parameterNameProvider(parameterNameProviderCaptor.capture())).thenReturn(configuration);
customizer.customize(configuration);
assertThat(parameterNameProviderCaptor.getValue()).isInstanceOf(AnnotationParameterNameProvider.class);
}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.github.belgif.rest.problem.spring;
package io.github.belgif.rest.problem.internal;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
Expand All @@ -7,35 +7,35 @@
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;

import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.MatrixVariable;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;

/**
* ParameterNameDiscoverer that retrieves the parameter name from Spring MVC annotations (if present).
* Helper class for cached retrieval of parameter names from Spring MVC annotations.
*
* @param <T> the result type
*/
public class AnnotationParameterNameDiscoverer implements ParameterNameDiscoverer {
public abstract class CachedAnnotationParameterNameSupport<T> {

private final ConcurrentHashMap<Executable, String[]> parameterNameCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Executable, T> parameterNameCache = new ConcurrentHashMap<>();

@Override
public String[] getParameterNames(Constructor<?> constructor) {
public T getParameterNames(@NonNull Constructor<?> constructor) {
return parameterNameCache.computeIfAbsent(constructor, this::getParameterNames);
}

@Override
public String[] getParameterNames(Method method) {
public T getParameterNames(@NonNull Method method) {
return parameterNameCache.computeIfAbsent(method, this::getParameterNames);
}

private String[] getParameterNames(Executable executable) {
return Arrays.stream(executable.getParameters())
.map(this::getParameterName)
.toArray(String[]::new);
private T getParameterNames(Executable executable) {
return toResult(Arrays.stream(executable.getParameters())
.map(this::getParameterName));
}

private String getParameterName(Parameter parameter) {
Expand Down Expand Up @@ -64,4 +64,6 @@ private String getParameterNameFromAnnotations(Parameter parameter) {
return null;
}

protected abstract T toResult(Stream<String> parameterNames);

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package io.github.belgif.rest.problem.spring;
package io.github.belgif.rest.problem.internal;

import static org.assertj.core.api.Assertions.*;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
Expand All @@ -11,28 +15,34 @@
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;

class AnnotationParameterNameDiscovererTest {
class CachedAnnotationParameterNameSupportTest {

private final AnnotationParameterNameDiscoverer discoverer = new AnnotationParameterNameDiscoverer();
private final CachedAnnotationParameterNameSupport<List<String>> support =
new CachedAnnotationParameterNameSupport<List<String>>() {
@Override
protected List<String> toResult(Stream<String> parameterNames) {
return parameterNames.collect(Collectors.toList());
}
};

@Test
void getParameterNameConstructor() throws Exception {
assertThat(discoverer.getParameterNames(Tested.class.getDeclaredConstructor(String.class, Long.class)))
assertThat(support.getParameterNames(Tested.class.getDeclaredConstructor(String.class, Long.class)))
.containsExactly("name", "other");
}

@Test
void getParameterNamesMethod() throws Exception {
assertThat(
discoverer.getParameterNames(Tested.class.getDeclaredMethod("plainMethod", String.class, Long.class)))
support.getParameterNames(Tested.class.getDeclaredMethod("plainMethod", String.class, Long.class)))
.containsExactly("name", "other");
}

@ParameterizedTest
@ValueSource(
strings = { "requestParam", "pathVariable", "requestHeader", "cookieValue", "matrixVariable", "fallback" })
void getParameterNamesMethodWithJaxRsAnnotation(String method) throws Exception {
assertThat(discoverer.getParameterNames(Tested.class.getDeclaredMethod(method, String.class)))
assertThat(support.getParameterNames(Tested.class.getDeclaredMethod(method, String.class)))
.containsExactly("name");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
*
* @param <V> the input value type
*/
@JsonInclude(value = JsonInclude.Include.NON_EMPTY)
@JsonPropertyOrder({ "in", "name", "value" })
public class Input<V> {

Expand Down Expand Up @@ -52,7 +53,6 @@ public void setName(String name) {
this.name = name;
}

@JsonInclude(value = JsonInclude.Include.ALWAYS, content = JsonInclude.Include.ALWAYS)
public V getValue() {
return value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
*
* @see InputValidationProblem
*/
@JsonInclude(value = Include.NON_DEFAULT)
@JsonInclude(value = Include.NON_EMPTY)
@JsonPropertyOrder(value = { "type", "href", "title", "detail", "in", "name", "value", "inputs" })
public class InputValidationIssue {

Expand Down Expand Up @@ -123,7 +123,6 @@ public void setName(String name) {
this.name = name;
}

@JsonInclude(value = Include.NON_NULL, content = Include.ALWAYS)
public Object getValue() {
return value;
}
Expand Down
5 changes: 4 additions & 1 deletion src/main/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,6 @@ io.github.belgif.rest.problem.scan-additional-problem-packages=com.acme.custom
* *RoutingExceptionsHandler:* an exception handler for RestControllers that converts routing related validation exceptions to HTTP 400 BadRequestProblem.
* *ProblemResponseErrorHandler:* a RestTemplate error handler that converts problem responses to Problem exceptions.
* *ProblemRestTemplateCustomizer:* a RestTemplateCustomizer that registers the ProblemResponseErrorHandler.
* *AnnotationParameterNameDiscoverer:* a bean validation ParameterNameDiscoverer that retrieves parameter names from Spring MVC annotations

In general, these components make it possible to use standard java exception handling (throw and try-catch) for dealing with problems in Spring Boot REST APIs.

Expand All @@ -573,6 +572,8 @@ Rather than depending on <<belgif-rest-problem-spring>> directly, Spring Boot 2.

* *ProblemWebClientCustomizer:* a WebClientCustomizer that registers a filter that converts problem responses to Problem exceptions.
This handles integration with the https://docs.spring.io/spring-framework/reference/web/webflux-webclient.html[Reactive WebClient].
* *AnnotationParameterNameDiscoverer:* a bean validation ParameterNameDiscoverer that retrieves parameter names from Spring MVC annotations
* *ProblemValidatorConfiguration:* registers a LocalValidatorFactoryBean with the AnnotationParameterNameDiscoverer

[[belgif-rest-problem-spring-boot-3]]
==== belgif-rest-problem-spring-boot-3
Expand All @@ -583,3 +584,5 @@ Rather than depending on <<belgif-rest-problem-spring>> directly, Spring Boot 3.
This handles integration with the https://docs.spring.io/spring-framework/reference/web/webflux-webclient.html[Reactive WebClient].
* *NoResourceFoundExceptionHandler:* an exception handler for RestControllers that converts NoResourceFoundException to HTTP 404 ResourceNotFoundProblem.
* *ProblemRestClientCustomizer:* a RestClientCustomizer that registers the ProblemResponseErrorHandler.
* *AnnotationParameterNameProvider:* a bean validation ParameterNameProvider that retrieves parameter names from Spring MVC annotations
* *ProblemValidationConfigurationCustomizer:* a ValidationConfigurationCustomizer that registers the AnnotationParameterNameProvider
Loading

0 comments on commit 07b96a1

Please sign in to comment.