Not able to get error object in JSON format while using #Valid and MessageSource to get display errors in Spring boot - spring

I am currently learning Spring REST and I am trying to build a demo spring boot app. Incase of DTO object has validation error I want to show it as below:
{
"errors": [
{
"code": "first_error_code",
"message": "1st error message"
"field":"field_name"
}
]
}
Where the code in above JSON should display the validation message that I have given in my entity class i.e
#NotEmpty(message = "{name.not.empty}")
String name;
then code should be name.not.empty and message should be taken from messages.properties file.
Now to achieve this, I used several tutorials. Below are the classes:
Main class: (Included MessageSource and LocalValidatorFactoryBean)
#SpringBootApplication
#EnableSwagger2
public class Demo3PathvariableApplication implements WebMvcConfigurer {
#Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
#Bean
public LocalValidatorFactoryBean validator(MessageSource messageSource) {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource());
return bean;
}
public static void main(String[] args) {
SpringApplication.run(Demo3PathvariableApplication.class, args);
}
/*
* To enable matrix variables, configurePathMatch() method of WebMvcConfigurer
* needs to overriden. Matrix variables are disabled by default and the
* following configuration
*
* urlPathHelper.setRemoveSemicolonContent(false);
*
* should be present in the overriden method to enable the same. see below
* method.
*/
#Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("GET", "POST");
}
/* For Swagger Document Generation */
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2).select()
.apis(RequestHandlerSelectors.basePackage("com.infytel.controller")).paths(PathSelectors.any()).build()
.useDefaultResponseMessages(false);
// To scan for RestControllers from this package and For disabling default
// response messages
}
}
Controller class:
#RestController
#RequestMapping("/customers")
#Api(value = "CustomerController, REST APIs that deal with Customer DTO")
public class CustomerController {
#Autowired
private CustomerService customerService;
#PostMapping(consumes = "application/json")
public ResponseEntity createCustomer(#RequestBody #Valid CustomerDTO customer, Errors errors) {
return ResponseEntity.ok(customerService.createCustomer(customer));
}
}
FieldErrorDTO.java:
public class FieldErrorDTO {
private String errorCode;
private String message;
private String field;
public FieldErrorDTO(String errorCode, String message, String field) {
this.errorCode = errorCode;
this.message = message;
this.field = field;
}
//Getter setter
ValidationErrorDTO.java:
public class ValidationErrorDTO {
private List<FieldErrorDTO> fieldErrors = new ArrayList<>();
public ValidationErrorDTO() {
super();
}
public void addFieldError(String errorCode, String message, String field) {
FieldErrorDTO error = new FieldErrorDTO(errorCode, message, field);
fieldErrors.add(error);
}
public List<FieldErrorDTO> getFieldErrors() {
return fieldErrors;
}
public void setFieldErrors(List<FieldErrorDTO> fieldErrors) {
this.fieldErrors = fieldErrors;
}
}
RestErrorHandler .java
#ControllerAdvice
public class RestErrorHandler {
#Autowired
private MessageSource messageSource;
#ResponseStatus(BAD_REQUEST)
#ResponseBody
#ExceptionHandler(MethodArgumentNotValidException.class)
public ValidationErrorDTO processValidationError(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
List<org.springframework.validation.FieldError> fieldErrors = result.getFieldErrors();
return processFieldErrors(fieldErrors);
}
private ValidationErrorDTO processFieldErrors(List<FieldError> fieldErrors) {
ValidationErrorDTO dto = new ValidationErrorDTO();
for (FieldError fieldError : fieldErrors) {
String localizedErrorMessage = resolveLocalizedErrorMessage(fieldError);
dto.addFieldError(fieldError.getCode(), localizedErrorMessage, fieldError.getField());
}
return dto;
}
private String resolveLocalizedErrorMessage(FieldError fieldError) {
Locale currentLocale = LocaleContextHolder.getLocale();
String localizedErrorMessage = messageSource.getMessage(fieldError, currentLocale);
return localizedErrorMessage;
}
}
messages.properties
name.not.empty=Please provide a name.
email.not.valid=Please provide valid email id.
age.adult.only=Age should be more than 18.
Now with all these config, I am able to see below JSON,
{
"fieldErrors": [
{
"errorCode": "NotEmpty",
"message": "Please provide a name.",
"field": "name"
},
{
"errorCode": "Email",
"message": "Please provide valid email id.",
"field": "email"
}
]
}
How do I acheive this requirement, where instead of "errorCode": "NotEmpty", I want show
"errorCode": "name.not.empty"
From CustomerDTO class?

To do so you need to change you processFieldErrors:
First remove "{}" from your anotations:
#NotEmpty(message = "name.not.empty")
String name;
Second:
private ValidationErrorDTO processFieldErrors(List<FieldError> fieldErrors) {
ValidationErrorDTO dto = new ValidationErrorDTO();
for (FieldError fieldError : fieldErrors) {
String localizedErrorMessage = resolveLocalizedErrorMessage(fieldError);
dto.addFieldError(fieldError.getDefaultMessage(), localizedErrorMessage, fieldError.getField());
}
return dto;
}
And third, change your message.getMessage:
private String resolveLocalizedErrorMessage(FieldError fieldError) {
Locale currentLocale = LocaleContextHolder.getLocale();
String localizedErrorMessage = messageSource.getMessage(fieldError.getDefaultMessage(), null, currentLocale);
return localizedErrorMessage;
}
This way you would retrieve the key for the message. In your example it will be :
name.not.empty
Hope this helps

Related

EmptyResultDataAccessException when testing Spring controller

In my app, there is a controller, a service, a repo and a class. I am writing unit test to verify my PUT request. In postman, the put request works fine, however, when testing in JUnit test, it throws EmptyResultDataAccessException eror. Many other tests have the same problem and all of them require to find a specific entry in the repo by id. I think this is the problem. Please help me on this.
#Data
#Entity
public class ErrorMessage {
private #Id #GeneratedValue Long id;
private String message;
private int code;
public ErrorMessage() {
}
public ErrorMessage(int code, String message) {
this.code = code;
this.message = message;
}
}
#Repository
interface ErrorMessageRepository extends JpaRepository<ErrorMessage, Long> {
List<ErrorMessage> findByCode(int code);
}
#Service
public class ErrorMessageService {
#Autowired
ErrorMessageRepository repository;
#Transactional
public List<ErrorMessage> getAll()
{
return repository.findAll();
}
#Transactional(readOnly = true)
public Optional<ErrorMessage> getById(Long id)
{
return repository.findById(id);
}
#Transactional(readOnly = true)
public List<ErrorMessage> getByCode(int code)
{
return repository.findByCode(code);
}
#Transactional
public ErrorMessage saveOne(ErrorMessage messages)
{
return repository.save(messages);
}
#Transactional
public Optional<ErrorMessage> deleteById(long id)
{
Optional<ErrorMessage> em = repository.findById(id);
repository.deleteById(id);
return em;
}
#Transactional
public ErrorMessage updateById(long id, ErrorMessage newMessage)
{
ErrorMessage m = repository.findById(id).get();
m.setCode(newMessage.getCode());
m.setMessage(newMessage.getMessage());
repository.save(m);
return m;
}
}
class ErrorMessageController {
private static final Logger log = LoggerFactory.getLogger(ErrorMessageController.class);
#Autowired
ErrorMessageRepository repository;
#Autowired
private ErrorMessageService ems;
#GetMapping("/errormessages")
public List<ErrorMessage> getAll() {
return ems.getAll();
}
#GetMapping("/errormessagesbycode/{code}")
public List<ErrorMessage> getByCode(#PathVariable int code) {
return ems.getByCode(code);
}
#GetMapping("/errormessage/{id}")
ErrorMessage getById(#PathVariable Long id) {
return ems.getById(id)
.orElseThrow(() -> new MessageNotFoundException(id));
}
#PostMapping("/errormessage")
ErrorMessage newMessage(#RequestBody ErrorMessage newMessage) {
return ems.saveOne(newMessage);
}
#DeleteMapping("/errormessage/{id}")
Optional<ErrorMessage> deleteMessage(#PathVariable Long id) {
return ems.deleteById(id);
}
#PutMapping("/errormessage/{id}")
ErrorMessage updateMessage(#PathVariable Long id, #RequestBody ErrorMessage newMessage) {
return ems.updateById(id, newMessage);
}
}
#SpringBootTest
#AutoConfigureMockMvc
public class ErrorMessageTest {
private static ErrorMessage em, emId;
private static ObjectMapper mapper;
#Autowired
private MockMvc mockMvc;
#BeforeAll
public static void init() throws Exception {
mapper = new ObjectMapper();
em = new ErrorMessage(400, "bad request0");
emId = new ErrorMessage(400, "bad request0");
emId.setId(Long.valueOf(1));
}
#Test
void putMessage() throws Exception {
ErrorMessage modifiedMessage = new ErrorMessage(400, "modified");
this.mockMvc.perform(MockMvcRequestBuilders
.put("/errormessage/{id}", emId.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(modifiedMessage)))
.andExpect(status().isOk())
.andExpect(content().string(mapper.writeValueAsString(modifiedMessage)));
}
}
Try this
#Test
void putMessage() throws Exception {
ErrorMessage modifiedMessage = new ErrorMessage(400, "modified");
ErrorMessageService errorMessageService = Mockito.mock(ErrorMessageService.class);
Mockito.when(errorMessageService.updateById(Mockito.any(), Mockito.any())).thenReturn(modifiedMessage);
this.mockMvc.perform(MockMvcRequestBuilders
.put("/errormessage/{id}", emId.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(modifiedMessage)))
.andExpect(status().isOk())
.andExpect(content().string(mapper.writeValueAsString(modifiedMessage)));
}
I found out the bug. The order of the unit test is random. All i need to do is use #Order to ensure the order.

Accept Strings and XML data with RestController

I want to create REST Server which accepts XML requests and plain text into different controllers. I tried to implement this:
#SpringBootApplication
public class Application extends SpringBootServletInitializer implements WebMvcConfigurer {
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
..............
private BasicAuthenticationInterceptor basicAuthenticationInterceptor;
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.removeIf(converter -> converter instanceof MappingJackson2XmlHttpMessageConverter);
converters.removeIf(converter -> converter instanceof MappingJackson2HttpMessageConverter);
converters.add(new MappingJackson2XmlHttpMessageConverter(
((XmlMapper) createObjectMapper(Jackson2ObjectMapperBuilder.xml()))
.enable(ToXmlGenerator.Feature.WRITE_XML_DECLARATION)));
converters.add(new MappingJackson2HttpMessageConverter(createObjectMapper(Jackson2ObjectMapperBuilder.json())));
}
private ObjectMapper createObjectMapper(Jackson2ObjectMapperBuilder builder) {
builder.indentOutput(true);
builder.modules(new JaxbAnnotationModule());
builder.serializationInclusion(JsonInclude.Include.NON_NULL);
builder.defaultUseWrapper(false);
return builder.build();
}
#Autowired
public void setBasicAuthenticationInterceptor(BasicAuthenticationInterceptor basicAuthenticationInterceptor) {
this.basicAuthenticationInterceptor = basicAuthenticationInterceptor;
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(basicAuthenticationInterceptor);
}
}
Check for XML proper formatting:
#ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
PaymentTransaction response;
if (ex.getMessage().contains("Required request body")) {
response = new PaymentTransaction(PaymentTransaction.Response.failed_response, 350,
"Invalid XML message: No XML data received", "XML request parsing failed!");
} else {
response = new PaymentTransaction(PaymentTransaction.Response.failed_response, 351,
"Invalid XML message format", null);
}
return ResponseEntity.badRequest().body(response);
}
}
Controller Class:
#RestController()
public class HomeController {
#Autowired
public HomeController(Map<String, MessageProcessor> processors, Map<String, ReconcileProcessor> reconcileProcessors,
#Qualifier("defaultProcessor") MessageProcessor defaultProcessor,
AuthenticationService authenticationService, ClientRepository repository,
#Value("${request.limit}") int requestLimit) {
// Here I receive XML
}
#GetMapping(value = "/v1/*")
public String message() {
return "REST server";
}
#PostMapping(value = "/v1/{token}", consumes = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE })
public PaymentResponse handleMessage(#PathVariable("token") String token,
#RequestBody PaymentTransaction transaction, HttpServletRequest request) throws Exception {
// Here I receive XML
}
#PostMapping(value = "/v1/notification")
public ResponseEntity<String> handleNotifications(#RequestBody Map<String, String> keyValuePairs) {
// Here I receive key and value in request body
}
#PostMapping(value = "/v1/summary/by_date/{token}", consumes = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE })
public PaymentResponses handleReconcile(#PathVariable("token") String token, #RequestBody Reconcile reconcile,
HttpServletRequest request) throws Exception {
// Here I receive XML
}
#ResponseStatus(value = HttpStatus.UNAUTHORIZED)
public static class UnauthorizedException extends RuntimeException {
UnauthorizedException(String message) {
super(message);
}
}
}
As you can see in some methods I receive XML and in other I receive String in form of key=value&.....
How I configure Spring to accept both types?
Also should I split the Rest controller into different files?
EDIT:
Sample XML request:
<?xml version="1.0" encoding="UTF-8"?>
<payment_transaction>
<transaction_type>authorize</transaction_type>
<transaction_id>2aeke4geaclv7ml80</transaction_id>
<amount>1000</amount>
<currency>USD</currency>
<card_number>22</card_number>
<shipping_address>
<first_name>Name</first_name>
</shipping_address>
</payment_transaction>
Sample XML response:
<?xml version="1.0" encoding="UTF-8"?>
<payment_response>
<transaction_type>authorize</transaction_type>
<status>approved</status>
<unique_id>5f7edd36689f03324f3ef531beacfaae</unique_id>
<transaction_id>asdsdlddea4sdaasdsdsa4dadasda</transaction_id>
<code>500</code>
<amount>101</amount>
<currency>EUR</currency>
</payment_response>
Sample Notification request:
uniqueid=23434&type=sale&status=33
Sample Notification response: It should return only HTTP status OK.
I use:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath />
</parent>
Java version: "10.0.2" 2018-07-17
About the XML generation I use:
#XmlRootElement(name = "payment_transaction")
public class PaymentTransaction {
public enum Response {
failed_response, successful_response
}
#XmlElement(name = "transaction_type")
public String transactionType;
#XmlElement(name = "transaction_id")
public String transactionId;
#XmlElement(name = "usage")
POM Configuration: https://pastebin.com/zXqYhDH3
For Spring boot 2.0.4-RELEASE, it seems you don't have to do a lot.
I made this configuration:
#Configuration
public class WebConfiguration implements WebMvcConfigurer {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//MyRequestBodyHttpMessageConverter converter = new MyRequestBodyHttpMessageConverter();
FormHttpMessageConverter converter = new FormHttpMessageConverter();
//MediaType utf8FormEncoded = new MediaType("application","x-www-form-urlencoded", Charset.forName("UTF-8"));
//MediaType mediaType = MediaType.APPLICATION_FORM_URLENCODED; maybe UTF-8 is not needed
converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED));
//converter.setSupportedMediaTypes(Arrays.asList(utf8FormEncoded));
converters.add(converter);
MappingJackson2HttpMessageConverter conv1 = new MappingJackson2HttpMessageConverter();
conv1.getObjectMapper().registerModule(new JaxbAnnotationModule());
converters.add(conv1);
MappingJackson2XmlHttpMessageConverter conv = new MappingJackson2XmlHttpMessageConverter();
// required by jaxb annotations
conv.getObjectMapper().registerModule(new JaxbAnnotationModule());
converters.add(conv);
}
}
I used about your DTO:
#XmlRootElement(name = "payment_transaction")
public class PaymentTransaction {
#XmlElement(name = "transaction_type")
public String transactionType;
#XmlElement(name = "transaction_id")
public String transactionId;
public String getTransactionType() {
return transactionType;
}
public void setTransactionType(String transactionType) {
this.transactionType = transactionType;
}
public String getTransactionId() {
return transactionId;
}
public void setTransactionId(String transactionId) {
this.transactionId = transactionId;
}
#Override
public String toString() {
return "PaymentTransaction [transactionType=" + transactionType
+ ", transactionId=" + transactionId + "]";
}
}
The controller:
#RestController
public class MyController {
/**
* https://stackoverflow.com/questions/34782025/http-post-request-with-content-type-application-x-www-form-urlencoded-not-workin/38252762#38252762
*/
#PostMapping(value = "/v1/{token}",
consumes = MediaType.APPLICATION_XML_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public #ResponseBody PaymentTransaction handleMessage(#PathVariable("token") String token,
#RequestBody PaymentTransaction transaction, HttpServletRequest request) throws Exception {
System.out.println("handleXmlMessage");
System.out.println(transaction);
PaymentTransaction body = new PaymentTransaction();
body.setTransactionId(transaction.getTransactionId());
body.setTransactionType("received: " + transaction.getTransactionType());
return body;
}
#PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, value = "/v1/notification")
public ResponseEntity<String> handleNotifications(#ModelAttribute PaymentTransaction transaction) {
System.out.println("handleFormMessage");
System.out.println(transaction);
return new ResponseEntity<String>(HttpStatus.OK);
}
}
The only main thing to remember that it seems the filling of the DTO with the parsed data happens by reflection:
For your input
<payment_transaction>
<transaction_id>1</transaction_id>
<transaction_type>name</transaction_type>
</payment_transaction>
I got this response (see my controller):
{
"transactionType": "received: null",
"transactionId": null
}
But when I changed to the name of the fields of the DTO, it started to work (the root element did not matter, interesting):
<payment_transaction>
<transactionId>1</transactionId>
<transactionType>name</transactionType>
</payment_transaction>
result:
{
"transactionType": "received: name",
"transactionId": "1"
}
The same is true for the querystring. I don't know what to change to get spring to parse the xmls using the defined names in #XmlRootElement/#XmlElement.
This is an another solution (it worked well for me) with less Spring magic and using the good old way of HttpServletRequestWrapper.
In the WebMvcConfigurerAdapter class, now we don't need the MessageConverter:
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//MyRequestBodyHttpMessageConverter converter = new MyRequestBodyHttpMessageConverter();
//FormHttpMessageConverter converter = new FormHttpMessageConverter();
//MediaType utf8FormEncoded = new MediaType("application","x-www-form-urlencoded", Charset.forName("UTF-8"));
//MediaType mediaType = MediaType.APPLICATION_FORM_URLENCODED; maybe UTF-8 is not needed
//converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED));
//converter.setSupportedMediaTypes(Arrays.asList(utf8FormEncoded));
//converters.add(converter);
converters.add(new MappingJackson2HttpMessageConverter());
converters.add(new MappingJackson2XmlHttpMessageConverter());
super.configureMessageConverters(converters);
}
And everything else happens in this (servlet) Filter implementation:
#WebFilter("/v1/notification")
public class MyRequestBodyFilter implements Filter {
private static class MyServletInputStream extends ServletInputStream {
private ByteArrayInputStream buffer;
public MyServletInputStream(byte[] contents) {
this.buffer = new ByteArrayInputStream(contents);
}
#Override
public int read() throws IOException {
return buffer.read();
}
#Override
public boolean isFinished() {
return buffer.available() == 0;
}
#Override
public boolean isReady() {
return true;
}
#Override
public void setReadListener(ReadListener listener) {
throw new RuntimeException("Not implemented");
}
}
private class MyHttpServletRequestWrapper extends HttpServletRequestWrapper{
MyHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
#Override
public ServletInputStream getInputStream() throws IOException {
// converting the request parameters to the pojo and serialize it to XML
// the drawback of this way that the xml will be parsed again somewhere later
long id = Long.parseLong(getRequest().getParameter("id"));
String name = getRequest().getParameter("name");
MyRequestBody body = new MyRequestBody();
body.setId(id);
body.setName(name);
return new MyServletInputStream(new XmlMapper().writeValueAsBytes(body));
}
}
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
chain.doFilter(new MyHttpServletRequestWrapper(httpRequest), response);
}
#Override
public void destroy() {
}
}
I have changed nothing in my test controller, so the signature of the methods remained the same:
#PostMapping(value = "/v1/{token}",
consumes = MediaType.APPLICATION_XML_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public #ResponseBody MyResponseBody handleMessage(#PathVariable("token") String token, #RequestBody MyRequestBody transaction, HttpServletRequest request) throws Exception {
MyResponseBody body = new MyResponseBody();
body.setId(transaction.getId());
body.setName("received " + transaction.getName());
return body;
}
#PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, value = "/v1/notification")
public ResponseEntity<String> handleNotifications(#ModelAttribute MyRequestBody transaction) {
return new ResponseEntity<String>(HttpStatus.OK);
}
Update this solution works for pre-2.x Spring-boot versions. Another thing to consider that during my tests I used Jackson's XML annotations on my DTOs (JacksonXmlRootElement, JacksonXmlProperty) and maybe FormHttpMessageConverter can handle DTOs with standard JAXB annotations (see my answer for Spring 2.0.4-RELEASE) - so may you'd better to go to that direction if you can (or at least give it a try before you apply the sketched solution).
This is my solution. I dropped the RequestIntereptor (because that is rather for inspect the request not for modifying it) and the RequestBodyAdvice too (because it turned out that there is a better way.
If you have a look for the available MessageConverters you can see that the only MessageConverter that reads the posted form data is the FormHttpMessageConverter.
The problem with this class is the return type, which is Multivaluemap
But, using this class as a base, I have created an abstract class that reads the form data to this Multivaluemap, and have only one abstract funtion that you have to implement in the subclass: that will create an object from the values stored in the multivaluemap.
Unfortunately I had to introduce an interface (because I kept the original implementation of the writing part just adopt it) on the DTO you would like to read.
All in all, my working solution:
In the WebMvcConfigurerAdapter class, I have this config:
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
MyRequestBodyHttpMessageConverter converter = new MyRequestBodyHttpMessageConverter();
//FormHttpMessageConverter converter = new FormHttpMessageConverter();
MediaType utf8FormEncoded = new MediaType("application","x-www-form-urlencoded", Charset.forName("UTF-8"));
//MediaType mediaType = MediaType.APPLICATION_FORM_URLENCODED; maybe UTF-8 is not needed
//converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED));
converter.setSupportedMediaTypes(Arrays.asList(utf8FormEncoded));
converters.add(converter);
converters.add(new MappingJackson2HttpMessageConverter());
converters.add(new MappingJackson2XmlHttpMessageConverter());
super.configureMessageConverters(converters);
}
I modified a bit your controller functions:
#PostMapping(value = "/v1/{token}",
consumes = MediaType.APPLICATION_XML_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public #ResponseBody MyResponseBody handleMessage(#PathVariable("token") String token, #RequestBody MyRequestBody transaction, HttpServletRequest request) throws Exception {
MyResponseBody body = new MyResponseBody();
body.setId(transaction.getId());
body.setName("received " + transaction.getName());
return body;
}
// check #ModelAttribute workaround https://stackoverflow.com/questions/4339207/http-post-with-request-content-type-form-not-working-in-spring-mvc-3
#PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, value = "/v1/notification")
public ResponseEntity<String> handleNotifications(#ModelAttribute MyRequestBody transaction) {
return new ResponseEntity<String>(HttpStatus.OK);
}
(in the next part the import packages are meaningful, some mail api classes can be found somewhere else)
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.mail.internet.MimeUtility;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.StreamingHttpOutputMessage;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
/**
* based on {#link org.springframework.http.converter.FormHttpMessageConverter
*
* it uses the readed MultiValueMap to build up the DTO we would like to get from the request body.
*/
public abstract class AbstractRequestBodyFormHttpMessageConverter<T extends RequestParamSupport> implements HttpMessageConverter<T> {
/**
* This is the only method you have to implement for your DTO class
* the class must implement RequestParamSupport
*/
protected abstract T buildObject(MultiValueMap<String, Object> valueMap);
public interface RequestParamSupport{
MultiValueMap<String, Object> getRequestParams();
}
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private List<MediaType> supportedMediaTypes = new ArrayList<MediaType>();
private List<HttpMessageConverter<?>> partConverters = new ArrayList<HttpMessageConverter<?>>();
private Charset charset = DEFAULT_CHARSET;
private Charset multipartCharset;
private Class<T> bodyClass;
public AbstractRequestBodyFormHttpMessageConverter(Class<T> bodyClass) {
this.bodyClass = bodyClass;
this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setWriteAcceptCharset(false); // see SPR-7316
this.partConverters.add(new ByteArrayHttpMessageConverter());
this.partConverters.add(stringHttpMessageConverter);
this.partConverters.add(new ResourceHttpMessageConverter());
applyDefaultCharset();
}
/**
* Set the character set to use when writing multipart data to encode file
* names. Encoding is based on the encoded-word syntax defined in RFC 2047
* and relies on {#code MimeUtility} from "javax.mail".
* <p>If not set file names will be encoded as US-ASCII.
* #since 4.1.1
* #see Encoded-Word
*/
public void setMultipartCharset(Charset charset) {
this.multipartCharset = charset;
}
/**
* Apply the configured charset as a default to registered part converters.
*/
private void applyDefaultCharset() {
for (HttpMessageConverter<?> candidate : this.partConverters) {
if (candidate instanceof AbstractHttpMessageConverter) {
AbstractHttpMessageConverter<?> converter = (AbstractHttpMessageConverter<?>) candidate;
// Only override default charset if the converter operates with a charset to begin with...
if (converter.getDefaultCharset() != null) {
converter.setDefaultCharset(this.charset);
}
}
}
}
#Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
if (!bodyClass.isAssignableFrom(clazz)) {
return false;
}
if (mediaType == null) {
return true;
}
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
// We can't read multipart....
if (!supportedMediaType.equals(MediaType.MULTIPART_FORM_DATA) && supportedMediaType.includes(mediaType)) {
return true;
}
}
return false;
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
if (!bodyClass.isAssignableFrom(clazz)) {
return false;
}
if (mediaType == null || MediaType.ALL.equals(mediaType)) {
return true;
}
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
if (supportedMediaType.isCompatibleWith(mediaType)) {
return true;
}
}
return false;
}
/**
* Set the list of {#link MediaType} objects supported by this converter.
*/
public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {
this.supportedMediaTypes = supportedMediaTypes;
}
#Override
public List<MediaType> getSupportedMediaTypes() {
return Collections.unmodifiableList(this.supportedMediaTypes);
}
#Override
public T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
MediaType contentType = inputMessage.getHeaders().getContentType();
Charset charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset);
String body = StreamUtils.copyToString(inputMessage.getBody(), charset);
String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
MultiValueMap<String, Object> result = new LinkedMultiValueMap<String, Object>(pairs.length);
for (String pair : pairs) {
int idx = pair.indexOf('=');
if (idx == -1) {
result.add(URLDecoder.decode(pair, charset.name()), null);
}
else {
String name = URLDecoder.decode(pair.substring(0, idx), charset.name());
String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
result.add(name, value);
}
}
return buildObject(result);
}
#Override
public void write(T object, MediaType contentType,
HttpOutputMessage outputMessage) throws IOException,
HttpMessageNotWritableException {
if (!isMultipart(object, contentType)) {
writeForm(object.getRequestParams(), contentType, outputMessage);
}
else {
writeMultipart(object.getRequestParams(), outputMessage);
}
}
private boolean isMultipart(RequestParamSupport object, MediaType contentType) {
if (contentType != null) {
return MediaType.MULTIPART_FORM_DATA.includes(contentType);
}
MultiValueMap<String, Object> map = object.getRequestParams();
for (String name : map.keySet()) {
for (Object value : map.get(name)) {
if (value != null && !(value instanceof String)) {
return true;
}
}
}
return false;
}
private void writeForm(MultiValueMap<String, Object> form, MediaType contentType,
HttpOutputMessage outputMessage) throws IOException {
Charset charset;
if (contentType != null) {
outputMessage.getHeaders().setContentType(contentType);
charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset);
}
else {
outputMessage.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED);
charset = this.charset;
}
StringBuilder builder = new StringBuilder();
for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) {
String name = nameIterator.next();
for (Iterator<Object> valueIterator = form.get(name).iterator(); valueIterator.hasNext();) {
String value = (String) valueIterator.next();
builder.append(URLEncoder.encode(name, charset.name()));
if (value != null) {
builder.append('=');
builder.append(URLEncoder.encode(value, charset.name()));
if (valueIterator.hasNext()) {
builder.append('&');
}
}
}
if (nameIterator.hasNext()) {
builder.append('&');
}
}
final byte[] bytes = builder.toString().getBytes(charset.name());
outputMessage.getHeaders().setContentLength(bytes.length);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
#Override
public void writeTo(OutputStream outputStream) throws IOException {
StreamUtils.copy(bytes, outputStream);
}
});
}
else {
StreamUtils.copy(bytes, outputMessage.getBody());
}
}
private void writeMultipart(final MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage) throws IOException {
final byte[] boundary = generateMultipartBoundary();
Map<String, String> parameters = Collections.singletonMap("boundary", new String(boundary, "US-ASCII"));
MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters);
HttpHeaders headers = outputMessage.getHeaders();
headers.setContentType(contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
#Override
public void writeTo(OutputStream outputStream) throws IOException {
writeParts(outputStream, parts, boundary);
writeEnd(outputStream, boundary);
}
});
}
else {
writeParts(outputMessage.getBody(), parts, boundary);
writeEnd(outputMessage.getBody(), boundary);
}
}
private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
String name = entry.getKey();
for (Object part : entry.getValue()) {
if (part != null) {
writeBoundary(os, boundary);
writePart(name, getHttpEntity(part), os);
writeNewLine(os);
}
}
}
}
#SuppressWarnings("unchecked")
private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException {
Object partBody = partEntity.getBody();
Class<?> partType = partBody.getClass();
HttpHeaders partHeaders = partEntity.getHeaders();
MediaType partContentType = partHeaders.getContentType();
for (HttpMessageConverter<?> messageConverter : this.partConverters) {
if (messageConverter.canWrite(partType, partContentType)) {
HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os);
multipartMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody));
if (!partHeaders.isEmpty()) {
multipartMessage.getHeaders().putAll(partHeaders);
}
((HttpMessageConverter<Object>) messageConverter).write(partBody, partContentType, multipartMessage);
return;
}
}
throw new HttpMessageNotWritableException("Could not write request: no suitable HttpMessageConverter " +
"found for request type [" + partType.getName() + "]");
}
/**
* Generate a multipart boundary.
* <p>This implementation delegates to
* {#link MimeTypeUtils#generateMultipartBoundary()}.
*/
protected byte[] generateMultipartBoundary() {
return MimeTypeUtils.generateMultipartBoundary();
}
/**
* Return an {#link HttpEntity} for the given part Object.
* #param part the part to return an {#link HttpEntity} for
* #return the part Object itself it is an {#link HttpEntity},
* or a newly built {#link HttpEntity} wrapper for that part
*/
protected HttpEntity<?> getHttpEntity(Object part) {
return (part instanceof HttpEntity ? (HttpEntity<?>) part : new HttpEntity<Object>(part));
}
/**
* Return the filename of the given multipart part. This value will be used for the
* {#code Content-Disposition} header.
* <p>The default implementation returns {#link Resource#getFilename()} if the part is a
* {#code Resource}, and {#code null} in other cases. Can be overridden in subclasses.
* #param part the part to determine the file name for
* #return the filename, or {#code null} if not known
*/
protected String getFilename(Object part) {
if (part instanceof Resource) {
Resource resource = (Resource) part;
String filename = resource.getFilename();
if (filename != null && this.multipartCharset != null) {
filename = MimeDelegate.encode(filename, this.multipartCharset.name());
}
return filename;
}
else {
return null;
}
}
private void writeBoundary(OutputStream os, byte[] boundary) throws IOException {
os.write('-');
os.write('-');
os.write(boundary);
writeNewLine(os);
}
private static void writeEnd(OutputStream os, byte[] boundary) throws IOException {
os.write('-');
os.write('-');
os.write(boundary);
os.write('-');
os.write('-');
writeNewLine(os);
}
private static void writeNewLine(OutputStream os) throws IOException {
os.write('\r');
os.write('\n');
}
/**
* Implementation of {#link org.springframework.http.HttpOutputMessage} used
* to write a MIME multipart.
*/
private static class MultipartHttpOutputMessage implements HttpOutputMessage {
private final OutputStream outputStream;
private final HttpHeaders headers = new HttpHeaders();
private boolean headersWritten = false;
public MultipartHttpOutputMessage(OutputStream outputStream) {
this.outputStream = outputStream;
}
#Override
public HttpHeaders getHeaders() {
return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers);
}
#Override
public OutputStream getBody() throws IOException {
writeHeaders();
return this.outputStream;
}
private void writeHeaders() throws IOException {
if (!this.headersWritten) {
for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
byte[] headerName = getAsciiBytes(entry.getKey());
for (String headerValueString : entry.getValue()) {
byte[] headerValue = getAsciiBytes(headerValueString);
this.outputStream.write(headerName);
this.outputStream.write(':');
this.outputStream.write(' ');
this.outputStream.write(headerValue);
writeNewLine(this.outputStream);
}
}
writeNewLine(this.outputStream);
this.headersWritten = true;
}
}
private byte[] getAsciiBytes(String name) {
try {
return name.getBytes("US-ASCII");
}
catch (UnsupportedEncodingException ex) {
// Should not happen - US-ASCII is always supported.
throw new IllegalStateException(ex);
}
}
}
/**
* Inner class to avoid a hard dependency on the JavaMail API.
*/
private static class MimeDelegate {
public static String encode(String value, String charset) {
try {
return MimeUtility.encodeText(value, charset, null);
}
catch (UnsupportedEncodingException ex) {
throw new IllegalStateException(ex);
}
}
}
}
The bean converter implementation
public class MyRequestBodyHttpMessageConverter extends
AbstractRequestBodyFormHttpMessageConverter<MyRequestBody> {
public MyRequestBodyHttpMessageConverter() {
super(MyRequestBody.class);
}
#Override
protected MyRequestBody buildObject(MultiValueMap<String, Object> valueMap) {
MyRequestBody parsed = new MyRequestBody();
parsed.setId(Long.valueOf((String)valueMap.get("id").get(0)));
parsed.setName((String)valueMap.get("name").get(0));
parsed.setRequestParams(valueMap);
return parsed;
}
}
And finally the MyRequestBody DTO (the MyRequestBody was the same just with different name)
#JacksonXmlRootElement
public class MyRequestBody implements RequestParamSupport, Serializable {
#JsonIgnore
private transient MultiValueMap<String, Object> requestParams;
#JacksonXmlProperty
private Long id;
#JacksonXmlProperty
private String name;
//empty constructor, getters, setters, tostring, etc
#Override
public MultiValueMap<String, Object> getRequestParams() {
return requestParams;
}
}
** Finally my answers: **
How I configure Spring to accept both types?
As you can see, you have to have your own form-data to your bean converter.
(Do not forget that you have to use #ModelAttribute when you are mapping from form data and not #RequestBody.)
Also should I split the Rest controller into different files?
No, that is not necessary, just register your converter.

spring-data-rest: Validator not being invoked

I am using springboot 2.0.1.RELEASE with spring-data-rest and followed the workaround mentioned here and my Validator is still not being invoked. Here are the details:
ValidatorRegistrar: Workaround for a bug
#Configuration
public class ValidatorRegistrar implements InitializingBean {
private static final List<String> EVENTS;
static {
List<String> events = new ArrayList<String>();
events.add("beforeCreate");
events.add("afterCreate");
events.add("beforeSave");
events.add("afterSave");
events.add("beforeLinkSave");
events.add("afterLinkSave");
events.add("beforeDelete");
events.add("afterDelete");
EVENTS = Collections.unmodifiableList(events);
}
#Autowired
ListableBeanFactory beanFactory;
#Autowired
ValidatingRepositoryEventListener validatingRepositoryEventListener;
#Override
public void afterPropertiesSet() throws Exception {
Map<String, Validator> validators = beanFactory.getBeansOfType(Validator.class);
for (Map.Entry<String, Validator> entry : validators.entrySet()) {
EVENTS.stream().filter(p -> entry.getKey().startsWith(p)).findFirst()
.ifPresent(p -> validatingRepositoryEventListener.addValidator(p, entry.getValue()));
}
}
}
Validator class:
#Component("beforeSaveBidValidator")
public class BeforeSaveBidValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
return Bid.class.equals(clazz);
}
#Override
public void validate(Object target, Errors errors) {
Bid bid = (Bid)target;
if (!bid.getAddendaAcknowledged()) {
errors.rejectValue("addendaAcknowledged",
"addendaAcknowledged is not true");
}
}
}
Custom RestController for Bids:
#RestController
#RequestMapping(path = "/bids")
public class BidController {
private BidRepository bidRepository;
#Autowired
public BidController(
BidRepository bidRepository) {
this.bidRepository = bidRepository;
}
#PutMapping("{id}")
public Bid update(#RequestBody #Valid Bid bid) {
return bidRepository.save(bid);
}
}
Rest Client Test Code:
Bid bid = new Bid()
...
bid.setAddendaAcknowledged(false)
Map<String, String> uriVariables = new HashMap<String, String>()
uriVariables.put("id", bid.id)
HttpHeaders headers = new HttpHeaders()
headers.setContentType(MediaType.APPLICATION_JSON)
HttpEntity<Bid> entity = new HttpEntity<>(bid, headers)
ResponseEntity<String> response = restTemplate.exchange(
"/bids/{id}", HttpMethod.PUT, entity, Bid.class, bid.id)
// Expected: response.statusCode == HttpStatus.BAD_REQUEST
// Found: response.statusCode == HttpStatus.OK
// Debugger showed that Validator was never invoked.
Any idea what I am missing?
You are trying to use your validator with custom controller, not SDR controller. In this case you can just add it to your controller with #InitBinder annotation:
#RestController
#RequestMapping("/bids")
public class BidController {
//...
#InitBinder("bid") // add this parameter to apply this binder only to request parameters with this name
protected void bidValidator(WebDataBinder binder) {
binder.addValidators(new BidValidator());
}
#PutMapping("/{id}")
public Bid update(#RequestBody #Valid Bid bid) {
return bidRepository.save(bid);
}
}
#Component annotation on your validator is not necessary as well as ValidatorRegistrar class.
How to use validators with SDR controllers you can read in my another answer.

