All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.qq275860560.common.encoder.MultipartFormDataEncoder Maven / Gradle / Ivy

There is a newer version: 201905061822
Show newest version
package com.github.qq275860560.common.encoder;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;

import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.codec.Encoder;

/**
 * @author [email protected]
 */

public class MultipartFormDataEncoder implements Encoder {

	private final List> converters = new RestTemplate().getMessageConverters();
	private final HttpHeaders multipartHeaders = new HttpHeaders();
	private final HttpHeaders jsonHeaders = new HttpHeaders();

	public static final Charset UTF_8 = Charset.forName("UTF-8");

	public MultipartFormDataEncoder() {
		multipartHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
		jsonHeaders.setContentType(MediaType.APPLICATION_JSON);
	}

	/**
	 * {@inheritDoc }
	 */
	@Override
	public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
		if (isFormRequest(bodyType)) {
			encodeMultipartFormRequest((Map) object, template);
		} else {
			encodeRequest(object, jsonHeaders, template);
		}
	}

	/**
	 * Encodes the request as a multipart form. It can detect a single
	 * {@link MultipartFile}, an array of {@link MultipartFile}s, or POJOs (that
	 * are converted to JSON).
	 *
	 * @param formMap
	 * @param template
	 * @throws EncodeException
	 */
	private void encodeMultipartFormRequest(Map formMap, RequestTemplate template) throws EncodeException {
		if (formMap == null) {
			throw new EncodeException("Cannot encode request with null form.");
		}
		LinkedMultiValueMap map = new LinkedMultiValueMap<>();
		for (Entry entry : formMap.entrySet()) {
			Object value = entry.getValue();
			if (isMultipartFile(value)) {
				map.add(entry.getKey(), encodeMultipartFile((MultipartFile) value));
			} else if (isMultipartFileArray(value)) {
				encodeMultipartFiles(map, entry.getKey(), Arrays.asList((MultipartFile[]) value));
			} else {
				map.add(entry.getKey(), encodeJsonObject(value));
			}
		}
		encodeRequest(map, multipartHeaders, template);
	}

	private boolean isMultipartFile(Object object) {
		return object instanceof MultipartFile;
	}

	private boolean isMultipartFileArray(Object o) {
		return o != null && o.getClass().isArray()
				&& MultipartFile.class.isAssignableFrom(o.getClass().getComponentType());
	}

	/**
	 * Wraps a single {@link MultipartFile} into a {@link HttpEntity} and sets
	 * the {@code Content-type} header to {@code application/octet-stream}
	 *
	 * @param file
	 * @return
	 */
	private HttpEntity encodeMultipartFile(MultipartFile file) {
		HttpHeaders filePartHeaders = new HttpHeaders();
		filePartHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
		try {
			Resource multipartFileResource = new MultipartFileResource(file.getOriginalFilename(), file.getSize(),
					file.getInputStream());
			return new HttpEntity<>(multipartFileResource, filePartHeaders);
		} catch (IOException ex) {
			throw new EncodeException("Cannot encode request.", ex);
		}
	}

	/**
	 * Fills the request map with {@link HttpEntity}s containing the given
	 * {@link MultipartFile}s. Sets the {@code Content-type} header to
	 * {@code application/octet-stream} for each file.
	 *
	 * @param the
	 *            current request map.
	 * @param name
	 *            the name of the array field in the multipart form.
	 * @param files
	 */
	private void encodeMultipartFiles(LinkedMultiValueMap map, String name,
			List files) {
		HttpHeaders filePartHeaders = new HttpHeaders();
		filePartHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
		try {
			for (MultipartFile file : files) {
				Resource multipartFileResource = new MultipartFileResource(file.getOriginalFilename(), file.getSize(),
						file.getInputStream());
				map.add(name, new HttpEntity<>(multipartFileResource, filePartHeaders));
			}
		} catch (IOException ex) {
			throw new EncodeException("Cannot encode request.", ex);
		}
	}

	/**
	 * Wraps an object into a {@link HttpEntity} and sets the
	 * {@code Content-type} header to {@code application/json}
	 *
	 * @param o
	 * @return
	 */
	private HttpEntity encodeJsonObject(Object o) {
		HttpHeaders jsonPartHeaders = new HttpHeaders();
		jsonPartHeaders.setContentType(MediaType.APPLICATION_JSON);
		return new HttpEntity<>(o, jsonPartHeaders);
	}

	/**
	 * Calls the conversion chain actually used by
	 * {@link org.springframework.web.client.RestTemplate}, filling the body of
	 * the request template.
	 *
	 * @param value
	 * @param requestHeaders
	 * @param template
	 * @throws EncodeException
	 */
	private void encodeRequest(Object value, HttpHeaders requestHeaders, RequestTemplate template)
			throws EncodeException {
		ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
		HttpOutputMessage dummyRequest = new HttpOutputMessageImpl(outputStream, requestHeaders);
		try {
			Class requestType = value.getClass();
			MediaType requestContentType = requestHeaders.getContentType();
			for (HttpMessageConverter messageConverter : converters) {
				if (messageConverter.canWrite(requestType, requestContentType)) {
					((HttpMessageConverter) messageConverter).write(value, requestContentType, dummyRequest);
					break;
				}
			}
		} catch (IOException ex) {
			throw new EncodeException("Cannot encode request.", ex);
		}
		HttpHeaders headers = dummyRequest.getHeaders();
		if (headers != null) {
			for (Entry> entry : headers.entrySet()) {
				template.header(entry.getKey(), entry.getValue());
			}
		}
		/*
		 * we should use a template output stream... this will cause issues if files are
		 * too big, since the whole request will be in memory.
		 */
		template.body(outputStream.toByteArray(), UTF_8);
	}

	/**
	 * Minimal implementation of
	 * {@link org.springframework.http.HttpOutputMessage}. It's needed to
	 * provide the request body output stream to
	 * {@link org.springframework.http.converter.HttpMessageConverter}s
	 */
	private class HttpOutputMessageImpl implements HttpOutputMessage {

		private final OutputStream body;
		private final HttpHeaders headers;

		public HttpOutputMessageImpl(OutputStream body, HttpHeaders headers) {
			this.body = body;
			this.headers = headers;
		}

		@Override
		public OutputStream getBody() throws IOException {
			return body;
		}

		@Override
		public HttpHeaders getHeaders() {
			return headers;
		}

	}

	/**
	 * Heuristic check for multipart requests.
	 *
	 * @param type
	 * @return
	 * @see feign.Types#MAP_STRING_WILDCARD
	 */
	static boolean isFormRequest(Type type) {
		return MAP_STRING_WILDCARD.equals(type);
	}

	/**
	 * Dummy resource class. Wraps file content and its original name.
	 */
	static class MultipartFileResource extends InputStreamResource {

		private final String filename;
		private final long size;

		public MultipartFileResource(String filename, long size, InputStream inputStream) {
			super(inputStream);
			this.size = size;
			this.filename = filename;
		}

		@Override
		public String getFilename() {
			return this.filename;
		}

		@Override
		public InputStream getInputStream() throws IOException, IllegalStateException {
			return super.getInputStream(); // To change body of generated
											// methods, choose Tools |
											// Templates.
		}

		@Override
		public long contentLength() throws IOException {
			return size;
		}

	}

}