/*
 * Copyright 2012 The Netty Project
 *
 * The Netty Project licenses this file to you 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 io.netty.handler.codec.http.multipart;

import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.http.HttpConstants;
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.HttpRequest;
import io.netty.util.internal.StringUtil;

import java.nio.charset.Charset;
import java.util.List;

This decoder will decode Body and can handle POST BODY. You MUST call destroy() after completion to release all resources.
/** * This decoder will decode Body and can handle POST BODY. * * You <strong>MUST</strong> call {@link #destroy()} after completion to release all resources. * */
public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder { static final int DEFAULT_DISCARD_THRESHOLD = 10 * 1024 * 1024; private final InterfaceHttpPostRequestDecoder decoder;
Params:
  • request – the request to decode
Throws:
/** * * @param request * the request to decode * @throws NullPointerException * for request * @throws ErrorDataDecoderException * if the default charset was wrong when decoding or other * errors */
public HttpPostRequestDecoder(HttpRequest request) { this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, HttpConstants.DEFAULT_CHARSET); }
Params:
  • factory – the factory used to create InterfaceHttpData
  • request – the request to decode
Throws:
/** * * @param factory * the factory used to create InterfaceHttpData * @param request * the request to decode * @throws NullPointerException * for request or factory * @throws ErrorDataDecoderException * if the default charset was wrong when decoding or other * errors */
public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request) { this(factory, request, HttpConstants.DEFAULT_CHARSET); }
Params:
  • factory – the factory used to create InterfaceHttpData
  • request – the request to decode
  • charset – the charset to use as default
