How to treat ConversionFailedException as a validation error? And why is th:object null for unrelated fields if ConversionFailedException occurs? - spring

Say in my entity I have a var yearOfBirth: Year?:
#Entity
#Table(name = "employee")
class Employee(
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
#field:NotEmpty
var lastName: String?,
#field:NotEmpty
var email: String?,
#field:Enumerated(value = EnumType.STRING)
var sex: Sex?,
#field:Past
#field:NotNull
var yearOfBirth: Year?,
)
The controller just saves the update. When a client makes a call, an integer is expected for yearOfBirth, but of course, anything could be posted:
#PostMapping("/pages/profile")
fun profileUpdate(#ModelAttribute("employee") #Validated employee: Employee, result: BindingResult, model: Model, principal: Principal) : String {
println(result.hasErrors())
if(result.hasErrors()){
result
.getFieldErrors()
.stream()
.forEach{f -> println(f.getField() + ": " + f.getDefaultMessage())};
println(employee.yearOfBirth)
println(employee.lastName)
return "profile"
}
employeeRepository.save(employee)
return "profile"
}
And this is the part of the template relating to my questions:
<form action="#" th:action="#{/pages/profile}" th:object="${employee}" method="post">
<input type="hidden" th:field="${employee.id}"/>
<input class="input" th:classappend="${#fields.hasErrors('lastName') ? 'is-danger' : ''}" type="text"
th:field="${employee.lastName}"
placeholder="Last name">
<div class="help is-danger" th:if="${#fields.hasErrors('lastName')}" th:errors="*{lastName}"></div>
<input class="input" type="text"
th:field="${employee.yearOfBirth}"
placeholder="1234">
</div>
<div class="help is-danger" th:if="${#fields.hasErrors('yearOfBirth')}" th:errors="*{yearOfBirth}"></div>
<select th:field="${employee.sex}">
<option th:each="sex : ${T(org.example.validation.domain.Sex).values()}"
th:value="${sex}"
th:text="${sex.toString()}"
th:selected="${employee != null && sex.toString() == employee.sex}"></option>
</select>
<div class="help is-danger" th:if="${#fields.hasErrors('sex')}" th:errors="*{sex}"></div>
</form>
With this, if someone enters an invalid year, say asdf, the backend encounters an exception because it can't convert the String to a Year.
yearOfBirth: Failed to convert value of type 'java.lang.String' to required type 'java.time.Year'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [#javax.validation.constraints.Past #javax.validation.constraints.NotNull java.time.Year] for value 'asdf'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [asdf]
The error is passed along as a whole to the frontend.
My first question: how do I treat any marshalling/conversion error as a validation error in my application with a reasonable default message like "invalid type of value" or somesuch? I see I could implement a method tagged with #ExceptionHandler(ConversionFailedException.class) but I don't see how I can access the original object there to pass it back to the frontend.
But what I don't understand at all, is that for another field in the rendered Thymeleaf template, the employee somehow is null if such an exception occurs. If I remove the employee != null check from th:selected="${employee != null && sex.toString() == employee.sex}", then the page cannot be rendered and this exception is thrown:
2022-06-18 15:14:39.794 ERROR 40079 --- [nio-8080-exec-7] org.thymeleaf.TemplateEngine : [THYMELEAF][http-nio-8080-exec-7] Exception processing template "profile": Exception evaluating SpringEL expression: "sex.toString() == employee.sex" (template: "profile" - line 63, col 49)
org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "sex.toString() == employee.sex" (template: "profile" - line 63, col 49)
at org.thymeleaf.spring5.expression.SPELVariableExpressionEvaluator.evaluate(SPELVariableExpressionEvaluator.java:292) ~[thymeleaf-spring5-3.0.14.RELEASE.jar:3.0.14.RELEASE]
(...)
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1007E: Property or field 'sex' cannot be found on null
at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:213) ~[spring-expression-5.3.15.jar:5.3.15]
(...)
2022-06-18 15:14:39.796 ERROR 40079 --- [nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "sex.toString() == employee.sex" (template: "profile" - line 63, col 49)] with root cause
org.springframework.expression.spel.SpelEvaluationException: EL1007E: Property or field 'sex' cannot be found on null
at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:213) ~[spring-expression-5.3.15.jar:5.3.15]
But this only happens if the validation error is from a ConversionFailedException. For other validation errors (e.g. NotEmpty from the lastName field), employee remains not null. It also only affects this th:selected expression, but in other places (e.g. th:field="${employee.lastName}" above), the object is still not null. How can that even be? Why is employee null in one expression of the template but not another?
I have a complete but minimal working example but I'm not sure where to upload it.

