Spring Web: How to transfer many multipart file and JSON to #RestController - spring

I have challenge to create spring controller which consumes array of MultipartFile and JSON.
I've already done controller for JSON and one Multipart file:
Controller:
#PostMapping("/upload")
#ApiOperation("Загрузить новый файл")
#ResponseBody
public Map uploadFile(#RequestPart("file") MultipartFile file,
#RequestPart(required = false) JsonObject json) throws IOException {
UUID fileId = fileService.save(file);
UUID jsond = jsonService.save(json);
return ImmutableMap.of("fileId", fileId, "jsond", jsond );
}
Config file:
#Component
public class JsonConventerConfig extends AbstractJackson2HttpMessageConverter {
protected JsonConventerConfig(ObjectMapper objectMapper) {
super(objectMapper, MediaType.APPLICATION_OCTET_STREAM);
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return false;
}
#Override
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
return false;
}
#Override
protected boolean canWrite(MediaType mediaType) {
return false;
}
}
But I have no idea how to adapt my controller for array with MultipartFile.
Thank you!

The problem is solved by simply adding an array to #RequestPart.
#PostMapping(value = "/file")
#ApiOperation("Загрузить новый файл")
#ResponseBody
public Set<UUID> uploadFile(#RequestPart MultipartFile[] files,
#RequestPart(required = false) Document document) throws IOException
As it turned out swagger does not support multipart requests, so I were getting exception during testing.

Related

How to decompress gzipped content in spring reactive?

While migrating my spring server from servlets to reactive I had to change all the filters in the code to WebFilter. One of the filters was decompressing gzipped content, but I couldn't do the same with the new WebFilter.
With servlets I wrapped the inputstream with a GzipInputStream. What is the best practice to do it with spring reactive?
Solution:
#Component
public class GzipFilter implements WebFilter {
private static final Logger LOG = LoggerFactory.getLogger(GzipFilter.class);
public static final String CONTENT_ENCODING = "content-encoding";
public static final String GZIP = "gzip";
public static final String UTF_8 = "UTF-8";
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
if (!isGzip(request)) {
return chain.filter(exchange);
}
else {
ServerHttpRequest mutatedRequest = new ServerHttpRequestWrapper(request);
ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
return chain.filter(mutatedExchange);
}
}
private boolean isGzip(ServerHttpRequest serverHttpRequest) {
String encoding = serverHttpRequest.getHeaders().getFirst(CONTENT_ENCODING);
return encoding != null && encoding.contains(GZIP);
}
private static class ServerHttpRequestWrapper implements ServerHttpRequest {
private ServerHttpRequest request;
public ServerHttpRequestWrapper(ServerHttpRequest request) {
this.request = request;
}
private static byte[] getDeflatedBytes(GZIPInputStream gzipInputStream) throws IOException {
StringWriter writer = new StringWriter();
IOUtils.copy(gzipInputStream, writer, UTF_8);
return writer.toString().getBytes();
}
#Override
public String getId() {
return request.getId();
}
#Override
public RequestPath getPath() {
return request.getPath();
}
#Override
public MultiValueMap<String, String> getQueryParams() {
return request.getQueryParams();
}
#Override
public MultiValueMap<String, HttpCookie> getCookies() {
return request.getCookies();
}
#Override
public String getMethodValue() {
return request.getMethodValue();
}
#Override
public URI getURI() {
return request.getURI();
}
#Override
public Flux<DataBuffer> getBody() {
Mono<DataBuffer> mono = request.getBody()
.map(dataBuffer -> dataBuffer.asInputStream(true))
.reduce(SequenceInputStream::new)
.map(inputStream -> {
try (GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream)) {
byte[] targetArray = getDeflatedBytes(gzipInputStream);
return new DefaultDataBufferFactory().wrap(targetArray);
}
catch (IOException e) {
throw new IllegalGzipRequest(String.format("failed to decompress gzip content. Path: %s", request.getPath()));
}
});
return mono.flux();
}
#Override
public HttpHeaders getHeaders() {
return request.getHeaders();
}
}
}
love #Yuval's solution!
My original idea was to convert Flux to a local file, and then decompress the local file.
But getting a file downloaded in Spring Reactive is too challenging. I googled a lot, and most of them are blocking way to get file, (e.g. Spring WebClient: How to stream large byte[] to file? and How to correctly read Flux<DataBuffer> and convert it to a single inputStream , none of them works...) which makes no sense and will throw error when calling block() in a reactive flow.
#Yuval saved my day! It works well for me!

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.

