com.hubrick.vertx.rest.converter.MultipartHttpMessageConverter.java Source code

Java tutorial

Introduction

Here is the source code for com.hubrick.vertx.rest.converter.MultipartHttpMessageConverter.java

Source

/**
 * Copyright (C) 2015 Etaia AS (oss@hubrick.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.hubrick.vertx.rest.converter;

import com.google.common.base.Charsets;
import com.google.common.collect.Multimap;
import com.hubrick.vertx.rest.HttpInputMessage;
import com.hubrick.vertx.rest.HttpOutputMessage;
import com.hubrick.vertx.rest.MediaType;
import com.hubrick.vertx.rest.converter.model.Part;
import com.hubrick.vertx.rest.exception.HttpMessageConverterException;
import com.hubrick.vertx.rest.message.MultipartHttpOutputMessage;
import io.vertx.core.MultiMap;
import io.vertx.core.http.HttpHeaders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Random;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

/**
 * @author Emir Dizdarevic
 * @since 1.3.0
 */
public class MultipartHttpMessageConverter implements HttpMessageConverter<Multimap<String, Part>> {

    private static final Logger log = LoggerFactory.getLogger(MultipartHttpMessageConverter.class);

    private static final String CONTENT_DISPOSITION = "Content-Disposition";
    private static final byte[] BOUNDARY_CHARS = new byte[] { '-', '_', '1', '2', '3', '4', '5', '6', '7', '8', '9',
            '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
            'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
            'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' };

    private static final Charset charset = Charsets.UTF_8;
    private Charset multipartCharset = Charsets.US_ASCII;
    private final Random random = new Random();

    private final List<MediaType> supportedMediaTypes = new ArrayList<>();
    private List<HttpMessageConverter> partConverters = new ArrayList<>();

    public MultipartHttpMessageConverter() {
        this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);