Jackson configuration to consume list of records in rabbitmq

I am using spring boot amqp in which I will be consuming a list of Employee objects from a queue. My listener method looks like this:
#RabbitListener(queues = "emp_queue")
public void processAndPortEmployeeData(List<Employee> empList) {
empList.forEach(emp -> { some logic })
}
However, when I try to consume the message, I get a class cast exception: For some reason, I'm getting a LinkedHashMap.
Caused by: java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.integration.domain.Employee
If I change my listener method to consume a single employee object, it works fine and I'm using the following jackson configurations for it:
#Configuration
#EnableRabbit
public class RabbitConfiguration implements RabbitListenerConfigurer {
#Bean
public MappingJackson2MessageConverter jackson2Converter() {
return new MappingJackson2MessageConverter();
}
#Bean
public DefaultMessageHandlerMethodFactory handlerMethodFactory() {
DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
factory.setMessageConverter(jackson2Converter());
return factory;
}
#Override
public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
registrar.setMessageHandlerMethodFactory(handlerMethodFactory());
}
}
Is there some other jackson configuration that I need to do to consume the list of employee objects?
Thanks a lot!
Sample Input Json message which I will be consuming:
[
{
"name" : "Jasmine",
"age" : "24",
"emp_id" : 1344
},
{
"name" : "Mark",
"age" : "32",
"emp_id" : 1314
}
]
What version of Spring AMQP are you using?
If 1.6 or greater, the framework passes the argument type to the message converter.
Before 1.6 you either need type information in the message headers, or you need to configure the converter with type information.
That said, since the converter created a map, it implies that was what received (not a list).
Please show a sample of the JSON in a message.
EDIT
Note that boot auto-configures the message converter if there's a single bean of that type...
#SpringBootApplication
public class So40491628Application {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(So40491628Application.class, args);
Resource resource = new ClassPathResource("data.json");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
FileCopyUtils.copy(resource.getInputStream(), baos);
context.getBean(RabbitTemplate.class).send("foo", MessageBuilder.withBody(baos.toByteArray())
.andProperties(MessagePropertiesBuilder.newInstance().setContentType("application/json").build()).build());
Thread.sleep(10000);
context.close();
}
#Bean
public Jackson2JsonMessageConverter converter() {
return new Jackson2JsonMessageConverter();
}
#Bean
public Queue foo() {
return new Queue("foo");
}
#RabbitListener(queues = "foo")
public void listen(List<Employee> emps) {
System.out.println(emps);
}
public static class Employee {
private String name;
private String age;
private int empId;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return this.age;
}
public void setAge(String age) {
this.age = age;
}
public int getEmpId() {
return this.empId;
}
public void setEmpId(int empId) {
this.empId = empId;
}
#Override
public String toString() {
return "Employee [name=" + this.name + ", age=" + this.age + ", empId=" + this.empId + "]";
}
}
}
Result:
[Employee [name=Jasmine, age=24, empId=0], Employee [name=Mark, age=32, empId=0]]

