reactor.ipc.netty.http.client.HttpClientFormEncoder.java Source code

Java tutorial

Introduction

Here is the source code for reactor.ipc.netty.http.client.HttpClientFormEncoder.java

Source

/*
 * Copyright (c) 2011-2016 Pivotal Software Inc, All Rights Reserved.
 *
 * 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 reactor.ipc.netty.http.client;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.DecoderResult;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultHttpContent;
import io.netty.handler.codec.http.EmptyHttpHeaders;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.multipart.Attribute;
import io.netty.handler.codec.http.multipart.DiskAttribute;
import io.netty.handler.codec.http.multipart.DiskFileUpload;
import io.netty.handler.codec.http.multipart.FileUpload;
import io.netty.handler.codec.http.multipart.HttpData;
import io.netty.handler.codec.http.multipart.HttpDataFactory;
import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder.EncoderMode;
import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder.ErrorDataEncoderException;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import io.netty.handler.codec.http.multipart.MemoryFileUpload;
import io.netty.handler.stream.ChunkedInput;
import io.netty.util.AbstractReferenceCounted;
import io.netty.util.internal.ThreadLocalRandom;
import reactor.core.Exceptions;
import reactor.core.publisher.DirectProcessor;

import static io.netty.buffer.Unpooled.wrappedBuffer;

/**
 * Modified {@link io.netty.handler.codec.http.multipart.HttpPostRequestEncoder} for
 * optional filename and builder support
 * <p>
 * This encoder will help to encode Request for a FORM as POST.
 */
final class HttpClientFormEncoder implements ChunkedInput<HttpContent>, Runnable, HttpClientRequest.Form {

    /**
     * Factory used to create InterfaceHttpData
     */
    final HttpDataFactory factory;
    /**
     * Request to encode
     */
    final HttpRequest request;
    /**
     * Default charset to use
     */
    final Charset charset;
    /**
     * InterfaceHttpData for Body (without encoding)
     */
    final List<InterfaceHttpData> bodyListDatas;
    /**
     * The final Multipart List of InterfaceHttpData including encoding
     */
    final List<InterfaceHttpData> multipartHttpDatas;
    /**
     * Does this request is a Multipart request
     */
    final boolean isMultipart;
    /**
     * Form mode
     */
    final EncoderMode encoderMode;
    /**
     * Progress flux
     */
    final DirectProcessor<Long> progressFlux;
    /**
     * clean files on terminate
     */
    boolean cleanOnTerminate;
    /**
     * Produce a new encoder (dataFactory changes...)
     */
    boolean needNewEncoder;
    /**
     * Chunked false by default
     */
    boolean isChunked;
    /**
     * If multipart, this is the boundary for the flobal multipart
     */
    String multipartDataBoundary;
    /**
     * If multipart, there could be internal multiparts (mixed) to the global multipart.
     * Only one level is allowed.
     */
    String multipartMixedBoundary;
    /**
     * To check if the header has been finalized
     */
    boolean headerFinalized;
    /**
     * Does the last non empty chunk already encoded so that next chunk will be empty
     * (last chunk)
     */
    boolean isLastChunk;
    /**
     * Last chunk already sent
     */
    boolean isLastChunkSent;
    /**
     * The current FileUpload that is currently in encode process
     */
    FileUpload currentFileUpload;
    /**
     * While adding a FileUpload, is the multipart currently in Mixed Mode
     */
    boolean duringMixedMode;
    /**
     * Global Body size
     */
    long globalBodySize;
    /**
     * Global Transfer progress
     */
    long globalProgress;
    /**
     * Iterator to be used when encoding will be called chunk after chunk
     */
    ListIterator<InterfaceHttpData> iterator;
    /**
     * The ByteBuf currently used by the encoder
     */
    ByteBuf currentBuffer;
    /**
     * The current InterfaceHttpData to encode (used if more chunks are available)
     */
    InterfaceHttpData currentData;
    /**
     * If not multipart, does the currentBuffer stands for the Key or for the Value
     */
    boolean isKey = true;

    Charset newCharset;
    boolean newMultipart;
    EncoderMode newMode;

    /**
     * @param factory the factory used to create InterfaceHttpData
     * @param request the request to encode
     * @param multipart True if the FORM is a ENCTYPE="multipart/form-data"
     * @param charset the charset to use as default
     * @param encoderMode the mode for the encoder to use. See {@link EncoderMode} for the
     * details.
     *
     * @throws NullPointerException      for request or charset or factory
     * @throws ErrorDataEncoderException if the request is not a POST
     */
    HttpClientFormEncoder(HttpDataFactory factory, HttpRequest request, boolean multipart, Charset charset,
            EncoderMode encoderMode) throws ErrorDataEncoderException {
        if (factory == null) {
            throw new NullPointerException("factory");
        }
        if (request == null) {
            throw new NullPointerException("request");
        }
        if (charset == null) {
            throw new NullPointerException("charset");
        }
        HttpMethod method = request.method();
        if (!(method.equals(HttpMethod.POST) || method.equals(HttpMethod.PUT) || method.equals(HttpMethod.PATCH)
                || method.equals(HttpMethod.OPTIONS))) {
            throw new ErrorDataEncoderException("Cannot create a Encoder if not a POST");
        }
        this.request = request;
        this.charset = charset;
        this.newCharset = charset;
        this.factory = factory;
        this.progressFlux = DirectProcessor.create();
        this.cleanOnTerminate = true;
        this.newMode = encoderMode;
        this.newMultipart = multipart;

        // Fill default values
        bodyListDatas = new ArrayList<>();
        // default mode
        isLastChunk = false;
        isLastChunkSent = false;
        isMultipart = multipart;
        multipartHttpDatas = new ArrayList<>();
        this.encoderMode = encoderMode;
        if (isMultipart) {
            initDataMultipart();
        }
    }

    @Override
    public void close() throws Exception {
        // NO since the user can want to reuse (broadcast for instance)
        // cleanFiles();
    }

