9

I have a java controller which have to send me some text data and different byte arrays. So I am building n multipart request and writing it to stream from HttpServletResponse.

Now my problem is how to parse the response at client side and extract the multiple parts.

SERVER CODE SNIPPET:-

        MultipartEntityBuilder builder = MultipartEntityBuilder.create();
        // Prepare payload
        builder.addBinaryBody("document1", file);
        builder.addBinaryBody("document2", file2);
        builder.addPart("stringData", new StringBody(jsonData, ContentType.TEXT_PLAIN));

        // Set to request body

        HttpEntity entity = builder.build();
        postRequest.setEntity(entity);

CLIENT CODE SNIPPET:-

        HttpPost httpPost = new HttpPost(finalUrl);

        StringEntity entity = new StringEntity(json);
        httpPost.setEntity(entity);
        httpPost.setHeader("Content-type", APPLICATION_JSON_TYPE);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        CloseableHttpResponse response = httpClient.execute(httpPost);
        InputStream in = new BufferedInputStream(response.getEntity().getContent());

I checked CloseableHttpResponse and HttpEntity but none of them is providing method to parse multipart request.

EDIT 1: This is my sample response I am receiving at client side stream:-

--bvRi5oZum37DUldtLgQGSbc5RRVZxKpjZMO4SYDe
Content-Disposition: form-data; name="numeric"
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: 8bit
01010110
--bvRi5oZum37DUldtLgQGSbc5RRVZxKpjZMO4SYDe
Content-Disposition: form-data; name="stringmessage"
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding:8bit
testmessage
--bvRi5oZum37DUldtLgQGSbc5RRVZxKpjZMO4SYDe
Content-Disposition: form-data; name="binarydata"; filename="file1"
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary
HI, THIS IS MY BINARY DATA
--bvRi5oZum37DUldtLgQGSbc5RRVZxKpjZMO4SYDe
Content-Disposition: form-data; name="ending"
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: 8bit
ending
--bvRi5oZum37DUldtLgQGSbc5RRVZxKpjZMO4SYDe--
kleash
  • 1,211
  • 1
  • 12
  • 31
  • I believe this can help you: http://stackoverflow.com/questions/3337056/convenient-way-to-parse-incoming-multipart-form-data-parameters-in-a-servlet – Boschi Mar 01 '17 at 14:18
  • @Boschi Fileupload API methods accept HTTPServletRequest which is not possible in my case as it's a response from the servlet. Please let me know if I am missing some point here. – kleash Mar 02 '17 at 05:27
  • I have added a sample message response received at client.. – kleash Mar 02 '17 at 05:42

3 Answers3

21

I have finally got a workaround for it.

I will be using javax mail MimeMultipart.

Below is a code snipped for the solution:-

    ByteArrayDataSource datasource = new ByteArrayDataSource(in, "multipart/form-data");
    MimeMultipart multipart = new MimeMultipart(datasource);

    int count = multipart.getCount();
    log.debug("count " + count);
    for (int i = 0; i < count; i++) {
        BodyPart bodyPart = multipart.getBodyPart(i);
        if (bodyPart.isMimeType("text/plain")) {
            log.info("text/plain " + bodyPart.getContentType());
            processTextData(bodyPart.getContent());
        } else if (bodyPart.isMimeType("application/octet-stream")) {
            log.info("application/octet-stream " + bodyPart.getContentType());
            processBinaryData(bodyPart.getInputStream()));
        } else {
            log.warn("default " + bodyPart.getContentType());
        }
    }

Please let me know if anybody else have any standard solution.

