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.
I created a small example project to show two problems I'm experiencing in the configuration of Spring Boot validation and its integration with Hibernate.
I already tried other replies I found about the topic but unfortunately they didn't work for me or that asked to disable Hibernate validation.
I want use a custom Validator implementing ConstraintValidator<ValidUser, User> and inject in it my UserRepository.
At the same time I want to keep the default behaviour of Hibernate that checks for validation errors during update/persist.
I write here for completeness main sections of the app.
Custom configuration
In this class I set a custom validator with a custom MessageSource, so Spring will read messages from the file resources/messages.properties
#Configuration
public class CustomConfiguration {
#Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath:/messages");
messageSource.setUseCodeAsDefaultMessage(false);
messageSource.setCacheSeconds((int) TimeUnit.HOURS.toSeconds(1));
messageSource.setFallbackToSystemLocale(false);
return messageSource;
}
#Bean
public LocalValidatorFactoryBean validator() {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
factoryBean.setValidationMessageSource(messageSource());
return factoryBean;
}
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
methodValidationPostProcessor.setValidator(validator());
return methodValidationPostProcessor;
}
}
The bean
Nothing special here if not the custom validator #ValidUser
#ValidUser
#Entity
public class User extends AbstractPersistable<Long> {
private static final long serialVersionUID = 1119004705847418599L;
#NotBlank
#Column(nullable = false)
private String name;
/** CONTACT INFORMATION **/
#Pattern(regexp = "^\\+{1}[1-9]\\d{1,14}$")
private String landlinePhone;
#Pattern(regexp = "^\\+{1}[1-9]\\d{1,14}$")
private String mobilePhone;
#NotBlank
#Column(nullable = false, unique = true)
private String username;
#Email
private String email;
#JsonIgnore
private String password;
#Min(value = 0)
private BigDecimal cashFund = BigDecimal.ZERO;
public User() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLandlinePhone() {
return landlinePhone;
}
public void setLandlinePhone(String landlinePhone) {
this.landlinePhone = landlinePhone;
}
public String getMobilePhone() {
return mobilePhone;
}
public void setMobilePhone(String mobilePhone) {
this.mobilePhone = mobilePhone;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public BigDecimal getCashFund() {
return cashFund;
}
public void setCashFund(BigDecimal cashFund) {
this.cashFund = cashFund;
}
}
Custom validator
Here is where I try to inject the repository. The repository is always null if not when I disable Hibernate validation.
public class UserValidator implements ConstraintValidator<ValidUser, User> {
private Logger log = LogManager.getLogger();
#Autowired
private UserRepository userRepository;
#Override
public void initialize(ValidUser constraintAnnotation) {
}
#Override
public boolean isValid(User value, ConstraintValidatorContext context) {
try {
User foundUser = userRepository.findByUsername(value.getUsername());
if (foundUser != null && foundUser.getId() != value.getId()) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("{ValidUser.unique.username}").addConstraintViolation();
return false;
}
} catch (Exception e) {
log.error("", e);
return false;
}
return true;
}
}
messages.properties
#CUSTOM VALIDATORS
ValidUser.message = I dati inseriti non sono validi. Verificare nuovamente e ripetere l'operazione.
ValidUser.unique.username = L'username [${validatedValue.getUsername()}] è già stato utilizzato. Sceglierne un altro e ripetere l'operazione.
#DEFAULT VALIDATORS
org.hibernate.validator.constraints.NotBlank.message = Il campo non può essere vuoto
# === USER ===
Pattern.user.landlinePhone = Il numero di telefono non è valido. Dovrebbe essere nel formato E.123 internazionale (https://en.wikipedia.org/wiki/E.123)
In my tests, you can try from the source code, I've two problems:
The injected repository inside UserValidator is null if I don't disable Hibernate validation (spring.jpa.properties.javax.persistence.validation.mode=none)
Even if I disable Hibernate validator, my test cases fail because something prevent Spring to use the default string interpolation for validation messages that should be something like [Constraint].[class name lowercase].[propertyName]. I don't want to use the constraint annotation with the value element like this #NotBlank(message="{mycustom.message}") because I don't see the point considering that has his own convetion for interpolation and I can take advantage of that...that means less coding.
I attach the code; you can just run Junit tests and see errors (Hibernate validation is enable, check application.properties).
What am I doing wrong? What could I do to solve those two problems?
====== UPDATE ======
Just to clarify, reading Spring validation documentation https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation-beanvalidation-spring-constraints they say:
By default, the LocalValidatorFactoryBean configures a SpringConstraintValidatorFactory that uses Spring to create ConstraintValidator instances. This allows your custom ConstraintValidators to benefit from dependency injection like any other Spring bean.
As you can see, a ConstraintValidator implementation may have its dependencies #Autowired like any other Spring bean.
In my configuration class I created my LocalValidatorFactoryBean as they write.
Another interesting questions are this and this, but I had not luck with them.
====== UPDATE 2 ======
After a lot of reseach, seems with Hibernate validator the injection is not provided.
I found a couple of way you can do that:
1st way
Create this configuration class:
#Configuration
public class HibernateValidationConfiguration extends HibernateJpaAutoConfiguration {
public HibernateValidationConfiguration(DataSource dataSource, JpaProperties jpaProperties,
ObjectProvider<JtaTransactionManager> jtaTransactionManager,
ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
super(dataSource, jpaProperties, jtaTransactionManager, transactionManagerCustomizers);
}
#Autowired
private Validator validator;
#Override
protected void customizeVendorProperties(Map<String, Object> vendorProperties) {
super.customizeVendorProperties(vendorProperties);
vendorProperties.put("javax.persistence.validation.factory", validator);
}
}
2nd way
Create an utility bean
#Service
public class BeanUtil implements ApplicationContextAware {
private static ApplicationContext context;
#Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static <T> T getBean(Class<T> beanClass) {
return context.getBean(beanClass);
}
}
and then in the validator initialization:
#Override
public void initialize(ValidUser constraintAnnotation) {
userRepository = BeanUtil.getBean(UserRepository.class);
em = BeanUtil.getBean(EntityManager.class);
}
very important
In both cases, in order to make the it works you have to "reset" the entity manager in this way:
#Override
public boolean isValid(User value, ConstraintValidatorContext context) {
try {
em.setFlushMode(FlushModeType.COMMIT);
//your code
} finally {
em.setFlushMode(FlushModeType.AUTO);
}
}
Anyway, I don't know if this is really a safe way. Probably it's not a good practice access to the persistence layer at all.
If you really need to use injection in your Validator try adding #Configurable annotation on it:
#Configurable(autowire = Autowire.BY_TYPE, dependencyCheck = true)
public class UserValidator implements ConstraintValidator<ValidUser, User> {
private Logger log = LogManager.getLogger();
#Autowired
private UserRepository userRepository;
// this initialize method wouldn't be needed if you use HV 6.0 as it has a default implementation now
#Override
public void initialize(ValidUser constraintAnnotation) {
}
#Override
public boolean isValid(User value, ConstraintValidatorContext context) {
try {
User foundUser = userRepository.findByUsername( value.getUsername() );
if ( foundUser != null && foundUser.getId() != value.getId() ) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate( "{ValidUser.unique.username}" ).addConstraintViolation();
return false;
}
} catch (Exception e) {
log.error( "", e );
return false;
}
return true;
}
}
From the documentation to that annotation:
Marks a class as being eligible for Spring-driven configuration
So this should solve your null problem. To make it work though, you would need to configure AspectJ... (Check how to use #Configurable in Spring for that)
I'm getting the following error when I'm trying to run a test:
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.IllegalStateException: Invalid target for Validator [userCreateFormValidator bean]: com.ar.empresa.forms.UserCreateForm#15c3585
Caused by: java.lang.IllegalStateException: Invalid target for Validator [userCreateFormValidator bean]: com.ar.empresa.forms.UserCreateForm#15c3585
at org.springframework.validation.DataBinder.assertValidators(DataBinder.java:567)
at org.springframework.validation.DataBinder.addValidators(DataBinder.java:578)
at com.ar.empresa.controllers.UserController.initBinder(UserController.java:36)
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)
The code is:
Controller:
#Controller
public class UserController {
private UserService userService;
private UserCreateFormValidator userCreateFormValidator;
#Autowired
public UserController(UserService userService, UserCreateFormValidator userCreateFormValidator) {
this.userService = userService;
this.userCreateFormValidator = userCreateFormValidator;
}
#InitBinder("form")
public void initBinder(WebDataBinder binder) {
binder.addValidators(userCreateFormValidator);
}
#PreAuthorize("hasAuthority('ADMIN')")
#RequestMapping(value = "/user/create", method = RequestMethod.GET)
public ModelAndView getUserCreatePage() {
return new ModelAndView("user_create", "form", new UserCreateForm());
}
#PreAuthorize("hasAuthority('ADMIN')")
#RequestMapping(value = "/user/create", method = RequestMethod.POST)
public String handleUserCreateForm(#Valid #ModelAttribute("form") UserCreateForm form, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "user_create";
}
try {
userService.create(form);
} catch (DataIntegrityViolationException e) {
bindingResult.reject("email.exists", "Email already exists");
return "user_create";
}
return "redirect:/users";
}
}
Validator:
#Component
public class UserCreateFormValidator implements Validator {
private final UserService userService;
#Autowired
public UserCreateFormValidator(UserService userService) {
this.userService = userService;
}
#Override
public boolean supports(Class<?> clazz) {
return clazz.equals(UserCreateForm.class);
}
#Override
public void validate(Object target, Errors errors) {
UserCreateForm form = (UserCreateForm) target;
validatePasswords(errors, form);
validateEmail(errors, form);
}
private void validatePasswords(Errors errors, UserCreateForm form) {
if (!form.getPassword().equals(form.getPasswordRepeated())) {
errors.reject("password.no_match", "Passwords do not match");
}
}
private void validateEmail(Errors errors, UserCreateForm form) {
if (userService.getUserByEmail(form.getEmail()).isPresent()) {
errors.reject("email.exists", "User with this email already exists");
}
}
}
UserCreateForm:
public class UserCreateForm {
#NotEmpty
private String email = "";
#NotEmpty
private String password = "";
#NotEmpty
private String passwordRepeated = "";
#NotNull
private Role role = Role.USER;
public String getEmail() {
return email;
}
public String getPassword() {
return password;
}
public String getPasswordRepeated() {
return passwordRepeated;
}
public Role getRole() {
return role;
}
public void setEmail(String email) {
this.email = email;
}
public void setPassword(String password) {
this.password = password;
}
public void setPasswordRepeated(String passwordRepeated) {
this.passwordRepeated = passwordRepeated;
}
public void setRole(Role role) {
this.role = role;
}
}
Test:
#RunWith(SpringRunner.class)
#SpringBootTest
public class UserControllerTest {
private MockMvc mockMvc;
private MediaType contentType = new MediaType(APPLICATION_JSON.getType(),
APPLICATION_JSON.getSubtype(),
Charset.forName("utf8"));
#MockBean
private UserService userService;
#MockBean
private UserCreateFormValidator userCreateFormValidator;
#Autowired
FilterChainProxy springSecurityFilterChain;
#Before
public void setup() {
this.mockMvc = MockMvcBuilders.standaloneSetup(new UserController(userService,userCreateFormValidator)).apply(SecurityMockMvcConfigurers.springSecurity(springSecurityFilterChain)).build();
}
#Test
#WithMockUser(username="user",
password="password",
roles="ADMIN")
public void homePage_authenticatedUser() throws Exception {
mockMvc.perform(get("/user/create"))
.andExpect(status().isOk())
.andExpect(view().name("user_create"));
}
}
I don't know why, because it is a GET method, so it don't have to validate it.
Thanks! :)
You got this exception because you didn't mock the behaviour of public boolean supports(Class<?> clazz) method on your userCreateFormValidator #Mockbean.
If you take a look at the code of org.springframework.validation.DataBinder.assertValidators(DataBinder.java) from the log you posted, you can find there how the validators are processed and how java.lang.IllegalStateException is thrown. In Spring 4.3.8, it looks like this
if(validator != null && this.getTarget() != null && !validator.supports(this.getTarget().getClass())) {
throw new IllegalStateException("Invalid target for Validator [" + validator + "]: " + this.getTarget());
}
You didn't mock supports method of the validator and returns false by default, causing Spring code above throw the IllegalStateException.
TLDR, just give me solution:
You have to mock supports method on your validator. Add following to #Before or #BeforeClass method.
when(requestValidatorMock.supports(any())).thenReturn(true);
I cant comment on the correct answer but his solution worked:
Here is what I had to do for this exact error.
//Imports
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
#MockBean
ApiValidationRouter apiValidationRouter;
#Before
public void beforeClass() throws Exception {
when(apiValidationRouter.supports(any())).thenReturn(true);
}
I have the following classes:
#WebFault(faultBean="com.myapp.FaultBean", name="MyWSException", targetNamespace = "exception.myapp.com")
public class MyWSException extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = 5911956366562576341L;
private FaultBean faultBean;
public MyWSException(String message, FaultBean faultBean) {
super(message);
this.faultBean = faultBean;
}
public MyWSException(String message, Throwable cause, FaultBean faultBean) {
super(message);
this.faultBean = faultBean;
}
public FaultBean getFaultInfo() {
return faultBean;
}
}
And this is my FaultBean:
#XmlType(name="WSFaultType", namespace = "exception.myapp.com")
#XmlRootElement(name="MyWSException")
#XmlAccessorType(XmlAccessType.FIELD)
public final class FaultBean {
#XmlElement(name="message", required=true)
private String message;
#XmlElement(name="code", required=true)
private String code;
public FaultBean(){
super();
}
public FaultBean( String message, String code) {
super();
this.message= message;
this.code = code;
}
public final String getMessage() {
return message;
}
public final void setMessage(String message) {
this.message= message;
}
public final String getCode() {
return code;
}
public final void setCode(String code) {
this.code = code;
}
}
Now I have two AOP classes, one to capture all my custom exceptions, and an additional one to capture Spring's exceptions when committing my transactions, in order to catch all unexpected errors of my persistence layer:
#Aspect
public class SpringExceptionAspect {
#Pointcut("execution(public * *(..))")
private final void anyPublicOperation() {
}
#Pointcut("execution(void org.springframework.transaction.PlatformTransactionManager.commit(*))")
public final void anyTransactionError() {
}
#AfterThrowing(pointcut = "anyPublicOperation() && anyTransactionError()", throwing = "error")
public void catchException(JoinPoint jp, Throwable error) {
FaultBean bean = new FaultBean(error.getMessage(), "ERRCODE");
throw new MyWSException("Persistence error:", bean);
}
}
#Aspect
public class CustomExceptionAspect{
#Pointcut("execution(public * *(..))")
private final void anyPublicOperation() {
}
#Pointcut("within(com.myapp.service..*)")
public final void anyServerMethod() {
}
#AfterThrowing(pointcut = "anyPublicOperation() && anyServerMethod()", throwing = "error")
public void catchException(JoinPoint jp, Throwable error) {
FaultBean bean = new FaultBean(error.getMessage(), "ERRCODE");
throw new MyWSException("Error:", bean);
}
}
Now when Exceptions get caught in the second Aspect (when I throw my custom exceptions), everything works and I get the expected soap:fault response. However when Spring's own Transaction exceptions are caught, I get this response:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault>
<faultcode>soap:Server</faultcode>
<faultstring
Marshalling Error: neither class com.myapp.FaultBean nor any of its superclasses are known to this conext</faultstring>
</soap:Fault>
</soap:Body>
</soap:Envelope>
If they are the same classes, how can this be happening? Why is Jax-b able to marshall those from one point but not from the other one?