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
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ plugins {
}

group 'ee.bitweb'
version '3.3.0'
version '3.4.0'
java {
sourceCompatibility = '17'
}
Expand Down
3 changes: 1 addition & 2 deletions src/main/java/ee/bitweb/core/api/ControllerAdvisor.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import ee.bitweb.core.api.model.exception.GenericErrorResponse;
import ee.bitweb.core.api.model.exception.PersistenceErrorResponse;
import ee.bitweb.core.api.model.exception.ValidationErrorResponse;
import ee.bitweb.core.exception.persistence.PersistenceException;
import ee.bitweb.core.exception.validation.InvalidFormatValidationException;

import com.fasterxml.jackson.databind.exc.InvalidFormatException;
Expand Down Expand Up @@ -164,7 +163,7 @@ public ValidationErrorResponse handleException(
log(properties.getLogging().getConstraintViolationException(), e.getMessage(), e);

return logAndReturn(
new ValidationErrorResponse(getResponseId(), ExceptionConverter.convert(e))
new ValidationErrorResponse(getResponseId(), ExceptionConverter.convert(e, properties.isShowDetailedFieldNames()))
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class ControllerAdvisorProperties {
static final String PREFIX = "ee.bitweb.core.controller-advice";

private boolean autoConfiguration = false;
private boolean showDetailedFieldNames = false;

@Valid
private Logging logging = new Logging();
Expand Down
19 changes: 12 additions & 7 deletions src/main/java/ee/bitweb/core/api/ExceptionConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;

import java.util.HashSet;
import java.util.Set;
Expand All @@ -20,11 +19,15 @@ public class ExceptionConverter {
public static final String CONSTRAINT_VIOLATION_MESSAGE = "CONSTRAINT_VIOLATION";

public static ValidationException convert(ConstraintViolationException e) {
return convert(e, false);
}

public static ValidationException convert(ConstraintViolationException e, boolean showDetailedFieldNames) {
Set<FieldError> fieldErrors = e
.getConstraintViolations()
.stream()
.map(error -> new FieldError(
getFieldName(error),
getFieldName(error, showDetailedFieldNames),
getValidatorName(error),
error.getMessage()
))
Expand All @@ -40,21 +43,23 @@ public static ValidationException translateBindingResult(BindingResult bindingRe
error.getField(),
error.getCodes() != null ? error.getCodes()[0].split("\\.")[0] : null,
parseMessage(error)
)).collect(Collectors.toList()));
)).toList());

fieldErrors.addAll(bindingResult.getGlobalErrors().stream().map(error -> new FieldError(
error.getObjectName(),
error.getCodes() != null ? error.getCodes()[0].split("\\.")[0] : null,
error.getDefaultMessage()
)).collect(Collectors.toList()));
)).toList());

return new ValidationException(ErrorMessage.INVALID_ARGUMENT.toString(), fieldErrors);
}