        final StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
        stringHttpMessageConverter.setWriteAcceptCharset(false);
        this.partConverters.add(new ByteArrayHttpMessageConverter());
        this.partConverters.add(stringHttpMessageConverter);
    }

    /**
     * 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.
     *
     * @param multipartCharset the charset to use
     * @see <a href="http://en.wikipedia.org/wiki/MIME#Encoded-Word">Encoded-Word</a>
     * @since 4.1.1
     */
    public void setMultipartCharset(Charset multipartCharset) {
        checkNotNull(multipartCharset, "multipartCharset must not be null");
        this.multipartCharset = multipartCharset;
    }

    /**
     * Set the message body converters to use. These converters are used to
     * convert objects to MIME parts.
     */
    public void setPartConverters(List<HttpMessageConverter> partConverters) {
        checkNotNull(partConverters, "'partConverters' must not be null");
        checkArgument(!partConverters.isEmpty(), "'partConverters' must not be empty");
        this.partConverters = partConverters;
    }

    /**
     * Add a message body converter. Such a converter is used to convert objects
     * to MIME parts.
     */
    public void addPartConverter(HttpMessageConverter partConverter) {
        checkNotNull(partConverters, "'partConverters' must not be null");
        checkArgument(!partConverters.isEmpty(), "'partConverters' must not be empty");
        this.partConverters.add(partConverter);
    }

    @Override
    public Multimap<String, Part> read(Class<? extends Multimap<String, Part>> clazz,
            HttpInputMessage httpInputMessage) throws HttpMessageConverterException {
        throw new UnsupportedOperationException("The multipart read operation is currently not supported.");
    }

    @Override
    public void write(Multimap<String, Part> object, MediaType contentType, HttpOutputMessage httpOutputMessage)
            throws HttpMessageConverterException {
        try {
            if (isMultipart(object, contentType)) {
                writeMultipart(object, httpOutputMessage);
            } else {
                throw new HttpMessageConverterException(
                        "Unable to write multipart data. Content-Type wrong or Multimap contains wrong data.");
            }
        } catch (IOException e) {
            throw new HttpMessageConverterException(e);
        }
    }

    private boolean isMultipart(Multimap<String, Part> map, MediaType contentType) {
        if (contentType != null) {
            if (!MediaType.MULTIPART_FORM_DATA.includes(contentType)) {
                return false;
            }
        }

        for (Map.Entry<String, Part> value : map.entries()) {
            if (!(value.getValue() instanceof Part)) {
                return false;
            }
        }

        return true;
    }

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

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

    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        if (!Multimap.class.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;
    }

    private void writeMultipart(Multimap<String, Part> parts, HttpOutputMessage httpOutputMessage)
            throws IOException {
        String boundary = generateMultipartBoundary();
        Map<String, String> parameters = Collections.singletonMap("boundary", boundary);

        final MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters);
        httpOutputMessage.getHeaders().set(HttpHeaders.CONTENT_TYPE, contentType.toString());

        writeParts(httpOutputMessage, parts, boundary);
        writeEnd(httpOutputMessage, boundary);
    }

    private void writeParts(HttpOutputMessage httpOutputMessage, Multimap<String, Part> parts, String boundary)
            throws IOException {
        for (Map.Entry<String, Part> entry : parts.entries()) {
            String name = entry.getKey();
            if (entry.getValue() != null) {
                writeBoundary(httpOutputMessage, boundary);
                writePart(name, entry.getValue(), httpOutputMessage);
                writeNewLine(httpOutputMessage);
            }
        }
    }

    private void writeBoundary(HttpOutputMessage httpOutputMessage, String boundary) throws IOException {
        httpOutputMessage.write("--".getBytes());
        httpOutputMessage.write(boundary.getBytes());
        writeNewLine(httpOutputMessage);
    }

    private void writeEnd(HttpOutputMessage httpOutputMessage, String boundary) throws IOException {
        httpOutputMessage.write("--".getBytes());
        httpOutputMessage.write(boundary.getBytes());
        httpOutputMessage.write("--".getBytes());
        writeNewLine(httpOutputMessage);
    }

    private void writeNewLine(HttpOutputMessage httpOutputMessage) throws IOException {
        httpOutputMessage.write("\r\n".getBytes());
    }

    @SuppressWarnings("unchecked")
    private void writePart(String name, Part part, HttpOutputMessage httpOutputMessage) throws IOException {
        final Object partBody = part.getObject();
        final Class<?> partType = partBody.getClass();
        final MultiMap partHeaders = part.getHeaders();
        final String fileName = part.getFileName();

        if (!partHeaders.contains(HttpHeaders.CONTENT_TYPE)) {
            throw new IllegalStateException("Parts headers don't contain Content-Type");
        }

        final String partContentTypeString = partHeaders.get(HttpHeaders.CONTENT_TYPE);
        final MediaType partContentType = MediaType.parseMediaType(partContentTypeString);
        for (HttpMessageConverter messageConverter : this.partConverters) {
            if (messageConverter.canWrite(partType, partContentType)) {
                final MultipartHttpOutputMessage multipartHttpOutputMessage = new MultipartHttpOutputMessage();
                setContentDispositionFormData(name, fileName, multipartHttpOutputMessage.getHeaders());
                if (!partHeaders.isEmpty()) {
                    multipartHttpOutputMessage.putAllHeaders(partHeaders);
                }
                messageConverter.write(partBody, partContentType, multipartHttpOutputMessage);
                httpOutputMessage.write(multipartHttpOutputMessage.getBody());
                return;
            }
        }
        throw new HttpMessageConverterException("Could not write request: no suitable HttpMessageConverter "
                + "found for request type [" + partType.getName() + "]");
    }

    private void setContentDispositionFormData(String name, String filename, MultiMap multiMap) {
        checkNotNull(name, "name must not be null");
        checkArgument(!name.isEmpty(), "name must not be empty");

        final StringBuilder builder = new StringBuilder("form-data; name=\"");
        builder.append(name).append('\"');
        if (filename != null) {
            builder.append("; filename=\"");
            builder.append(filename).append('\"');
        }
        multiMap.set(CONTENT_DISPOSITION, builder.toString());
    }

    /**
     * Generate a multipart boundary.
     * <p>The default implementation returns a random boundary.
     * Can be overridden in subclasses.
     */
    private String generateMultipartBoundary() {
        byte[] boundary = new byte[this.random.nextInt(11) + 30];
        for (int i = 0; i < boundary.length; i++) {
            boundary[i] = BOUNDARY_CHARS[this.random.nextInt(BOUNDARY_CHARS.length)];
        }

        final String constructedBoundry = "----" + new String(boundary, Charsets.US_ASCII);
        return constructedBoundry.toLowerCase();
    }
}