Spring Integration | HTTP outbound gateway | Multipart - spring

Service A receives a file
Service A performs business logic.
Service B exposed an HTTP inbound to receive the file and post it
to S3.
Service A calling Service B using http outbound gateway.
I am getting an error, unable to find suitable message converter when service A calls Service B using http outbound gateway.
<int:header-enricher input-channel="addHeader" output-channel="s3publishWithHeader">`
<int:header name="Content-Type" value="multipart/form-data" overwrite="true"/> </int:header-enricher>`
<util:list id="converters">
<bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter"/>
<bean class="org.springframework.http.converter.FormHttpMessageConverter"/>
</util:list>`
<http:outbound-gateway request-channel="s3publishWithHeader"
http-method="POST" url="http://localhost:8090/com/api/upload"
extract-request-payload="true" message-converters="converters"
></http:outbound-gateway>
Version of Spring Integration: 4.3.12

When using a Resource multipart it seems crucial that this resource has a file name, otherwise I found that the receiving server complains that the part is not present.
The MultiValueMap which will become the multipart request body can contain HttpEntity parts where you can specify the parts more precisely, e.g. you can specify a file name.
Assuming you have two parts, request and file, and file is an InputStreamResource (which never has a file name), you must define the filename by means of the Content-Disposition header. Use the resulting map as outgoing message payload, like so:
MultiValueMap payload = new LinkedMultiValueMap();
// some json part named "request"
HttpHeaders requestPartHeaders = new HttpHeaders();
requestPartHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<ConversionRequest> requestEntity =
new HttpEntity<>(requestBean, requestPartHeaders);
// InputStreamResource part named "file"
HttpHeaders fileRequestHeaders = new HttpHeaders();
fileRequestHeaders.setContentDisposition(ContentDisposition.builder("form-data")
.name("file") // name of the part in the map
.filename("my-file-name.pdf") // <-- mandatory file name
.build());
fileRequestHeaders.setContentType(MediaType.parseMediaType(fileMimeType));
HttpEntity<InputStreamResource> fileEntity =
new HttpEntity<>(new InputStreamResource(fileOriginalStream),
fileRequestHeaders);
payload.add("request", requestEntity);
payload.add("file", fileEntity);

You don't need to specify that Content-Type header and you don't need to configure custom converters.
Only what you need is a payload as Map<String, Object>:
else if (content instanceof Map) {
// We need to check separately for MULTIPART as well as URLENCODED simply because
// MultiValueMap<Object, Object> is actually valid content for serialization
if (this.isFormData((Map<Object, ?>) content)) {
if (this.isMultipart((Map<String, ?>) content)) {
contentType = MediaType.MULTIPART_FORM_DATA;
}
else {
contentType = MediaType.APPLICATION_FORM_URLENCODED;
}
}
}
Nonetheless you have to show the StackTrace for error because I think there is something with response parsing.

Related

Spring Boot / RabbitMQ different content_type with Jackson2JsonMessageConverter

I have a usecase where a service consumes different RabbitMQ queues.
On one of the queues there is JSON encoded data, where the content_type header is set to
application/json
There is a second queue, where binary data is being consumed, content_type is application/octet-stream
In the configuration there is a MessageConverter defined:
#Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
Consuming methods are declared like that:
#RabbitListener(queues = "rpc-device-cmd")
public byte[] rpcCommand(byte[] request) throws IOException, ConfigurationException { .... }
My issue is that Jackson2JsonMessageConverter complains about the application/octet-stream header (
Could not convert incoming message with content-type application/octet-stream 'json' keyword missing.) and even worse, it is encoding the byte[] response of the method above to JSON and base64.
Question:
How is it possible to make sure that the JSON converter does not touch my byte[] responses and ignores messages with a "non-json" content_type?
You achieve this by creating your own MessageConverter implementation to handle the rabbitMQ response string like as shown in the below example
https://github.com/nidhishkrishnan/messaging - here I have demonstrated how we can marshall and convert the RabbitMQ message string to Java domain object based on the binding key using MessageConverter
In your case you can retrieve the content-type from your message to marshal the output as per your requirement
String contentType = message.getMessageProperties().getContentType();

How to determine the request URI in a JAX-RS client's MessageBodyReader?

I am implementing a custom MessageBodyReader that needs to resolve URIs against the request URI during processing. If the MessageBodyReader is used in a JAX-RS server implementation, which eg. processes incoming POST requests, I can obtain the request URI from a UriInfo object that is injected at runtime using #Context (as described eg. at https://stackoverflow.com/a/3314076):
public class MyProvider implements MessageBodyReader {
#javax.ws.rs.core.Context
javax.ws.rs.core.UriInfo uriInfo;
#Override
public Iterable<URI> readFrom(Class<Iterable<URI>> clazz, Type genericType,
Annotation annotations[], MediaType mediaType,
MultivaluedMap httpHeaders, InputStream entityStream)
throws IOException, WebApplicationException {
URI requestURI = uriinfo.getAbsolutePath(); // NPE if called in a client
// Parsing entityStream resolving relative URIs using requestURI
}
}
If the MessageBodyReader is called in a JAX-RS client implementation during the reading of an Response's entity, the injection does not happen, hence I get a NullPointerException (see above for the line) when I write a client using JAX-RS like:
Iterable<URI> it = ClientBuilder.newClient().target("http://example.org/").request().get()
.readEntity(new GenericType<Iterable<URI>>(){});
With the aim of resolving URIs, my question is:
How do I find out the request URI in readFrom(...) in the case that the MessageBodyReader is called in a JAX-RS client implementation?
My current workaround: Having read Chapter 10.4 "Filter and interceptor execution order" of the Jersey documentation, I found that in step 16, the ClientResponseFilter, I have access to both the request URI and the response headers. Hence I wrote a ClientResponseFilter that puts the request URI in a custom response header. Then, I can retrieve that custom header from the 5th parameter of readFrom(...).

How to enable Spring Reactive Web MVC to handle Multipart-file?

I'm trying to use the new reactive web-mvc implementation in a spring boot 2.0 application. I'm trying to define a method which consume multipart file but do not succeed at making it working :( - I always get a 415 error.
On one hand I have a controller containing the following request mapping :
#RequestMapping(method = RequestMethod.POST, path = "/myPath/{param}/{param2}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
#ResponseBody
public Mono<Void> postFile(
#RequestBody MultipartFile data,
#PathVariable("param") String param,
#PathVariable("param2") String param2,
#RequestHeader(name = HEADER_DATE, required = false) #DateTimeFormat(pattern = DATE_FORMAT) Instant instant
){
return fileService.handleData(Mono.just(data), param, param2, instant);
}
On the other hand I had to add a server on the top of the basic dependencies as it seems netty do not handle multipart files. I so added the spring-boot-starter-tomcatdependency which enabled the MultipartAutoConfiguration to be matched and satisfied on application auto configuration.
When posting something using a curl call :
curl 'Meta-Date: 20170101104532' --form "file=#file.bin" http://localhost:8082/myPath/foo/bar
while debug logs are activated (logging.level.org.springframework.web=DEBUG) I got this exception :
org.springframework.web.server.UnsupportedMediaTypeStatusException: Request failure [status: 415, reason: "Content type 'multipart/form-data;boundary=------------------------58fa43b8f1a26de4' not supported"]
This error is thrown by the RequestBodyArgumentResolver which has the the following supported media types : [*/*, text/xml, application/*+json;charset=UTF-8, application/xml, text/plain;charset=UTF-8, application/x-www-form-urlencoded, application/json;charset=UTF-8] provided by 9 DecoderHttpMessageReader.
Before posting I also took a look at :
Spring MultiPart MediaType Unsupported which seems to not be relevant here as my autoconf report contains the following entry : MultipartAutoConfiguration#multipartResolver matched
set content-type to utf-8 with angularjs $http Adding a header setting Content-Transfer-Encoding: binary didn't changed anything.
My understanding is that Spring web 5.0 uses a new request decoder system as I don't find these classes on a spring 4 spring boot application, and there is not yet any DecoderHttpMessageReader dealing with multipart file
Did I miss something ? Or should I wait one to be implemented ?
Okay, It seems this is just not implemented for now as it currently exists a pull request for this feature : Add reactive multipart request support #1201
Should have check this earlier...
[EDIT] : The issue has been solved and merged into Spring master branch. Should no longer be an issue.
#PutMapping(value="/{..}",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono<Void> save(#RequestPart("file") FilePart multipartFormData,#RequestParam("fileName") String fileName,#PathVariable("..") String ..) throws IOException {
List<ByteBuffer> bytesList = new LinkedList<>();
multipartFormData.content().
subscribe(item->bytesList.add(item.asByteBuffer()));
int totalBytes = bytesList.stream().mapToInt(item->item.capacity()).sum();
ByteBuffer buffer = ByteBuffer.allocate(totalBytes);
bytesList.stream().forEach(byteBuff->buffer.put(byteBuff));
baseImageHandler.saveImage(buffer, fileName, baseItemId);
return Mono.empty();
}
Please note that it is a dev verison, but this is how I have managed to do it.

Set spring integration header into soap header in spring integration (without interceptor/ threadlocal)

Is there anyway to set SOAP-ENV header in spring integration xml from spring integration headers ?
I tried below approach and expected bId to be populated to soap headers
<int:chain id="soapcall" input-channel="soapchannel">
<int-xml:header-enricher>
<int-xml:header name="bId" expression="headers['bId']"/>
</int-xml:header-enricher>
<int-ws:outbound-gateway uri="${soap.url}" interceptor="myInterceptor">
</int-ws:outbound-gateway>
</int:chain>
However Actual output doesn't have the header. see below:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:n1="http://namespace/n1">
<SOAP-ENV:Header/>
<SOAP-ENV:Body> ...
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
I also looked into below approach inside chain before calling outbound gateway. It seems this header enricher takes only soap action as header. So this throws exception action headers not found
<int-ws:header-enricher >
<int-ws:soap-action expression="headers['bId']"/>
</int-ws:header-enricher>
I also looked into this post. But I couldn't get this compiled inside a chain.
I'm not sure what is the <int-xml:header-enricher>, but it doesn't matter.
What you need is called header-mapper on the <int-ws:outbound-gateway>.
Its code looks like:
#Override
protected void populateStandardHeaders(Map<String, Object> headers, SoapMessage target) {
String soapAction = getHeaderIfAvailable(headers, WebServiceHeaders.SOAP_ACTION, String.class);
if (!StringUtils.hasText(soapAction)) {
soapAction = "\"\"";
}
target.setSoapAction(soapAction);
}
#Override
protected void populateUserDefinedHeader(String headerName, Object headerValue, SoapMessage target) {
SoapHeader soapHeader = target.getSoapHeader();
if (headerValue instanceof String) {
QName qname = QNameUtils.parseQNameString(headerName);
soapHeader.addAttribute(qname, (String) headerValue);
}
}
So, by default it maps WebServiceHeaders.SOAP_ACTION as a standard one, and all those user defined if they are String and only as attribute of the target SOAP-ENV:Header element.
Those user defined headers can be mapped by the:
<xsd:attribute name="mapped-request-headers" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Comma-separated list of names of SOAP Headers to be mapped from the SOAP request into the MessageHeaders.
This can only be provided if the 'header-mapper' reference is not being set directly. The values in
this list can also be simple patterns to be matched against the header names (e.g. "foo*" or "*foo").
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
If you need to populate sub-elements to the <SOAP-ENV:Header>, you don't have choice unless extend DefaultSoapHeaderMapper and override that populateUserDefinedHeader() and use SoapHeader.addHeaderElement() API.
Also look into the Reference Manual.

Spring MVC with Spring Integration HTTP: how to pass the queryString avoiding the "?" encoding with "%3F" ?

I have two Spring MVC applications interconneted between each others with Spring Integration HTTP.
I have an "application1" and an "application2".
The application1 receive this HTTP GET request:
http://localhost:8080/application1/api/persons/search/findByName?name=Name
The application1 manage the request with this #Controller:
#Controller
public class ApplicationController {
#RequestMapping(value = "/api/{repository}/search/{methodName}", method = RequestMethod.GET)
public void search(#PathVariable(value="repository") String repository,
#PathVariable(value="methodName") String methodName,
ModelMap model,
HttpServletRequest request,
HttpServletResponse response) {
// handling the request ...
}
}
Here is what I can see in the request properties:
getRequestURI=/application1/api/persons/search/findByName
getRequestedSessionId=null
getContextPath=/application1
getPathTranslated=null
getAuthType=null
getMethod=GET
getQueryString=name=Name
getServletPath=/api/persons/search/findByName
getPathInfo=null
getRemoteUser=null
I want to "transfer" this request to the application2 using Spring Integration HTTP with an <int-http:outbound-gateway>.
The message for the channel used by the outbound-gateway is originated in the #Controller in this way:
MessagingChannel messagingChannel = (MessagingChannel)appContext.getBean("requestChannelBean");
String payload = repository+"/search/"+methodName;
Message<String> message = MessageBuilder.withPayload(payload).build();
MessageChannel requestChannel = messagingChannel.getRequestChannel();
MessagingTemplate messagingTemplate = new MessagingTemplate();
Message<?> response = messagingTemplate.sendAndReceive(requestChannel, message);
This is the <int-http:outbound-gateway> configuration:
<int-http:outbound-gateway id="gateway" rest-template="restTemplate"
url="http://localhost:8080/application2/api/service/{pathToCall}"
http-method="POST" header-mapper="headerMapper" extract-request-payload="true"
expected-response-type="java.lang.String">
<int-http:uri-variable name="pathToCall" expression="payload"/>
</int-http:outbound-gateway>
This gateway produces an HTTP POST request towards the url:
http://localhost:8080/application2/api/service/persons/search/findByName
But in this request I lose the original QueryString received by the application1.
I have tried to add the queryString directly to the payload as follows:
String queryString = "";
if (request.getQueryString()!=null)
queryString = request.getQueryString();
String payload = repository+"/search/"+methodName+"?"+queryString;
But this doesn't work: the produced url is:
http://localhost:8080/application2/api/service/persons/search/findByName%3Fname=Name
The "?" symbol is replaced by "%3F", so the called method is "/service/persons/search/findByName%3Fname=Name", instead of "/service/persons/search/findByName"
I suppose that it depends on the http-method="POST"; I want to use the POST method in any case, because I want to use this "service" for general requests.
So what I have to do in order to transfer the queryString of the original request to the other side in the simplest way as possible?
Thanks in advance.
I have found the answer.
The encoding of the "?" into "%3F" was made by the <int-http:outbound-gateway> beacuse the encode-uri="false" attribute was missing.
The outbound gateway encodes the URI by default, because the encode-uri attribute is setted to true by default.

Resources