private static String getFieldName(ConstraintViolation<?> error) {
String[] parts = error.getPropertyPath().toString().split("\\.");
private static String getFieldName(ConstraintViolation<?> error, boolean showDetailedFieldNames) {
if (showDetailedFieldNames) {
return FieldNameResolver.resolve(error);
}

return parts[parts.length - 1];
return FieldNameResolver.resolveWithRegex(error);
}

private static String getValidatorName(ConstraintViolation<?> constraintViolation) {
Expand Down
60 changes: 60 additions & 0 deletions src/main/java/ee/bitweb/core/api/FieldNameResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package ee.bitweb.core.api;


import jakarta.validation.ConstraintViolation;
import jakarta.validation.ElementKind;
import jakarta.validation.Path;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.hibernate.validator.internal.engine.ConstraintViolationImpl;
import org.hibernate.validator.internal.engine.path.NodeImpl;
import org.hibernate.validator.internal.engine.path.PathImpl;

import java.util.EnumSet;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class FieldNameResolver {

private static final String INDEX_OPEN = "[";
private static final String INDEX_CLOSE = "]";
private static final String FIELD_NAME_DELIMITER = ".";
private static final EnumSet<ElementKind> IGNORED_ELEMENTS = EnumSet.of(ElementKind.METHOD, ElementKind.PARAMETER);

public static String resolve(ConstraintViolation<?> error) {
if (error instanceof ConstraintViolationImpl<?> violationImpl
&& violationImpl.getPropertyPath() instanceof PathImpl pathImpl) {

return resolveFieldName(pathImpl);
}

return resolveWithRegex(error);
}

private static String resolveFieldName(PathImpl path) {
StringBuilder builder = new StringBuilder();
for (Path.Node node : path) {
if (!(node instanceof NodeImpl nodeImpl) || IGNORED_ELEMENTS.contains(node.getKind())) {
continue;
}

if (nodeImpl.isInIterable()) {
builder.append(INDEX_OPEN);
builder.append(nodeImpl.getIndex());
builder.append(INDEX_CLOSE);
}
if (!builder.isEmpty()) {
builder.append(FIELD_NAME_DELIMITER);
}
builder.append(nodeImpl.getName());
}

return builder.toString();
}

public static String resolveWithRegex(ConstraintViolation<?> error) {
String[] parts = error.getPropertyPath().toString().split("\\.");

return parts[parts.length - 1];
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package ee.bitweb.core.api.model.exception;

import com.fasterxml.jackson.databind.ObjectMapper;
import ee.bitweb.core.TestSpringApplication;
import ee.bitweb.core.api.testcomponents.TestPingController;
import ee.bitweb.http.api.response.Error;
import ee.bitweb.http.api.response.ResponseAssertions;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;

import java.util.List;

import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;

@Tag("integration")
@AutoConfigureMockMvc
@ActiveProfiles("MockedInvokerTraceIdCreator")
@SpringBootTest(
classes = TestSpringApplication.class,
properties = {
"ee.bitweb.core.trace.auto-configuration=true",
"ee.bitweb.core.controller-advice.auto-configuration=true",
"ee.bitweb.core.controller-advice.show-detailed-field-names=true"
}
)
class ControllerAdvisorCompleteFileNamesIntegrationTests {

private static final String TRACE_ID_HEADER_NAME = "X-Trace-ID";

@Autowired
private ObjectMapper mapper;

@Autowired
private MockMvc mockMvc;

@Test
void onConstraintViolationExceptionInCodeShouldReturnBadRequestError() throws Exception {
MockHttpServletRequestBuilder mockMvcBuilder = get(TestPingController.BASE_URL + "/constraint-violation")
.header(TRACE_ID_HEADER_NAME, "1234567890");

ResultActions result = mockMvc.perform(mockMvcBuilder).andDo(print());
ResponseAssertions.assertConstraintViolationErrorResponse(
result,
List.of(
Error.notBlank("simpleProperty"),
Error.notNull("simpleProperty")
)
);
assertIdField(result);
}

@Test
void onConstraintViolationExceptionShouldReturnBadRequestError() throws Exception {
TestPingController.ComplexValidatedObject data = new TestPingController.ComplexValidatedObject();
data.setNestedObject(new TestPingController.SimpleValidatedObject());

MockHttpServletRequestBuilder mockMvcBuilder = post(TestPingController.BASE_URL + "/validated")
.header(TRACE_ID_HEADER_NAME, "1234567890")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(mapper.writeValueAsString(data));

ResultActions result = mockMvc.perform(mockMvcBuilder).andDo(print());
ResponseAssertions.assertValidationErrorResponse(
result,
List.of(
Error.notBlank("complexProperty"),
Error.notNull("complexProperty"),
Error.notBlank("nestedObject.simpleProperty"),
Error.notNull("nestedObject.simpleProperty"),
Error.notEmpty("objects"),
Error.notNull("objects")
)
);
assertIdField(result);
}

@Test
void onConstraintViolationExceptionInNestedObjectShouldReturnBadRequestError() throws Exception {
TestPingController.ComplexValidatedObject data = new TestPingController.ComplexValidatedObject();
data.setNestedObject(new TestPingController.SimpleValidatedObject());
data.setObjects(List.of(new TestPingController.SimpleValidatedObject()));

MockHttpServletRequestBuilder mockMvcBuilder = post(TestPingController.BASE_URL + "/validated")
.header(TRACE_ID_HEADER_NAME, "1234567890")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(mapper.writeValueAsString(data));

ResultActions result = mockMvc.perform(mockMvcBuilder).andDo(print());
ResponseAssertions.assertValidationErrorResponse(
result,
List.of(
Error.notBlank("complexProperty"),
Error.notNull("complexProperty"),
Error.notBlank("nestedObject.simpleProperty"),
Error.notNull("nestedObject.simpleProperty"),
Error.notBlank("objects[0].simpleProperty"),
Error.notNull("objects[0].simpleProperty")
)
);
assertIdField(result);
}

@Test
void onConstraintViolationExceptionInListObjectShouldReturnBadRequestError() throws Exception {
TestPingController.SimpleValidatedObject firstValidSimpleObject = new TestPingController.SimpleValidatedObject();
firstValidSimpleObject.setSimpleProperty("property");

TestPingController.ComplexValidatedObject complexObject = new TestPingController.ComplexValidatedObject();
complexObject.setNestedObject(new TestPingController.SimpleValidatedObject());
complexObject.setObjects(List.of(firstValidSimpleObject, new TestPingController.SimpleValidatedObject()));

List<TestPingController.ComplexValidatedObject> data = List.of(
new TestPingController.ComplexValidatedObject(),
complexObject
);

MockHttpServletRequestBuilder mockMvcBuilder = post(TestPingController.BASE_URL + "/validated-list")
.header(TRACE_ID_HEADER_NAME, "1234567890")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(mapper.writeValueAsString(data));

ResultActions result = mockMvc.perform(mockMvcBuilder).andDo(print());
ResponseAssertions.assertConstraintViolationErrorResponse(
result,
List.of(
Error.notBlank("[0].complexProperty"),
Error.notNull("[0].complexProperty"),
Error.notNull("[0].nestedObject"),
Error.notEmpty("[0].objects"),
Error.notNull("[0].objects"),
Error.notBlank("[1].complexProperty"),
Error.notNull("[1].complexProperty"),
Error.notBlank("[1].nestedObject.simpleProperty"),
Error.notNull("[1].nestedObject.simpleProperty"),
Error.notBlank("[1].objects[1].simpleProperty"),
Error.notNull("[1].objects[1].simpleProperty")
)
);
assertIdField(result);
}

private static void assertIdField(ResultActions actions) throws Exception {
actions.andExpect(jsonPath("$.id", is("1234567890_generated-trace-id")));
}
}
Loading