Throws:
/** * * @param factory * the factory used to create InterfaceHttpData * @param request * the request to decode * @param charset * the charset to use as default * @throws NullPointerException * for request or charset or factory * @throws ErrorDataDecoderException * if the default charset was wrong when decoding or other * errors */
public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) { if (factory == null) { throw new NullPointerException("factory"); } if (request == null) { throw new NullPointerException("request"); } if (charset == null) { throw new NullPointerException("charset"); } // Fill default values if (isMultipart(request)) { decoder = new HttpPostMultipartRequestDecoder(factory, request, charset); } else { decoder = new HttpPostStandardRequestDecoder(factory, request, charset); } }
states follow NOTSTARTED PREAMBLE ( (HEADERDELIMITER DISPOSITION (FIELD | FILEUPLOAD))* (HEADERDELIMITER DISPOSITION MIXEDPREAMBLE (MIXEDDELIMITER MIXEDDISPOSITION MIXEDFILEUPLOAD)+ MIXEDCLOSEDELIMITER)* CLOSEDELIMITER)+ EPILOGUE First getStatus is: NOSTARTED Content-type: multipart/form-data, boundary=AaB03x => PREAMBLE in Header --AaB03x => HEADERDELIMITER content-disposition: form-data; name="field1" => DISPOSITION Joe Blow => FIELD --AaB03x => HEADERDELIMITER content-disposition: form-data; name="pics" => DISPOSITION Content-type: multipart/mixed, boundary=BbC04y --BbC04y => MIXEDDELIMITER Content-disposition: attachment; filename="file1.txt" => MIXEDDISPOSITION Content-Type: text/plain ... contents of file1.txt ... => MIXEDFILEUPLOAD --BbC04y => MIXEDDELIMITER Content-disposition: file; filename="file2.gif" => MIXEDDISPOSITION Content-type: image/gif Content-Transfer-Encoding: binary ...contents of file2.gif... => MIXEDFILEUPLOAD --BbC04y-- => MIXEDCLOSEDELIMITER --AaB03x-- => CLOSEDELIMITER Once CLOSEDELIMITER is found, last getStatus is EPILOGUE
/** * states follow NOTSTARTED PREAMBLE ( (HEADERDELIMITER DISPOSITION (FIELD | * FILEUPLOAD))* (HEADERDELIMITER DISPOSITION MIXEDPREAMBLE (MIXEDDELIMITER * MIXEDDISPOSITION MIXEDFILEUPLOAD)+ MIXEDCLOSEDELIMITER)* CLOSEDELIMITER)+ * EPILOGUE * * First getStatus is: NOSTARTED * * Content-type: multipart/form-data, boundary=AaB03x => PREAMBLE in Header * * --AaB03x => HEADERDELIMITER content-disposition: form-data; name="field1" * => DISPOSITION * * Joe Blow => FIELD --AaB03x => HEADERDELIMITER content-disposition: * form-data; name="pics" => DISPOSITION Content-type: multipart/mixed, * boundary=BbC04y * * --BbC04y => MIXEDDELIMITER Content-disposition: attachment; * filename="file1.txt" => MIXEDDISPOSITION Content-Type: text/plain * * ... contents of file1.txt ... => MIXEDFILEUPLOAD --BbC04y => * MIXEDDELIMITER Content-disposition: file; filename="file2.gif" => * MIXEDDISPOSITION Content-type: image/gif Content-Transfer-Encoding: * binary * * ...contents of file2.gif... => MIXEDFILEUPLOAD --BbC04y-- => * MIXEDCLOSEDELIMITER --AaB03x-- => CLOSEDELIMITER * * Once CLOSEDELIMITER is found, last getStatus is EPILOGUE */
protected enum MultiPartStatus { NOTSTARTED, PREAMBLE, HEADERDELIMITER, DISPOSITION, FIELD, FILEUPLOAD, MIXEDPREAMBLE, MIXEDDELIMITER, MIXEDDISPOSITION, MIXEDFILEUPLOAD, MIXEDCLOSEDELIMITER, CLOSEDELIMITER, PREEPILOGUE, EPILOGUE }
Check if the given request is a multipart request
Returns:True if the request is a Multipart request
/** * Check if the given request is a multipart request * @return True if the request is a Multipart request */
public static boolean isMultipart(HttpRequest request) { if (request.headers().contains(HttpHeaderNames.CONTENT_TYPE)) { return getMultipartDataBoundary(request.headers().get(HttpHeaderNames.CONTENT_TYPE)) != null; } else { return false; } }
Check from the request ContentType if this request is a Multipart request.
Returns:an array of String if multipartDataBoundary exists with the multipartDataBoundary as first element, charset if any as second (missing if not set), else null
/** * Check from the request ContentType if this request is a Multipart request. * @return an array of String if multipartDataBoundary exists with the multipartDataBoundary * as first element, charset if any as second (missing if not set), else null */
protected static String[] getMultipartDataBoundary(String contentType) { // Check if Post using "multipart/form-data; boundary=--89421926422648 [; charset=xxx]" String[] headerContentType = splitHeaderContentType(contentType); final String multiPartHeader = HttpHeaderValues.MULTIPART_FORM_DATA.toString(); if (headerContentType[0].regionMatches(true, 0, multiPartHeader, 0 , multiPartHeader.length())) { int mrank; int crank; final String boundaryHeader = HttpHeaderValues.BOUNDARY.toString(); if (headerContentType[1].regionMatches(true, 0, boundaryHeader, 0, boundaryHeader.length())) { mrank = 1; crank = 2; } else if (headerContentType[2].regionMatches(true, 0, boundaryHeader, 0, boundaryHeader.length())) { mrank = 2; crank = 1; } else { return null; } String boundary = StringUtil.substringAfter(headerContentType[mrank], '='); if (boundary == null) { throw new ErrorDataDecoderException("Needs a boundary value"); } if (boundary.charAt(0) == '"') { String bound = boundary.trim(); int index = bound.length() - 1; if (bound.charAt(index) == '"') { boundary = bound.substring(1, index); } } final String charsetHeader = HttpHeaderValues.CHARSET.toString(); if (headerContentType[crank].regionMatches(true, 0, charsetHeader, 0, charsetHeader.length())) { String charset = StringUtil.substringAfter(headerContentType[crank], '='); if (charset != null) { return new String[] {"--" + boundary, charset}; } } return new String[] {"--" + boundary}; } return null; } @Override public boolean isMultipart() { return decoder.isMultipart(); } @Override public void setDiscardThreshold(int discardThreshold) { decoder.setDiscardThreshold(discardThreshold); } @Override public int getDiscardThreshold() { return decoder.getDiscardThreshold(); } @Override public List<InterfaceHttpData> getBodyHttpDatas() { return decoder.getBodyHttpDatas(); } @Override public List<InterfaceHttpData> getBodyHttpDatas(String name) { return decoder.getBodyHttpDatas(name); } @Override public InterfaceHttpData getBodyHttpData(String name) { return decoder.getBodyHttpData(name); } @Override public InterfaceHttpPostRequestDecoder offer(HttpContent content) { return decoder.offer(content); } @Override public boolean hasNext() { return decoder.hasNext(); } @Override public InterfaceHttpData next() { return decoder.next(); } @Override public InterfaceHttpData currentPartialHttpData() { return decoder.currentPartialHttpData(); } @Override public void destroy() { decoder.destroy(); } @Override public void cleanFiles() { decoder.cleanFiles(); } @Override public void removeHttpDataFromClean(InterfaceHttpData data) { decoder.removeHttpDataFromClean(data); }
Split the very first line (Content-Type value) in 3 Strings
Returns:the array of 3 Strings
/** * Split the very first line (Content-Type value) in 3 Strings * * @return the array of 3 Strings */
private static String[] splitHeaderContentType(String sb) { int aStart; int aEnd; int bStart; int bEnd; int cStart; int cEnd; aStart = HttpPostBodyUtil.findNonWhitespace(sb, 0); aEnd = sb.indexOf(';'); if (aEnd == -1) { return new String[] { sb, "", "" }; } bStart = HttpPostBodyUtil.findNonWhitespace(sb, aEnd + 1); if (sb.charAt(aEnd - 1) == ' ') { aEnd--; } bEnd = sb.indexOf(';', bStart); if (bEnd == -1) { bEnd = HttpPostBodyUtil.findEndOfString(sb); return new String[] { sb.substring(aStart, aEnd), sb.substring(bStart, bEnd), "" }; } cStart = HttpPostBodyUtil.findNonWhitespace(sb, bEnd + 1); if (sb.charAt(bEnd - 1) == ' ') { bEnd--; } cEnd = HttpPostBodyUtil.findEndOfString(sb); return new String[] { sb.substring(aStart, aEnd), sb.substring(bStart, bEnd), sb.substring(cStart, cEnd) }; }
Exception when try reading data from request in chunked format, and not enough data are available (need more chunks)
/** * Exception when try reading data from request in chunked format, and not * enough data are available (need more chunks) */
public static class NotEnoughDataDecoderException extends DecoderException { private static final long serialVersionUID = -7846841864603865638L; public NotEnoughDataDecoderException() { } public NotEnoughDataDecoderException(String msg) { super(msg); } public NotEnoughDataDecoderException(Throwable cause) { super(cause); } public NotEnoughDataDecoderException(String msg, Throwable cause) { super(msg, cause); } }
Exception when the body is fully decoded, even if there is still data
/** * Exception when the body is fully decoded, even if there is still data */
public static class EndOfDataDecoderException extends DecoderException { private static final long serialVersionUID = 1336267941020800769L; }
Exception when an error occurs while decoding
/** * Exception when an error occurs while decoding */
public static class ErrorDataDecoderException extends DecoderException { private static final long serialVersionUID = 5020247425493164465L; public ErrorDataDecoderException() { } public ErrorDataDecoderException(String msg) { super(msg); } public ErrorDataDecoderException(Throwable cause) { super(cause); } public ErrorDataDecoderException(String msg, Throwable cause) { super(msg, cause); } } }