    @Override
    public boolean isEndOfInput() throws Exception {
        return isLastChunkSent;
    }

    @Override
    public long length() {
        return isMultipart ? globalBodySize : globalBodySize - 1;
    }

    @Override
    public long progress() {
        return globalProgress;
    }

    @Deprecated
    @Override
    public HttpContent readChunk(ChannelHandlerContext ctx) throws Exception {
        return readChunk(ctx.alloc());
    }

    /**
     * Returns the next available HttpChunk. The caller is responsible to test if this
     * chunk is the last one (isLast()), in order to stop calling this getMethod.
     *
     * @return the next available HttpChunk
     *
     * @throws ErrorDataEncoderException if the encoding is in error
     */
    @Override
    public HttpContent readChunk(ByteBufAllocator allocator) throws Exception {
        if (isLastChunkSent) {
            progressFlux.onComplete();
            return null;
        } else {
            HttpContent nextChunk = nextChunk();
            globalProgress += nextChunk.content().readableBytes();
            progressFlux.onNext(progress());
            if (isLastChunkSent) {
                progressFlux.onComplete();
            }
            return nextChunk;
        }
    }

    /**
     * Add a simple attribute in the body as Name=Value
     *
     * @param name name of the parameter
     * @param value the value of the parameter
     *
     * @throws NullPointerException      for name
     * @throws ErrorDataEncoderException if the encoding is in error or if the finalize
     *                                   were already done
     */
    void addBodyAttribute(String name, String value) throws ErrorDataEncoderException {
        if (name == null) {
            throw new NullPointerException("name");
        }
        String svalue = value;
        if (value == null) {
            svalue = "";
        }
        Attribute data = factory.createAttribute(request, name, svalue);
        addBodyHttpData(data);
    }

    /**
     * Add a file as a FileUpload
     *
     * @param name the name of the parameter
     * @param file the file to be uploaded (if not Multipart mode, only the filename will
     * be included)
     * @param contentType the associated contentType for the File
     * @param isText True if this file should be transmitted in Text format (else binary)
     *
     * @throws NullPointerException      for name and file
     * @throws ErrorDataEncoderException if the encoding is in error or if the finalize
     *                                   were already done
     */
    void addBodyFileUpload(String name, File file, String contentType, boolean isText)
            throws ErrorDataEncoderException {
        if (name == null) {
            throw new NullPointerException("name");
        }
        if (file == null) {
            throw new NullPointerException("file");
        }
        String scontentType = contentType;
        String contentTransferEncoding = null;
        if (contentType == null) {
            if (isText) {
                scontentType = DEFAULT_TEXT_CONTENT_TYPE;
            } else {
                scontentType = DEFAULT_BINARY_CONTENT_TYPE;
            }
        }
        if (!isText) {
            contentTransferEncoding = DEFAULT_TRANSFER_ENCODING;
        }
        FileUpload fileUpload = factory.createFileUpload(request, name, file.getName(), scontentType,
                contentTransferEncoding, null, file.length());
        try {
            fileUpload.setContent(file);
        } catch (IOException e) {
            throw new ErrorDataEncoderException(e);
        }
        addBodyHttpData(fileUpload);
    }

    /**
     * Add a series of Files associated with one File parameter
     *
     * @param name the name of the parameter
     * @param file the array of files
     * @param contentType the array of content Types associated with each file
     * @param isText the array of isText attribute (False meaning binary mode) for each
     * file
     *
     * @throws NullPointerException      also throws if array have different sizes
     * @throws ErrorDataEncoderException if the encoding is in error or if the finalize
     *                                   were already done
     */
    void addBodyFileUploads(String name, File[] file, String[] contentType, boolean[] isText)
            throws ErrorDataEncoderException {
        if (file.length != contentType.length && file.length != isText.length) {
            throw new NullPointerException("Different array length");
        }
        for (int i = 0; i < file.length; i++) {
            addBodyFileUpload(name, file[i], contentType[i], isText[i]);
        }
    }