Related

Assert multiple field error codes from Validation using MockMvc

I am trying to assert two errors due to two given constraints to my form. My form has two constraints on its single field:
#Data
#NoArgsConstructor
#NotExistingGroup(groups = SecondGroupValidation.class)
public class GroupForm {
#NotBlank(groups = FirstGroupValidation.class)
#Size(min = 2, max = 30, groups = FirstGroupValidation.class)
private String name;
}
With the following test, I want to trigger both the #NotBlank and #Size validation and assert both raised errors:
#Test
void givenGroupEmptyName_groupPost_assertErrors() throws Exception {
mvc.perform(post("/groups/add").param("name", ""))
.andDo(print())
.andExpect(status().isOk())
.andExpect(view().name("groups-add"))
.andExpect(model().hasErrors())
.andExpect(model().attributeErrorCount("groupForm", 2))
.andExpect(model().attributeHasFieldErrorCode("groupForm", "name", "NotBlank"))
.andExpect(model().attributeHasFieldErrorCode("groupForm", "name", "Size"));
}
The mvc doPrint() method shows both errors are given
ModelAndView:
View name = groups-add
View = null
Attribute = groupForm
value = GroupForm(name=)
errors = [Field error in object 'groupForm' on field 'name': rejected value []; codes [NotBlank.groupForm.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [groupForm.name,name]; arguments []; default message [name]]; default message [must not be blank], Field error in object 'groupForm' on field 'name': rejected value []; codes [Size.groupForm.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [groupForm.name,name]; arguments []; default message [name],30,2]; default message [size must be between 2 and 30]]
However, the test breaks with the following error
java.lang.AssertionError: Field error code expected:<Size> but was:<NotBlank>

MethodArgumentNotValidException not thrown in case of validation fail