How to make json response Unified format in spring?

I want all json response is
{
"status":"ok"
"data":"..."
}
I only care #ResponseBody function return value;Don't need wrap any object to do it;
example:
#ResponseBody
public String test(){
return "Hello,World"
}
I want get
{
"status":"ok"
"data":"Hello,World"
}
ResponseBodyAdvicecan do it.you can implement the interface to handle ReqeustBody. Here is the api.
In the documents.
Allows customizing the response after the execution of an #ResponseBody or a ResponseEntity controller method but before the body is written with an HttpMessageConverter.
Implementations may be registered directly with RequestMappingHandlerAdapter and ExceptionHandlerExceptionResolver or more likely annotated with #ControllerAdvice in which case they will be auto-detected by both.
here is a simple code.
#ControllerAdvice
public customerResponseBody implements ResponseBodyAdvice{
#Override
public boolean supports (MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType){
return true;
}
#Override
public Object beforeBodyWrite (Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response){
body = new ResponseTemplate<Object>("001",body);
return body;
}
}
You must return a Object instead of String for example :
public class CustomResponse {
private String status;
private String data;
// Getters & Setters
}
#ResponseBody
public CustomResponse test(){
CustomResponse response = new CustomResponse();
response.setStatus("OK");
response.setData("Hello,World");
return response;
}
I thought it is not a good solution but you can format string
#RequestMapping(value="testData")
public #ResponseBody String testData(){
String sdata="ok";
String value ="Hello,World";
return "{\"status\" :\""+sdata+"\",\"data \":\""+value+"\"}";
}
I found this solve problem.
rewrite default RequestResponseBodyMethodProcessor can wrap any object to return value

Mapping HTTP request value to object in Sprint Boot application when field and value has different field name?

This instance of class AuthorizationRequest is created during HTTP request, params are sent in query string.
#RequestMapping(value = "/authorize", method = {RequestMethod.GET, RequestMethod.POST})
public String authorize(
#Valid AuthorizationRequest authorizationRequest,
BindingResult result
) {
I would like to use this code, this is an example parameter from AuthorizationRequest class:
#NotEmpty
#JsonProperty("client_id")
private String clientId;
but new instance has a filed clientId empty, because in query string there is a value for this parameter under client_id parameter.
Is there some way how to tell Spring which parameter from HTTP request should use for one particular field of created instance? I need to solve problem with different naming clientId andclient_id`.
What you need is a setter to handle each kind of clientId. Keep in mind that if both clientId and client_id is specified that it is unknown which will take precedence.
//These methods will allow clientId or client_id
to be used as arguments setting the same field this.clientId
public void setClient_id(String client_id) {
this.clientId = client_id;
}
public void setClientId(String client_id) {
this.clientId = client_id;
}
I tested this with a post and a get
get - http://localhost:8080/authorize?clientId=2&username=someusername
get - http://localhost:8080/authorize?client_id=2&username=someusername
post - http://localhost:8080/authorize
Content-Type: application/x-www-form-urlencoded
Body: clientId=2&username=someusername
or Body: client_id=2&username=someusername
I was only able to have #JsonProperty("client_id") to be recognized when I annotated AuthorizationRequest with #RequestBody and then used application/json instead of application/x-www-form-urlencoded
I found the solution with own implementation of org.springframework.web.method.supportHandlerMethodArgumentResolver.
Resolver implementation:
public class AuthorizationRequestResolver implements HandlerMethodArgumentResolver {
private static Logger LOG = Logger.getLogger(AuthorizationRequestResolver.class);
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(AuthorizationRequest.class);
}
#Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
AuthorizationRequest authRequest = mapFromServletRequest(request);
return authRequest;
}
private AuthorizationRequest mapFromServletRequest(HttpServletRequest request) {
AuthorizationRequest authorizationRequest = new AuthorizationRequest();
authorizationRequest.setClientId(request.getParameter("client_id"));
authorizationRequest.setRedirectUri(request.getParameter("request_uri"));
authorizationRequest.setResponseType(request.getParameter("response_type"));
authorizationRequest.setScope(request.getParameter("scope"));
authorizationRequest.setState(request.getParameter("state"));
return authorizationRequest;
}
}
and cofiguration class:
#Configuration
public class WebappConfig extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new AuthorizationRequestResolver());
}
}

Resources