kleash
  • 1,211
  • 1
  • 12
  • 31
  • 1
    You are a genius man. I was looking for this for 3 days. Its sad to see java has no standard in their Apache libraries for reading multi-part data or that jaxrs has no standard either. this was simple to use and only took three jars. – yfdgvf asdasdas Oct 10 '18 at 15:15
  • @yfdgvfasdasdas Thanks for your kind words :) – kleash Oct 10 '18 at 15:40
  • it is not working when i try to parse the lambda response of multipart/form-data.. :(Is there anyother alternative to it – Ak S Nov 23 '18 at 12:17
  • @AkS I tried it with sending a JSON as string and 2 pdf and it worked fine here.. – kleash Nov 24 '18 at 03:50
  • @LeiYang, Which classes are you talking about. I was not using com.sun.xml package anywhere... – kleash May 07 '19 at 07:04
1

TL;DR

  1. If you know what HttpMessageConverters are, then skip the "In General" part.
  2. If your client is reactive, than you most likely don't have a big problem. Go to the part named "Reactive Client (Spring Webflux)" for details.
  3. If your client is non-reactive i.e. you're using Spring MVC and RestTemplate then the last section is for you. In short, it is not possible out of the box and you need to write custom code.

In General

When we want to read multipart data, then we are at the serialization/marshalling layer of our application. This is basically the same layer as when we transform a JSON or an XML document to a POJO via Jackson for example. What I want to emphasize here is that the logic for parsing multipart data should not take place in a service but rather much earlier.

Our hook to transform multipart data comes as early, as to when an HTTP response enters our application in form of an HttpInputMessage. Out of the box, Spring provides a set of HttpMessageConverters, that are able to transform our HTTP response to an object with which we can work. For example, the MappingJackson2HttpMessageConverter is used to read and write all request/responses that have the MediaType "application/Json".

If the application is reactive, then Spring uses HttpMessageReader and HttpMessageWriter instead of HttpMessageConverters. They save the same purpose.

The following two sections show how to read (download) a multipart response via the two different paradigms.

Reactive Client (Spring Webflux)

This would be the easiest use case and the only thing we need, is already available in Spring Webflux out of the box.

The class MultipartHttpMessageReader would be doing all the heavy lifting. In case it does not behave exactly how you need it to, you can easily extend it and overwrite the methods to your liking. Your custom Reader can then be registered as a bean like so:

@Configuration
public class MultipartMessageConverterConfiguration {

  @Bean
  public CodecCustomizer myCustomMultipartHttpMessageWriter() {
      return configurer -> configurer.customCodecs()
                                     .register(new MyCustomMultipartHttpMessageWriter());
    }
}

Non-Reactive Client (Spring MVC/RestTemplate)

If you have a "classic" application that uses RestTemplate to communicate via HTTP, then you need to rely on the aforementioned HttpMessageConverters. Unfortunately, the MessageConverter that is responsible for reading multipart data, does not support reading/downloading data:

Implementation of HttpMessageConverter to read and write 'normal' HTML forms and also to write (but not read) multipart data (e.g. file uploads)

Source: FormHttpMessageConverter Documentation

So what we need to do, is write our own MessageConverter, which is able to download multipart data. An easy way to do that, would be to make use of the DefaultPartHttpMessageReader that is internally used by MultipartHttpMessageReader. We don't even need Webflux for that, as it is already shipped with spring-web.

First let us define 2 classes in which we save the several parts the we read:

public class MyCustomPart  {

    public MyCustomPart(byte[] content, String filename, MediaType contentType) {
        //assign to corresponding member variables; here omitted
    }
}

/**
 * Basically a container for a list of objects of the class above.
 */
public class MyCustomMultiParts {
    public MyCustomMultiParts(List<MyCustomPart> parts){
        //assign to corresponding member variable; here omitted
    }
}

Later on, you can always take each Part and convert it to whatever is appropriate. The MyCustomPart represents a single block of your multipart data response. The MyCustomMultiParts represent the whole multipart data.

Now we come to the meaty stuff:

public class CustomMultipartHttpMessageConverter implements HttpMessageConverter<MyCustomMultiParts> {
  
    private final List<MediaType> supportedMediaTypes = new ArrayList<>();

    private final DefaultPartHttpMessageReader defaultPartHttpMessageReader;

    public CustomMultipartHttpMessageConverter() {
        this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
        this.defaultPartHttpMessageReader = new DefaultPartHttpMessageReader();
    }

    @Override
    public boolean canRead(final Class<?> clazz, @Nullable final MediaType mediaType) {
        if (!MyCustomMultiParts.class.isAssignableFrom(clazz)) { 
            return false;
        }
        if (mediaType == null) {
            return true;
        }
        for (final MediaType supportedMediaType : getSupportedMediaTypes()) {
            if (supportedMediaType.includes(mediaType) && mediaType.getParameter("boundary") != null) {
                return true;
            }
        }
        return false;
    }

    /**
     * This wraps the input message into a "reactive" input message, that the reactive DefaultPartHttpMessageReader uses.
     */
    private ReactiveHttpInputMessage wrapHttpInputMessage(final HttpInputMessage message) {
        return new ReactiveHttpInputMessage() {
            @Override
            public HttpHeaders getHeaders() {
                return message.getHeaders();
            }

            @SneakyThrows //Part of lombok. Just use a try catch block if you're not using it
            @Override
            public Flux<DataBuffer> getBody() {
                final DefaultDataBuffer wrappedBody = new DefaultDataBufferFactory()
                        .wrap(message.getBody().readAllBytes());
                return Flux.just(wrappedBody);
            }
        };
    }

    @Override
    public MyCustomMultiParts read(@Nullable final Class<? extends MyCustomMultiParts> clazz,
                                           final HttpInputMessage message) throws IOException, HttpMessageNotReadableException {
        final ReactiveHttpInputMessage wrappedMessage = wrapHttpInputMessage(message);
        final ResolvableType resolvableType = ResolvableType.forClass(byte[].class); //plays no role
        List<Part> rawParts = defaultPartHttpMessageReader.read(resolvableType, wrappedMessage, Map.of())//
                .buffer()//
                .blockFirst();
        //You can check here whether the result exists or just continue
        final List<MyCustomPart> customParts = rawParts.stream()// Now we convert to our customPart
                .map(part -> {
                    //Part consists of a DataBuffer, we make a byte[] so we can convert it to whatever we want later
                    final byte[] content = Optional.ofNullable(part.content().blockFirst())//
                            .map(DataBuffer::asByteBuffer)//
                            .map(ByteBuffer::array)//
                            .orElse(new byte[]{});

                    final HttpHeaders headers = part.headers();
                    final String filename = headers.getContentDisposition().getFilename();
                    final MediaType contentType = headers.getContentType();
                    return new MyCustomPart(content, filename, contentType);
                }).collect(Collectors.toList());
        return new MyCustomMultiParts(customParts);
    }

    @Override
    public void write(final MyCustomMultiParts parts, final MediaType contentType,
                      final HttpOutputMessage outputMessage) {
        // we're just interested in reading
        throw new UnsupportedOperationException();
    }


    @Override
    public boolean canWrite(final Class<?> clazz, final MediaType mediaType) {
        // we're just interested in reading
        return false;
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return this.supportedMediaTypes;
    }
}

From here on, you should know better what to do with your "CustomPart". Whether it is a JSON, a bitmap or a PDF. From the byte array you can convert it into anything.

Now if you want to test it, you only have to add your CustomConverter to a RestTemplate and then "await" the MyCustomMultiParts that we defined:

// This could also be inside your @Bean definition of RestTemplate of course
final RestTemplate restTemplate = new RestTemplate();
final List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
messageConverters.add(new CustomMultipartHttpMessageConverter());

String url = "http://server.of.choice:8080/whatever-endpoint-that-sends-multiparts/";
final HttpHeaders headers = new HttpHeaders();
headers.setAccept(List.of(MediaType.MULTIPART_FORM_DATA));
final HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
//here we await our MyCustomMultiParts
final MyCustomMultiParts entity = restTemplate.exchange(url, GET, requestEntity, MyCustomMultiParts.class);
Younes El Ouarti
  • 2,200
  • 2
  • 20
  • 31
  • This is a very interesting answer, but it is Spring Framework specific, whereas the question makes no mention of spring. An answer for this question should stick to Apache HTTP Client's api. – GPI Oct 02 '22 at 15:07
  • Now that you mentioned it, I do see it. Welp, guess I'll still leave it, if it isn't too off topic. – Younes El Ouarti Oct 03 '22 at 10:14
0

Mime4j from Apache is one way to parse the responses from client-side. Its a common practice to use a tool like this.

You can refer this link - http://www.programcreek.com/java-api-examples/index.php?api=org.apache.james.mime4j.MimeException

You can download the jar from this link - http://james.apache.org/download.cgi#Apache_Mime4J

SiKing
  • 10,003
  • 10
  • 39
  • 90
Vijayan Kani
  • 368
  • 3
  • 9
  • I tried with it but it's not able to parse the stream. Most probably because it's expecting in different way. I have added a sample message response received at client – kleash Mar 02 '17 at 05:42