    /**
     * Add the InterfaceHttpData to the Body list
     *
     * @throws NullPointerException      for data
     * @throws ErrorDataEncoderException if the encoding is in error or if the finalize
     *                                   were already done
     */
    void addBodyHttpData(InterfaceHttpData data) throws ErrorDataEncoderException {
        if (headerFinalized) {
            throw new ErrorDataEncoderException("Cannot add value once finalized");
        }
        if (data == null) {
            throw new NullPointerException("data");
        }
        bodyListDatas.add(data);
        if (!isMultipart) {
            if (data instanceof Attribute) {
                Attribute attribute = (Attribute) data;
                try {
                    // name=value& with encoded name and attribute
                    String key = encodeAttribute(attribute.getName(), charset);
                    String value = encodeAttribute(attribute.getValue(), charset);
                    Attribute newattribute = factory.createAttribute(request, key, value);
                    multipartHttpDatas.add(newattribute);
                    globalBodySize += newattribute.getName().length() + 1 + newattribute.length() + 1;
                } catch (IOException e) {
                    throw new ErrorDataEncoderException(e);
                }
            } else if (data instanceof FileUpload) {
                // since not Multipart, only name=filename => Attribute
                FileUpload fileUpload = (FileUpload) data;
                // name=filename& with encoded name and filename
                String key = encodeAttribute(fileUpload.getName(), charset);
                String value = encodeAttribute(fileUpload.getFilename(), charset);
                Attribute newattribute = factory.createAttribute(request, key, value);
                multipartHttpDatas.add(newattribute);
                globalBodySize += newattribute.getName().length() + 1 + newattribute.length() + 1;
            }
            return;
        }
        /*
         * Logic:
           * if not Attribute:
           *      add Data to body list
           *      if (duringMixedMode)
           *          add endmixedmultipart delimiter
           *          currentFileUpload = null
           *          duringMixedMode = false;
           *      add multipart delimiter, multipart body header and Data to multipart list
           *      reset currentFileUpload, duringMixedMode
           * if FileUpload: take care of multiple file for one field => mixed mode
           *      if (duringMixeMode)
           *          if (currentFileUpload.name == data.name)
           *              add mixedmultipart delimiter, mixedmultipart body header and Data to multipart list
           *          else
           *              add endmixedmultipart delimiter, multipart body header and Data to multipart list
           *              currentFileUpload = data
           *              duringMixedMode = false;
           *      else
           *          if (currentFileUpload.name == data.name)
           *              change multipart body header of previous file into multipart list to
           *                      mixedmultipart start, mixedmultipart body header
           *              add mixedmultipart delimiter, mixedmultipart body header and Data to multipart list
           *              duringMixedMode = true
           *          else
           *              add multipart delimiter, multipart body header and Data to multipart list
           *              currentFileUpload = data
           *              duringMixedMode = false;
           * Do not add last delimiter! Could be:
           * if duringmixedmode: endmixedmultipart + endmultipart
           * else only endmultipart
           */
        if (data instanceof Attribute) {
            if (duringMixedMode) {
                InternalAttribute internal = new InternalAttribute(charset);
                internal.addValue("\r\n--" + multipartMixedBoundary + "--");
                multipartHttpDatas.add(internal);
                multipartMixedBoundary = null;
                currentFileUpload = null;
                duringMixedMode = false;
            }
            InternalAttribute internal = new InternalAttribute(charset);
            if (!multipartHttpDatas.isEmpty()) {
                // previously a data field so CRLF
                internal.addValue("\r\n");
            }
            internal.addValue("--" + multipartDataBoundary + "\r\n");
            // content-disposition: form-data; name="field1"
            Attribute attribute = (Attribute) data;
            internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; "
                    + HttpHeaderValues.NAME + "=\"" + attribute.getName() + "\"\r\n");
            // Add Content-Length: xxx
            internal.addValue(HttpHeaderNames.CONTENT_LENGTH + ": " + attribute.length() + "\r\n");
            Charset localcharset = attribute.getCharset();
            if (localcharset != null) {
                // Content-Type: text/plain; charset=charset
                internal.addValue(HttpHeaderNames.CONTENT_TYPE + ": " + DEFAULT_TEXT_CONTENT_TYPE + "; "
                        + HttpHeaderValues.CHARSET + '=' + localcharset.name() + "\r\n");
            }
            // CRLF between body header and data
            internal.addValue("\r\n");
            multipartHttpDatas.add(internal);
            multipartHttpDatas.add(data);
            globalBodySize += attribute.length() + internal.size();
        } else if (data instanceof FileUpload) {
            FileUpload fileUpload = (FileUpload) data;
            InternalAttribute internal = new InternalAttribute(charset);
            if (!multipartHttpDatas.isEmpty()) {
                // previously a data field so CRLF
                internal.addValue("\r\n");
            }
            boolean localMixed;
            if (duringMixedMode) {
                if (currentFileUpload != null && currentFileUpload.getName().equals(fileUpload.getName())) {
                    // continue a mixed mode

                    localMixed = true;
                } else {
                    // end a mixed mode

                    // add endmixedmultipart delimiter, multipart body header
                    // and
                    // Data to multipart list
                    internal.addValue("--" + multipartMixedBoundary + "--");
                    multipartHttpDatas.add(internal);
                    multipartMixedBoundary = null;
                    // start a new one (could be replaced if mixed start again
                    // from here
                    internal = new InternalAttribute(charset);
                    internal.addValue("\r\n");
                    localMixed = false;
                    // new currentFileUpload and no more in Mixed mode
                    currentFileUpload = fileUpload;
                    duringMixedMode = false;
                }
            } else {
                if (encoderMode != EncoderMode.HTML5 && currentFileUpload != null
                        && currentFileUpload.getName().equals(fileUpload.getName())) {
                    // create a new mixed mode (from previous file)

                    // change multipart body header of previous file into
                    // multipart list to
                    // mixedmultipart start, mixedmultipart body header

                    // change Internal (size()-2 position in multipartHttpDatas)
                    // from (line starting with *)
                    // --AaB03x
                    // * Content-Disposition: form-data; name="files";
                    // filename="file1.txt"
                    // Content-Type: text/plain
                    // to (lines starting with *)
                    // --AaB03x
                    // * Content-Disposition: form-data; name="files"
                    // * Content-Type: multipart/mixed; boundary=BbC04y
                    // *
                    // * --BbC04y
                    // * Content-Disposition: attachment; filename="file1.txt"
                    // Content-Type: text/plain
                    initMixedMultipart();
                    InternalAttribute pastAttribute = (InternalAttribute) multipartHttpDatas
                            .get(multipartHttpDatas.size() - 2);
                    // remove past size
                    globalBodySize -= pastAttribute.size();
                    StringBuilder replacement = new StringBuilder(
                            139 + multipartDataBoundary.length() + multipartMixedBoundary.length() * 2
                                    + fileUpload.getFilename().length() + fileUpload.getName().length())

                                            .append("--").append(multipartDataBoundary).append("\r\n")

                                            .append(HttpHeaderNames.CONTENT_DISPOSITION).append(": ")
                                            .append(HttpHeaderValues.FORM_DATA).append("; ")
                                            .append(HttpHeaderValues.NAME).append("=\"")
                                            .append(fileUpload.getName()).append("\"\r\n")

                                            .append(HttpHeaderNames.CONTENT_TYPE).append(": ")
                                            .append(HttpHeaderValues.MULTIPART_MIXED).append("; ")
                                            .append(HttpHeaderValues.BOUNDARY).append('=')
                                            .append(multipartMixedBoundary).append("\r\n\r\n")

                                            .append("--").append(multipartMixedBoundary).append("\r\n")

                                            .append(HttpHeaderNames.CONTENT_DISPOSITION).append(": ")
                                            .append(HttpHeaderValues.ATTACHMENT).append("; ");

                    if (!fileUpload.getFilename().isEmpty()) {
                        replacement.append(HttpHeaderValues.FILENAME).append("=\"")
                                .append(fileUpload.getFilename());
                    }

                    replacement.append("\"\r\n");

                    pastAttribute.setValue(replacement.toString(), 1);
                    pastAttribute.setValue("", 2);

                    // update past size
                    globalBodySize += pastAttribute.size();

                    // now continue
                    // add mixedmultipart delimiter, mixedmultipart body header
                    // and
                    // Data to multipart list
                    localMixed = true;
                    duringMixedMode = true;
                } else {
                    // a simple new multipart
                    // add multipart delimiter, multipart body header and Data
                    // to multipart list
                    localMixed = false;
                    currentFileUpload = fileUpload;
                    duringMixedMode = false;
                }
            }

            if (localMixed) {
                // add mixedmultipart delimiter, mixedmultipart body header and
                // Data to multipart list
                internal.addValue("--" + multipartMixedBoundary + "\r\n");
                // Content-Disposition: attachment; filename="file1.txt"
                if (!fileUpload.getFilename().isEmpty()) {
                    internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.ATTACHMENT
                            + "; " + HttpHeaderValues.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n");
                } else {
                    internal.addValue(
                            HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.ATTACHMENT + ";\r\n");
                }
            } else {
                internal.addValue("--" + multipartDataBoundary + "\r\n");
                // Content-Disposition: form-data; name="files";
                // filename="file1.txt"
                if (!fileUpload.getFilename().isEmpty()) {
                    internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; "
                            + HttpHeaderValues.NAME + "=\"" + fileUpload.getName() + "\"; "
                            + HttpHeaderValues.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n");
                } else {
                    internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; "
                            + HttpHeaderValues.NAME + "=\"" + fileUpload.getName() + "\";\r\n");
                }
            }
            // Add Content-Length: xxx
            internal.addValue(HttpHeaderNames.CONTENT_LENGTH + ": " + fileUpload.length() + "\r\n");
            // Content-Type: image/gif
            // Content-Type: text/plain; charset=ISO-8859-1
            // Content-Transfer-Encoding: binary
            internal.addValue(HttpHeaderNames.CONTENT_TYPE + ": " + fileUpload.getContentType());
            String contentTransferEncoding = fileUpload.getContentTransferEncoding();
            if (contentTransferEncoding != null && contentTransferEncoding.equals(DEFAULT_TRANSFER_ENCODING)) {
                internal.addValue("\r\n" + HttpHeaderNames.CONTENT_TRANSFER_ENCODING + ": "
                        + DEFAULT_BINARY_CONTENT_TYPE + "\r\n\r\n");
            } else if (fileUpload.getCharset() != null) {
                internal.addValue(
                        "; " + HttpHeaderValues.CHARSET + '=' + fileUpload.getCharset().name() + "\r\n\r\n");
            } else {
                internal.addValue("\r\n\r\n");
            }
            multipartHttpDatas.add(internal);
            multipartHttpDatas.add(data);
            globalBodySize += fileUpload.length() + internal.size();
        }
    }

    /**
     * Clean all HttpDatas (on Disk) for the current request.
     */
    void cleanFiles() {
        factory.cleanRequestHttpData(request);
    }

    /**
     * Encode one attribute
     *
     * @return the encoded attribute
     *
     * @throws ErrorDataEncoderException if the encoding is in error
     */
    String encodeAttribute(String s, Charset charset) throws ErrorDataEncoderException {
        if (s == null) {
            return "";
        }
        try {
            String encoded = URLEncoder.encode(s, charset.name());
            if (encoderMode == EncoderMode.RFC3986) {
                for (Map.Entry<Pattern, String> entry : percentEncodings.entrySet()) {
                    String replacement = entry.getValue();
                    encoded = entry.getKey().matcher(encoded).replaceAll(replacement);
                }
            }
            return encoded;
        } catch (UnsupportedEncodingException e) {
            throw new ErrorDataEncoderException(charset.name(), e);
        }
    }

    /**
     * From the current context (currentBuffer and currentData), returns the next
     * HttpChunk (if possible) trying to get sizeleft bytes more into the currentBuffer.
     * This is the Multipart version.
     *
     * @param sizeleft the number of bytes to try to get from currentData
     *
     * @return the next HttpChunk or null if not enough bytes were found
     *
     * @throws ErrorDataEncoderException if the encoding is in error
     */
    HttpContent encodeNextChunkMultipart(int sizeleft) throws ErrorDataEncoderException {
        if (currentData == null) {
            return null;
        }
        ByteBuf buffer;
        if (currentData instanceof InternalAttribute) {
            buffer = ((InternalAttribute) currentData).toByteBuf();
            currentData = null;
        } else {
            if (currentData instanceof Attribute) {
                try {
                    buffer = ((Attribute) currentData).getChunk(sizeleft);
                } catch (IOException e) {
                    throw new ErrorDataEncoderException(e);
                }
            } else {
                try {
                    buffer = ((HttpData) currentData).getChunk(sizeleft);
                } catch (IOException e) {
                    throw new ErrorDataEncoderException(e);
                }
            }
            if (buffer.capacity() == 0) {
                // end for current InterfaceHttpData, need more data
                currentData = null;
                return null;
            }
        }
        if (currentBuffer == null) {
            currentBuffer = buffer;
        } else {
            currentBuffer = wrappedBuffer(currentBuffer, buffer);
        }
        if (currentBuffer.readableBytes() < chunkSize) {
            currentData = null;
            return null;
        }
        buffer = fillByteBuf();
        return new DefaultHttpContent(buffer);
    }

    @Override
    public HttpClientRequest.Form attr(String name, String value) {
        try {
            addBodyAttribute(name, value);
        } catch (ErrorDataEncoderException e) {
            throw Exceptions.propagate(e);
        }
        return this;
    }

    @Override
    public HttpClientRequest.Form charset(Charset charset) {
        this.newCharset = Objects.requireNonNull(charset, "charset");
        this.needNewEncoder = true;
        return this;
    }

    @Override
    public HttpClientRequest.Form cleanOnTerminate(boolean clean) {
        this.cleanOnTerminate = clean;
        return this;
    }

    @Override
    public HttpClientRequest.Form file(String name, File file) {
        file(name, file, null);
        return this;
    }

    @Override
    public HttpClientRequest.Form file(String name, InputStream inputStream) {
        file(name, inputStream, null);
        return this;
    }

    @Override
    public HttpClientRequest.Form file(String name, String filename, File file, String contentType) {
        Objects.requireNonNull(name, "name");
        Objects.requireNonNull(file, "file");
        Objects.requireNonNull(filename, "filename");
        String scontentType = contentType;
        if (contentType == null) {
            scontentType = DEFAULT_BINARY_CONTENT_TYPE;
        }
        FileUpload fileUpload = factory.createFileUpload(request, name, filename, scontentType,
                DEFAULT_TRANSFER_ENCODING, null, file.length());
        try {
            fileUpload.setContent(file);
            addBodyHttpData(fileUpload);
        } catch (ErrorDataEncoderException e) {
            throw Exceptions.propagate(e);
        } catch (IOException e) {
            throw Exceptions.propagate(new ErrorDataEncoderException(e));
        }
        return this;
    }

    @Override
    public HttpClientRequest.Form file(String name, String filename, InputStream stream, String contentType) {
        Objects.requireNonNull(name, "name");
        Objects.requireNonNull(stream, "stream");
        try {
            String scontentType = contentType;
            if (contentType == null) {
                scontentType = DEFAULT_BINARY_CONTENT_TYPE;
            }
            MemoryFileUpload fileUpload = new MemoryFileUpload(name, filename, scontentType,
                    DEFAULT_TRANSFER_ENCODING, charset, -1);
            fileUpload.setMaxSize(-1);
            fileUpload.setContent(stream);
            addBodyHttpData(fileUpload);
        } catch (ErrorDataEncoderException e) {
            throw Exceptions.propagate(e);
        } catch (IOException e) {
            throw Exceptions.propagate(new ErrorDataEncoderException(e));
        }
        return this;
    }

    @Override
    public HttpClientRequest.Form files(String name, File[] files, String[] contentTypes) {
        for (int i = 0; i < files.length; i++) {
            file(name, files[i], contentTypes[i]);
        }
        return this;
    }

    @Override
    public HttpClientRequest.Form files(String name, File[] files, String[] contentTypes, boolean[] textFiles) {
        try {
            addBodyFileUploads(name, files, contentTypes, textFiles);
        } catch (ErrorDataEncoderException e) {
            throw Exceptions.propagate(e);
        }
        return this;
    }

    @Override
    public HttpClientRequest.Form encoding(EncoderMode mode) {
        this.newMode = Objects.requireNonNull(mode, "mode");
        this.needNewEncoder = true;
        return this;
    }

    @Override
    public HttpClientRequest.Form multipart(boolean isMultipart) {
        this.newMultipart = isMultipart;
        this.needNewEncoder = this.isMultipart != isMultipart;
        return this;
    }

    @Override
    public HttpClientRequest.Form textFile(String name, File file) {
        textFile(name, file, null);
        return this;
    }

    @Override
    public HttpClientRequest.Form textFile(String name, File file, String contentType) {
        try {
            addBodyFileUpload(name, file, contentType, true);
        } catch (ErrorDataEncoderException e) {
            throw Exceptions.propagate(e);
        }
        return this;
    }

    @Override
    public HttpClientRequest.Form textFile(String name, InputStream stream) {
        textFile(name, stream, null);
        return this;
    }

    @Override
    public HttpClientRequest.Form textFile(String name, InputStream stream, String contentType) {
        Objects.requireNonNull(name, "name");
        Objects.requireNonNull(stream, "stream");
        try {
            String scontentType = contentType;

            if (contentType == null) {
                scontentType = DEFAULT_TEXT_CONTENT_TYPE;
            }

            MemoryFileUpload fileUpload = new MemoryFileUpload(name, "", scontentType, null, charset, -1);
            fileUpload.setMaxSize(-1);
            fileUpload.setContent(stream);
            addBodyHttpData(fileUpload);
        } catch (ErrorDataEncoderException e) {
            throw Exceptions.propagate(e);
        } catch (IOException e) {
            throw Exceptions.propagate(new ErrorDataEncoderException(e));
        }
        return this;
    }

    @Override
    public void run() {
        cleanFiles();
    }

    final HttpClientFormEncoder applyChanges(HttpRequest request) throws ErrorDataEncoderException {
        if (!needNewEncoder) {
            return this;
        }

        HttpClientFormEncoder encoder = new HttpClientFormEncoder(factory, request, newMultipart, newCharset,
                newMode);

        encoder.setBodyHttpDatas(getBodyListAttributes());

        return encoder;
    }

    /**
     * From the current context (currentBuffer and currentData), returns the next
     * HttpChunk (if possible) trying to get sizeleft bytes more into the currentBuffer.
     * This is the UrlEncoded version.
     *
     * @param sizeleft the number of bytes to try to get from currentData
     *
     * @return the next HttpChunk or null if not enough bytes were found
     *
     * @throws ErrorDataEncoderException if the encoding is in error
     */
    HttpContent encodeNextChunkUrlEncoded(int sizeleft) throws ErrorDataEncoderException {
        if (currentData == null) {
            return null;
        }
        int size = sizeleft;
        ByteBuf buffer;

        // Set name=
        if (isKey) {
            String key = currentData.getName();
            buffer = wrappedBuffer(key.getBytes());
            isKey = false;
            if (currentBuffer == null) {
                currentBuffer = wrappedBuffer(buffer, wrappedBuffer("=".getBytes()));
                // continue
                size -= buffer.readableBytes() + 1;
            } else {
                currentBuffer = wrappedBuffer(currentBuffer, buffer, wrappedBuffer("=".getBytes()));
                // continue
                size -= buffer.readableBytes() + 1;
            }
            if (currentBuffer.readableBytes() >= chunkSize) {
                buffer = fillByteBuf();
                return new DefaultHttpContent(buffer);
            }
        }

        // Put value into buffer
        try {
            buffer = ((HttpData) currentData).getChunk(size);
        } catch (IOException e) {
            throw new ErrorDataEncoderException(e);
        }

        // Figure out delimiter
        ByteBuf delimiter = null;
        if (buffer.readableBytes() < size) {
            isKey = true;
            delimiter = iterator.hasNext() ? wrappedBuffer("&".getBytes()) : null;
        }

        // End for current InterfaceHttpData, need potentially more data
        if (buffer.capacity() == 0) {
            currentData = null;
            if (currentBuffer == null) {
                currentBuffer = delimiter;
            } else {
                if (delimiter != null) {
                    currentBuffer = wrappedBuffer(currentBuffer, delimiter);
                } else {
                    currentBuffer = wrappedBuffer(currentBuffer);
                }
            }
            if (currentBuffer.readableBytes() >= chunkSize) {
                buffer = fillByteBuf();
                return new DefaultHttpContent(buffer);
            }
            return null;
        }

        // Put it all together: name=value&
        if (currentBuffer == null) {
            if (delimiter != null) {
                currentBuffer = wrappedBuffer(buffer, delimiter);
            } else {
                currentBuffer = buffer;
            }
        } else {
            if (delimiter != null) {
                currentBuffer = wrappedBuffer(currentBuffer, buffer, delimiter);
            } else {
                currentBuffer = wrappedBuffer(currentBuffer, buffer);
            }
        }

        // end for current InterfaceHttpData, need more data
        if (currentBuffer.readableBytes() < chunkSize) {
            currentData = null;
            isKey = true;
            return null;
        }

        buffer = fillByteBuf();
        return new DefaultHttpContent(buffer);
    }

    /**
     * @return the next ByteBuf to send as a HttpChunk and modifying currentBuffer
     * accordingly
     */
    ByteBuf fillByteBuf() {
        int length = currentBuffer.readableBytes();
        if (length > chunkSize) {
            ByteBuf slice = currentBuffer.slice(currentBuffer.readerIndex(), chunkSize);
            currentBuffer.skipBytes(chunkSize);
            return slice;
        } else {
            // to continue
            ByteBuf slice = currentBuffer;
            currentBuffer = null;
            return slice;
        }
    }

    /**
     * Finalize the request by preparing the Header in the request and returns the request
     * ready to be sent.<br> Once finalized, no data must be added.<br> If the request
     * does not need chunk (isChunked() == false), this request is the only object to send
     * to the remote server.
     *
     * @return the request object (chunked or not according to size of body)
     *
     * @throws ErrorDataEncoderException if the encoding is in error or if the finalize
     *                                   were already done
     */
    HttpRequest finalizeRequest() throws ErrorDataEncoderException {
        // Finalize the multipartHttpDatas
        if (!headerFinalized) {
            if (isMultipart) {
                InternalAttribute internal = new InternalAttribute(charset);
                if (duringMixedMode) {
                    internal.addValue("\r\n--" + multipartMixedBoundary + "--");
                }
                internal.addValue("\r\n--" + multipartDataBoundary + "--\r\n");
                multipartHttpDatas.add(internal);
                multipartMixedBoundary = null;
                currentFileUpload = null;
                duringMixedMode = false;
                globalBodySize += internal.size();
            }
            headerFinalized = true;
        } else {
            throw new ErrorDataEncoderException("Header already encoded");
        }

        HttpHeaders headers = request.headers();
        List<String> contentTypes = headers.getAll(HttpHeaderNames.CONTENT_TYPE);
        List<String> transferEncoding = headers.getAll(HttpHeaderNames.TRANSFER_ENCODING);
        if (contentTypes != null) {
            headers.remove(HttpHeaderNames.CONTENT_TYPE);
            for (String contentType : contentTypes) {
                // "multipart/form-data; boundary=--89421926422648"
                String lowercased = contentType.toLowerCase();
                if (lowercased.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString())
                        || lowercased.startsWith(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString())) {
                    // ignore
                } else {
                    headers.add(HttpHeaderNames.CONTENT_TYPE, contentType);
                }
            }
        }
        if (isMultipart) {
            String value = HttpHeaderValues.MULTIPART_FORM_DATA + "; " + HttpHeaderValues.BOUNDARY + '='
                    + multipartDataBoundary;
            headers.add(HttpHeaderNames.CONTENT_TYPE, value);
        } else {
            // Not multipart
            headers.add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED);
        }
        // Now consider size for chunk or not
        long realSize = globalBodySize;
        if (isMultipart) {
            iterator = multipartHttpDatas.listIterator();
        } else {
            realSize -= 1; // last '&' removed
            iterator = multipartHttpDatas.listIterator();
        }
        headers.set(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(realSize));
        if (realSize > chunkSize || isMultipart) {
            isChunked = true;
            if (transferEncoding != null) {
                headers.remove(HttpHeaderNames.TRANSFER_ENCODING);
                for (CharSequence v : transferEncoding) {
                    if (HttpHeaderValues.CHUNKED.contentEqualsIgnoreCase(v)) {
                        // ignore
                    } else {
                        headers.add(HttpHeaderNames.TRANSFER_ENCODING, v);
                    }
                }
            }
            HttpUtil.setTransferEncodingChunked(request, true);

            // wrap to hide the possible content
            return new WrappedHttpRequest(request);
        } else {
            // get the only one body and set it to the request
            HttpContent chunk = nextChunk();
            if (request instanceof FullHttpRequest) {
                FullHttpRequest fullRequest = (FullHttpRequest) request;
                ByteBuf chunkContent = chunk.content();
                if (fullRequest.content() != chunkContent) {
                    fullRequest.content().clear().writeBytes(chunkContent);
                    chunkContent.release();
                }
                return fullRequest;
            } else {
                return new WrappedFullHttpRequest(request, chunk);
            }
        }
    }

    /**
     * This getMethod returns a List of all InterfaceHttpData from body part.<br>
     *
     * @return the list of InterfaceHttpData from Body part
     */
    List<InterfaceHttpData> getBodyListAttributes() {
        return bodyListDatas;
    }

    /**
     * Init the delimiter for Global Part (Data).
     */
    void initDataMultipart() {
        multipartDataBoundary = getNewMultipartDelimiter();
    }

    /**
     * Init the delimiter for Mixed Part (Mixed).
     */
    void initMixedMultipart() {
        multipartMixedBoundary = getNewMultipartDelimiter();
    }

    /**
     * @return True if the request is by Chunk
     */
    boolean isChunked() {
        return isChunked;
    }

    /**
     * True if this request is a Multipart request
     *
     * @return True if this request is a Multipart request
     */
    boolean isMultipart() {
        return isMultipart;
    }

    /**
     * Returns the next available HttpChunk. The caller is responsible to test if this
     * chunk is the last one (isLast()), in order to stop calling this getMethod.
     *
     * @return the next available HttpChunk
     *
     * @throws ErrorDataEncoderException if the encoding is in error
     */
    HttpContent nextChunk() throws ErrorDataEncoderException {
        if (isLastChunk) {
            isLastChunkSent = true;
            return LastHttpContent.EMPTY_LAST_CONTENT;
        }
        ByteBuf buffer;
        int size = chunkSize;
        // first test if previous buffer is not empty
        if (currentBuffer != null) {
            size -= currentBuffer.readableBytes();
        }
        if (size <= 0) {
            // NextChunk from buffer
            buffer = fillByteBuf();
            return new DefaultHttpContent(buffer);
        }
        // size > 0
        if (currentData != null) {
            // continue to read data
            if (isMultipart) {
                HttpContent chunk = encodeNextChunkMultipart(size);
                if (chunk != null) {
                    return chunk;
                }
            } else {
                HttpContent chunk = encodeNextChunkUrlEncoded(size);
                if (chunk != null) {
                    // NextChunk Url from currentData
                    return chunk;
                }
            }
            size = chunkSize - currentBuffer.readableBytes();
        }
        if (!iterator.hasNext()) {
            isLastChunk = true;
            // NextChunk as last non empty from buffer
            buffer = currentBuffer;
            currentBuffer = null;
            return new DefaultHttpContent(buffer);
        }
        while (size > 0 && iterator.hasNext()) {
            currentData = iterator.next();
            HttpContent chunk;
            if (isMultipart) {
                chunk = encodeNextChunkMultipart(size);
            } else {
                chunk = encodeNextChunkUrlEncoded(size);
            }
            if (chunk == null) {
                // not enough
                size = chunkSize - currentBuffer.readableBytes();
                continue;
            }
            // NextChunk from data
            return chunk;
        }
        // end since no more data
        isLastChunk = true;
        if (currentBuffer == null) {
            isLastChunkSent = true;
            // LastChunk with no more data
            return LastHttpContent.EMPTY_LAST_CONTENT;
        }
        // Previous LastChunk with no more data
        buffer = currentBuffer;
        currentBuffer = null;
        return new DefaultHttpContent(buffer);
    }

    /**
     * Set the Body HttpDatas list
     *
     * @throws NullPointerException      for datas
     * @throws ErrorDataEncoderException if the encoding is in error or if the finalize
     *                                   were already done
     */
    void setBodyHttpDatas(List<InterfaceHttpData> datas) throws ErrorDataEncoderException {
        if (datas == null) {
            throw new NullPointerException("datas");
        }
        globalBodySize = 0;
        bodyListDatas.clear();
        currentFileUpload = null;
        duringMixedMode = false;
        multipartHttpDatas.clear();
        for (InterfaceHttpData data : datas) {
            addBodyHttpData(data);
        }
    }

    static class WrappedHttpRequest implements HttpRequest {

        final HttpRequest request;

        public WrappedHttpRequest(HttpRequest request) {
            this.request = request;
        }

        @Override
        public DecoderResult decoderResult() {
            return request.decoderResult();
        }

        @Override
        @Deprecated
        public DecoderResult getDecoderResult() {
            return request.getDecoderResult();
        }

        @Override
        public void setDecoderResult(DecoderResult result) {
            request.setDecoderResult(result);
        }

        @Override
        public HttpMethod getMethod() {
            return request.method();
        }

        @Override
        public HttpVersion getProtocolVersion() {
            return request.protocolVersion();
        }

        @Override
        public String getUri() {
            return request.uri();
        }

        @Override
        public HttpHeaders headers() {
            return request.headers();
        }

        @Override
        public HttpMethod method() {
            return request.method();
        }

        @Override
        public HttpVersion protocolVersion() {
            return request.protocolVersion();
        }

        @Override
        public HttpRequest setMethod(HttpMethod method) {
            request.setMethod(method);
            return this;
        }

        @Override
        public HttpRequest setProtocolVersion(HttpVersion version) {
            request.setProtocolVersion(version);
            return this;
        }

        @Override
        public HttpRequest setUri(String uri) {
            request.setUri(uri);
            return this;
        }

        @Override
        public String uri() {
            return request.uri();
        }
    }

    static final class WrappedFullHttpRequest extends WrappedHttpRequest implements FullHttpRequest {

        final HttpContent content;

        WrappedFullHttpRequest(HttpRequest request, HttpContent content) {
            super(request);
            this.content = content;
        }

        @Override
        public ByteBuf content() {
            return content.content();
        }

        @Override
        public FullHttpRequest copy() {
            return replace(content().copy());
        }

        @Override
        public FullHttpRequest duplicate() {
            return replace(content().duplicate());
        }

        @Override
        public int refCnt() {
            return content.refCnt();
        }

        @Override
        public boolean release() {
            return content.release();
        }

        @Override
        public boolean release(int decrement) {
            return content.release(decrement);
        }

        @Override
        public FullHttpRequest replace(ByteBuf content) {
            DefaultFullHttpRequest duplicate = new DefaultFullHttpRequest(protocolVersion(), method(), uri(),
                    content);
            duplicate.headers().set(headers());
            duplicate.trailingHeaders().set(trailingHeaders());
            return duplicate;
        }

        @Override
        public FullHttpRequest retain(int increment) {
            content.retain(increment);
            return this;
        }

        @Override
        public FullHttpRequest retain() {
            content.retain();
            return this;
        }

        @Override
        public FullHttpRequest retainedDuplicate() {
            return replace(content().retainedDuplicate());
        }

        @Override
        public FullHttpRequest setMethod(HttpMethod method) {
            super.setMethod(method);
            return this;
        }

        @Override
        public FullHttpRequest setProtocolVersion(HttpVersion version) {
            super.setProtocolVersion(version);
            return this;
        }

        @Override
        public FullHttpRequest setUri(String uri) {
            super.setUri(uri);
            return this;
        }

        @Override
        public FullHttpRequest touch() {
            content.touch();
            return this;
        }

        @Override
        public FullHttpRequest touch(Object hint) {
            content.touch(hint);
            return this;
        }

        @Override
        public HttpHeaders trailingHeaders() {
            if (content instanceof LastHttpContent) {
                return ((LastHttpContent) content).trailingHeaders();
            } else {
                return EmptyHttpHeaders.INSTANCE;
            }
        }
    }

    /**
     * This Attribute is only for Encoder use to insert special command between object if
     * needed (like Multipart Mixed mode)
     */
    static final class InternalAttribute extends AbstractReferenceCounted implements InterfaceHttpData {

        final List<ByteBuf> value = new ArrayList<>();
        final Charset charset;
        int size;

        InternalAttribute(Charset charset) {
            this.charset = charset;
        }

        @Override
        public int compareTo(InterfaceHttpData o) {
            if (!(o instanceof InternalAttribute)) {
                throw new ClassCastException(
                        "Cannot compare " + getHttpDataType() + " with " + o.getHttpDataType());
            }
            return compareTo((InternalAttribute) o);
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof InternalAttribute)) {
                return false;
            }
            InternalAttribute attribute = (InternalAttribute) o;
            return getName().equalsIgnoreCase(attribute.getName());
        }

        @Override
        public HttpDataType getHttpDataType() {
            return HttpDataType.InternalAttribute;
        }

        @Override
        public String getName() {
            return "InternalAttribute";
        }

        @Override
        public int hashCode() {
            return getName().hashCode();
        }

        @Override
        public InterfaceHttpData retain() {
            for (ByteBuf buf : value) {
                buf.retain();
            }
            return this;
        }

        @Override
        public InterfaceHttpData retain(int increment) {
            for (ByteBuf buf : value) {
                buf.retain(increment);
            }
            return this;
        }

        @Override
        public String toString() {
            StringBuilder result = new StringBuilder();
            for (ByteBuf elt : value) {
                result.append(elt.toString(charset));
            }
            return result.toString();
        }

        @Override
        public InterfaceHttpData touch() {
            for (ByteBuf buf : value) {
                buf.touch();
            }
            return this;
        }

        @Override
        public InterfaceHttpData touch(Object hint) {
            for (ByteBuf buf : value) {
                buf.touch(hint);
            }
            return this;
        }

        @Override
        protected void deallocate() {
            // Do nothing
        }

        void addValue(String value) {
            if (value == null) {
                throw new NullPointerException("value");
            }
            ByteBuf buf = Unpooled.copiedBuffer(value, charset);
            this.value.add(buf);
            size += buf.readableBytes();
        }

        void addValue(String value, int rank) {
            if (value == null) {
                throw new NullPointerException("value");
            }
            ByteBuf buf = Unpooled.copiedBuffer(value, charset);
            this.value.add(rank, buf);
            size += buf.readableBytes();
        }

        int compareTo(InternalAttribute o) {
            return getName().compareToIgnoreCase(o.getName());
        }

        void setValue(String value, int rank) {
            if (value == null) {
                throw new NullPointerException("value");
            }
            ByteBuf buf = Unpooled.copiedBuffer(value, charset);
            ByteBuf old = this.value.set(rank, buf);
            if (old != null) {
                size -= old.readableBytes();
                old.release();
            }
            size += buf.readableBytes();
        }

        int size() {
            return size;
        }

        ByteBuf toByteBuf() {
            return Unpooled.compositeBuffer().addComponents(value).writerIndex(size()).readerIndex(0);
        }
    }

    static final Map<Pattern, String> percentEncodings = new HashMap<>();
    static final String DEFAULT_BINARY_CONTENT_TYPE = "application/octet-stream";
    static final String DEFAULT_TRANSFER_ENCODING = "binary";
    static final String DEFAULT_TEXT_CONTENT_TYPE = "text/plain";
    static final int chunkSize = 8096;

    static {
        DiskFileUpload.deleteOnExitTemporaryFile = true; // should delete file
        // on exit (in normal
        // exit)
        DiskFileUpload.baseDirectory = null; // system temp directory
        DiskAttribute.deleteOnExitTemporaryFile = true; // should delete file on
        // exit (in normal exit)
        DiskAttribute.baseDirectory = null; // system temp directory

        percentEncodings.put(Pattern.compile("\\*"), "%2A");
        percentEncodings.put(Pattern.compile("\\+"), "%20");
        percentEncodings.put(Pattern.compile("%7E"), "~");
    }

    /**
     * @return a newly generated Delimiter (either for DATA or MIXED)
     */
    static String getNewMultipartDelimiter() {
        // construct a generated delimiter
        return Long.toHexString(ThreadLocalRandom.current().nextLong()).toLowerCase();
    }
}