I'm trying to implement validation in Spring REST by following this tutorial. Though, my code is in Koltin unlike the tutorial.
My code is as follows -
Entity class
#Entity
class PodcastEntity(#Id #GeneratedValue(strategy = GenerationType.AUTO) #NotNull
var id: Long = 0,
#field:NotEmpty(message = "Please provide an author")
var author: String,
#field:NotEmpty(message = "Please provide a title")
var title: String,
#field:NotEmpty(message = "Please provide a description")
var description: String,
#field:NotEmpty(message = "Please provide category one")
var categoryOne: String,
#field:NotEmpty(message = "Please provide category two")
var categoryTwo: String,
var filePath: String = "")
My post method is like this in the controller -
#PostMapping("details")
fun addPodcast(#Valid #RequestBody podcastEntity: PodcastEntity) {
podcastService.addPodcast(podcastEntity)
}
My POST request in postman is like this -
{
"author" : "me 3",
"title" : "File three",
"description" : "this is a test desc"
}
Since categoryOne and categoryTwo are missing and I have not handled the exception on my own, my console should show MethodArgumentNotValidException according to the tutorial. However, I'm getting no such exception. What I'm getting is a HttpMessageNotReadableException exception -
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Instantiation of [simple type, class com.krtkush.test.entities.PodcastEntity] value failed for JSON property categoryOne due to missing (therefore NULL) value for creator parameter categoryOne which is a non-nullable type; nested exception is com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class com.krtkush.test.entities.PodcastEntity] value failed for JSON property categoryOne due to missing (therefore NULL) value for creator parameter categoryOne which is a non-nullable type at [Source: (PushbackInputStream); line: 5, column: 1] (through reference chain: com.krtkush.test.entities.PodcastEntity["categoryOne"])]
I'm unable to understand where I'm going wrong. Some help please?
You can handle this issue by providing HttpMessageNotReadableException handler
and then checking if the main cause is MissingKotlinParameterException.
After that, you can provide custom validation error.
#ExceptionHandler
override fun handleMessageNotReadableException(
exception: HttpMessageNotReadableException,
request: NativeWebRequest
): ResponseEntity<Problem> {
// workaround
val cause = exception.cause
if (cause is MissingKotlinParameterException) {
val violations = setOf(createMissingKotlinParameterViolation(cause))
return newConstraintViolationProblem(exception, violations, request)
}
return create(Status.BAD_REQUEST, UnableToReadInputMessageProblem(), request)
}
private fun createMissingKotlinParameterViolation(cause: MissingKotlinParameterException): Violation {
val name = cause.path.fold("") { jsonPath, ref ->
val suffix = when {
ref.index > -1 -> "[${ref.index}]"
else -> ".${ref.fieldName}"
}
(jsonPath + suffix).removePrefix(".")
}
return Violation(name, "must not be null")
}
This way you get get nice output with proper constraint error.
You may try to declare #ExceptionHandler for MissingKotlinParameterException directly.
Answer based on question Spring not null validation throwing HttpMessageNotReadableException instead of MethodArgumentNotValidException in kotlin
Following Damian's SO link in his answer, I found the first answer really helpful and more appropriate. I modified the #Entitiy class by making the required fields nullable (?) like this -
#Entity
class PodcastEntity(#Id #GeneratedValue(strategy = GenerationType.AUTO)
var id: Long = 0,
#field:NotEmpty(message = "Please provide an author")
var author: String?,
#field:NotEmpty(message = "Please provide a title")
var title: String?,
#field:NotEmpty(message = "Please provide a description")
var description: String?,
#field:NotEmpty(message = "Please provide category one")
var categoryOne: String?,
#field:NotEmpty(message = "Please provide category two")
var categoryTwo: String?,
var filePath: String = "")
This makes sure that the code throws MethodArgumentNotValidException in all three cases - 1. Empty argument 2. null argument 3. Missing argument

Mybatis: IllegalArgumentException: Mapped Statements collection does not contain value for xxx

I have two entities Vendor and Goods with one-to-many relation, the relation looks like:
I am using mybatis with annotation, the mapper:
GoodsMapper
public interface GoodsMapper {
#Select("select * from goods where id=#{goodsId}")
#Results({
#Result(id = true, column = "id", property = "id"),
#Result(column = "name", property = "name"),
#Result(column = "vendor_id", property = "vendor",
one = #One(select = "com.xxx.server.mapper.VendorMapper.getVendor"))
})
Goods getGoods(#Param("goodsId") String goodsId);
}
VendorMapper
public interface VendorMapper {
#Select("select * from vendor where id=#{vendorId}")
Vendor getVendor(#Param("vendorId") String vendorId);
}
ignore the entity code & others...
when I invoked goodsMapper.getGoods(goodsId), I caught the following exception :
Caused by: org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.lang.IllegalArgumentException: Mapped Statements collection does not contain value for com.xxx.server.mapper.VendorMapper.getVendor
### The error may exist in com/xxx/server/mapper/GoodsMapper.java (best guess)
### The error may involve com.xxx.server.mapper.GoodsMapper.getGoods
### The error occurred while handling results
### SQL: select * from goods where id=?
### Cause: java.lang.IllegalArgumentException: Mapped Statements collection does not contain value for com.xxx.server.mapper.VendorMapper.getVendor
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:150)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:77)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
... 117 more
Caused by: java.lang.IllegalArgumentException: Mapped Statements collection does not contain value for com.xxx.server.mapper.VendorMapper.getVendor
at org.apache.ibatis.session.Configuration$StrictMap.get(Configuration.java:933)
at org.apache.ibatis.session.Configuration.getMappedStatement(Configuration.java:726)
at org.apache.ibatis.session.Configuration.getMappedStatement(Configuration.java:719)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getNestedQueryMappingValue(DefaultResultSetHandler.java:740)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getPropertyMappingValue(DefaultResultSetHandler.java:465)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.applyPropertyMappings(DefaultResultSetHandler.java:441)
I have checked the class path com.xxx.server.mapper.VendorMapper.getVendor for the select of #One, it is correct.
Appreciate any kind help~
In my case this was caused by referenced collection not being initialized by Spring yet.
Solution is to add #DependsOn annotation to the "parent" mapper.
#DependsOn("VendorMapper")
public interface GoodsMapper{
...
}
#Repository("VendorMapper")
public interface VendorMapper {
...
}