Jersey 2.13 + Bean Validation

I'm using gradle and the following libs:
ext.library['jersey'] = "org.glassfish.jersey:project:2.13"
ext.library['jersey_jettison'] = "org.glassfish.jersey.media:jersey-media-json-jettison:2.13"
ext.library['jersey_jackson'] = "org.glassfish.jersey.media:jersey-media-json-jackson1:2.13"
ext.library['jersey_spring'] = "org.glassfish.jersey.ext:jersey-spring3:2.13"
ext.library['jersey_bean_validation'] = "org.glassfish.jersey.ext:jersey-bean-validation:2.13"
I created the bean validation structure, but its not validating at all. No error messages, nothing. This is the structure I've created:
The DTO
public class MergeSchedulesDto {
#NotNull(message = "validation.invalid.mergeFrom")
private Long mergeFrom;
#NotNull(message = "validation.invalid.mergeTo")
#NotEmpty(message = "validation.invalid.mergeTo")
private List<Long> mergeTo;
The Service
#Path("merge")
#POST
#Consumes({ MediaType.APPLICATION_JSON })
public Response merge(#Valid MergeSchedulesDto dto, #QueryParam("units") List<Long> units) {
The config
public class ApplicationJAXRS extends Application {
public ApplicationJAXRS() {
}
#Override
public Map<String, Object> getProperties() {
Map<String, Object> properties = new HashMap<>();
properties
.put("jersey.config.server.provider.packages",
"com.sifionsolution.sig.academic.resource.service,com.sifionsolution.sig.integration.resource.filter,com.sifionsolution.sig.academic.param.converter,com.sifionsolution.sig.datatables.resource.service,com.sifionsolution.sig.datatables.converter");
return properties;
}
#Override
public Set<Object> getSingletons() {
Set<Object> singletons = new HashSet<>();
singletons.add(new Jackson1Feature());
singletons.add(new ValidationExceptionMapper());
return singletons;
}
}
EDIT I forgot the provider:
#Provider
public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
#Override
public Response toResponse(ConstraintViolationException exception) {
return Response.status(BAD_REQUEST).entity(exception.getMessage()).build();
}
}
EDIT 2: I removed the JUnit test because I didnt test using Jersey Test Framework.
The problem here is that the ValidationExceptionMapper is not beeing called.
Put "#Valid" in your, like this:
#POST
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public Response insert(#Valid T obj) throws Exception{
...
}
This works here.

Resources