How to populate Form from Spring dropdown when form is submitted

My code lists the data that I would like to see, but when I hit submit it fails to populate the backing form with exception below. How can I make the bindings work? The exception I get is of Mismatch type, trying to insert a String in List when expecting Objects. Which makes sense.
Example
<tr>
<td><form:select id="myTypes" path="myTypes" multiple="false">
<form:option value="NONE" label="--- Select ---" />
<form:options items="${form.myTypes}" itemValue="id" itemLabel="label"/>
</form:select>
</td>
<td><form:errors path="myTypes" cssClass="error" /></td>
This is how form looks like
public class MyForm {
List<MyType> myTypes;
public List<MyType> getMyTypes() {
return myTypes;
}
public void setMyTypes(List<MyType> myTypes) {
this.myTypes = myTypes;
}
}
And of course MyType has id and label.
Link to above sample code and exception below
HTTP Status 500 - Request processing failed; nested exception is org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'Form' on field 'myTypes': rejected value [8768743658734587345]; codes [typeMismatch.Form.myTypes,typeMismatch.myTypes,typeMismatch.java.util.List,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [myForm.myTypes,myTypes]; arguments []; default message [myTypes]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.util.List' for property 'myTypes'; nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.String] to required type [com.x.x.MyTypeEntity] for property 'myTypes[0]': no matching editors or conversion strategy found]
Solution:
Make sure you are mapping to Single element and not to the list :)
It should be something like
<form:select path="someEntity[${status.index}].myType.id">
HTH
I think the problem is that Spring does not know how to convert the selected option value (which is a String posted towards your app as an HTTP parameter named "myTypes" when you submit the form) to a MyType object. You should configure a Formatter< MyType > and register it to the Spring FormatterRegistry (see Spring doc) to let Spring know how to convert the incoming String to a MyType object.
public class MyTypeFormatter implements Formatter<MyType> {
#Override
public MyType parse(String text, Locale locale) throws ParseException {
return myTypeService.getType(text); // for example
}
public String print(MyType t, Locale locale) {
return t.getId();// for example
};
}
By the way, if I may, since your dropdown list is not multiple, it means that you are going to select just one of the available MyType options. The path of the < form:select > should be named "myType" instead of "myTypes" and especially, it should refer to a MyType attribute within your Form object and not to a List< MyType > attribute. Maybe you should name your first list of available MyType objects "availableTypes" and create a second attribute named "selectedType" to bind the MyType object corresponding to the selected option on the GUI.

JAXB-ElipseLink: Marshaller not validating

I would like my Eclipselink 2.3 Marshaller to perform validation upon marshalling.
I have made sure that the Schema is correctly created by a SchemaFactory, i am passing it to Marshaller.setSchema and i have registered a handler via Marshaller.setEventHandler().
The marshal result is clearly not valid acc. to its Schema (verified in Eclipse), nevertheless i can see that my breakpoint in handleEvent(ValidationEvent event) is never hit.
I am marshalling XML-Fragments using marshal(Object, XMLStreamWriter) and would expect the Marshaller to perform validation on these fragments according to the Schema i passed.
Anybody any idea why this is not happening?
EDIT:
The Validation error that should occur: 2 missing attributes on an element.
The element corresponds to a Java-Object that is contained in a List<>. I am marshalling the List using:
<xml-element java-attribute="listInstance" xml-path="ListWrapperElement/ListElement" type="foo.ElementType" container-type="java.util.ArrayList"/>
The mapping for the element itself:
<java-type name="foo.ElementType" xml-accessor-type="PROPERTY">
<java-attributes>
// just <xml-attribute> elements here
</java-attributes>
</java-type>
Therefore all attributes are marshalled to ListWrapperElement/ListElement/#attribute.
2 of these are missing and not detected by validation.
I have not been able to reproduce the issue that you are seeing. Below is what I have tried (adapted from the follow blog post):
http://blog.bdoughan.com/2010/12/jaxb-and-marshalunmarshal-schema.html
MarshalDemo (adapted from blog post)
import java.io.File;
import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.eclipse.persistence.Version;
public class MarshalDemo {
public static void main(String[] args) throws Exception {
Customer customer = new Customer();
customer.setName("Jane Doe");
customer.getPhoneNumbers().add(new PhoneNumber());
customer.getPhoneNumbers().add(new PhoneNumber());
customer.getPhoneNumbers().add(new PhoneNumber());
SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
Schema schema = sf.newSchema(new File("src/blog/jaxb/validation/customer.xsd"));
JAXBContext jc = JAXBContext.newInstance(Customer.class);
System.out.println(jc.getClass());
System.out.println(Version.getVersion());
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.setSchema(schema);
marshaller.setEventHandler(new MyValidationEventHandler());
XMLStreamWriter xsw = XMLOutputFactory.newFactory().createXMLStreamWriter(System.out);
marshaller.marshal(customer, xsw);
}
}
Output
class org.eclipse.persistence.jaxb.JAXBContext
2.3.0
EVENT
SEVERITY: 1
MESSAGE: cvc-maxLength-valid: Value 'Jane Doe' with length = '8' is not facet-valid with respect to maxLength '5' for type 'stringWithMaxSize5'.
LINKED EXCEPTION: org.eclipse.persistence.oxm.record.ValidatingMarshalRecord$MarshalSAXParseException: cvc-maxLength-valid: Value 'Jane Doe' with length = '8' is not facet-valid with respect to maxLength '5' for type 'stringWithMaxSize5'.
LOCATOR
LINE NUMBER: -1
COLUMN NUMBER: -1
OFFSET: -1
OBJECT: forum8924293.Customer#ef2c60
NODE: null
URL: null
EVENT
SEVERITY: 1
MESSAGE: cvc-type.3.1.3: The value 'Jane Doe' of element 'name' is not valid.
LINKED EXCEPTION: org.eclipse.persistence.oxm.record.ValidatingMarshalRecord$MarshalSAXParseException: cvc-type.3.1.3: The value 'Jane Doe' of element 'name' is not valid.
LOCATOR
LINE NUMBER: -1
COLUMN NUMBER: -1
OFFSET: -1
OBJECT: forum8924293.Customer#ef2c60
NODE: null
URL: null
EVENT
SEVERITY: 1
MESSAGE: cvc-complex-type.2.4.d: Invalid content was found starting with element 'customer'. No child element '{phone-number}' is expected at this point.
LINKED EXCEPTION: org.eclipse.persistence.oxm.record.ValidatingMarshalRecord$MarshalSAXParseException: cvc-complex-type.2.4.d: Invalid content was found starting with element 'customer'. No child element '{phone-number}' is expected at this point.
LOCATOR
LINE NUMBER: -1
COLUMN NUMBER: -1
OFFSET: -1
OBJECT: forum8924293.Customer#ef2c60
NODE: null
URL: null
<?xml version="1.0"?><customer><name>Jane Doe</name><phone-number></phone-number><phone-number></phone-number><phone-number></phone-number></customer>

Resources