FileItemInputIteratorImpl.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.fileupload2.core;

  18. import java.io.IOException;
  19. import java.io.InputStream;
  20. import java.util.Iterator;
  21. import java.util.Locale;
  22. import java.util.NoSuchElementException;
  23. import java.util.Objects;

  24. import org.apache.commons.io.Charsets;
  25. import org.apache.commons.io.IOUtils;
  26. import org.apache.commons.io.input.BoundedInputStream;

  27. /**
  28.  * The iterator returned by {@link AbstractFileUpload#getItemIterator(RequestContext)}.
  29.  */
  30. class FileItemInputIteratorImpl implements FileItemInputIterator {
  31.     /**
  32.      * The file uploads processing utility.
  33.      *
  34.      * @see AbstractFileUpload
  35.      */
  36.     private final AbstractFileUpload<?, ?, ?> fileUpload;

  37.     /**
  38.      * The request context.
  39.      *
  40.      * @see RequestContext
  41.      */
  42.     private final RequestContext requestContext;

  43.     /**
  44.      * The maximum allowed size of a complete request.
  45.      */
  46.     private long sizeMax;

  47.     /**
  48.      * The maximum allowed size of a single uploaded file.
  49.      */
  50.     private long fileSizeMax;

  51.     /**
  52.      * The multi part stream to process.
  53.      */
  54.     private MultipartInput multiPartInput;

  55.     /**
  56.      * The notifier, which used for triggering the {@link ProgressListener}.
  57.      */
  58.     private MultipartInput.ProgressNotifier progressNotifier;

  59.     /**
  60.      * The boundary, which separates the various parts.
  61.      */
  62.     private byte[] multiPartBoundary;

  63.     /**
  64.      * The item, which we currently process.
  65.      */
  66.     private FileItemInputImpl currentItem;

  67.     /**
  68.      * The current items field name.
  69.      */
  70.     private String currentFieldName;

  71.     /**
  72.      * Whether we are currently skipping the preamble.
  73.      */
  74.     private boolean skipPreamble;

  75.     /**
  76.      * Whether the current item may still be read.
  77.      */
  78.     private boolean itemValid;

  79.     /**
  80.      * Whether we have seen the end of the file.
  81.      */
  82.     private boolean eof;

  83.     /**
  84.      * Is the Request of type {@code multipart/related}.
  85.      */
  86.     private final boolean multipartRelated;

  87.     /**
  88.      * Constructs a new instance.
  89.      *
  90.      * @param fileUploadBase Main processor.
  91.      * @param requestContext The request context.
  92.      * @throws FileUploadException An error occurred while parsing the request.
  93.      * @throws IOException         An I/O error occurred.
  94.      */
  95.     FileItemInputIteratorImpl(final AbstractFileUpload<?, ?, ?> fileUploadBase, final RequestContext requestContext) throws FileUploadException, IOException {
  96.         this.fileUpload = fileUploadBase;
  97.         this.sizeMax = fileUploadBase.getSizeMax();
  98.         this.fileSizeMax = fileUploadBase.getFileSizeMax();
  99.         this.requestContext = Objects.requireNonNull(requestContext, "requestContext");
  100.         this.multipartRelated = this.requestContext.isMultipartRelated();
  101.         this.skipPreamble = true;
  102.         findNextItem();
  103.     }

  104.     /**
  105.      * Finds the next item, if any.
  106.      *
  107.      * @return True, if an next item was found, otherwise false.
  108.      * @throws IOException An I/O error occurred.
  109.      */
  110.     private boolean findNextItem() throws FileUploadException, IOException {
  111.         if (eof) {
  112.             return false;
  113.         }
  114.         if (currentItem != null) {
  115.             currentItem.close();
  116.             currentItem = null;
  117.         }
  118.         final var multi = getMultiPartInput();
  119.         for (;;) {
  120.             final boolean nextPart;
  121.             if (skipPreamble) {
  122.                 nextPart = multi.skipPreamble();
  123.             } else {
  124.                 nextPart = multi.readBoundary();
  125.             }
  126.             if (!nextPart) {
  127.                 if (currentFieldName == null) {
  128.                     // Outer multipart terminated -> No more data
  129.                     eof = true;
  130.                     return false;
  131.                 }
  132.                 // Inner multipart terminated -> Return to parsing the outer
  133.                 multi.setBoundary(multiPartBoundary);
  134.                 currentFieldName = null;
  135.                 continue;
  136.             }
  137.             final var headers = fileUpload.getParsedHeaders(multi.readHeaders());
  138.             if (multipartRelated) {
  139.                 currentFieldName = "";
  140.                 currentItem = new FileItemInputImpl(
  141.                         this, null, null, headers.getHeader(AbstractFileUpload.CONTENT_TYPE),
  142.                         false, getContentLength(headers));
  143.                 currentItem.setHeaders(headers);
  144.                 progressNotifier.noteItem();
  145.                 itemValid = true;
  146.                 return true;
  147.             }
  148.             if (currentFieldName == null) {
  149.                 // We're parsing the outer multipart
  150.                 final var fieldName = fileUpload.getFieldName(headers);
  151.                 if (fieldName != null) {
  152.                     final var subContentType = headers.getHeader(AbstractFileUpload.CONTENT_TYPE);
  153.                     if (subContentType != null && subContentType.toLowerCase(Locale.ROOT).startsWith(AbstractFileUpload.MULTIPART_MIXED)) {
  154.                         currentFieldName = fieldName;
  155.                         // Multiple files associated with this field name
  156.                         final var subBoundary = fileUpload.getBoundary(subContentType);
  157.                         multi.setBoundary(subBoundary);
  158.                         skipPreamble = true;
  159.                         continue;
  160.                     }
  161.                     final var fileName = fileUpload.getFileName(headers);
  162.                     currentItem = new FileItemInputImpl(this, fileName, fieldName, headers.getHeader(AbstractFileUpload.CONTENT_TYPE), fileName == null,
  163.                             getContentLength(headers));
  164.                     currentItem.setHeaders(headers);
  165.                     progressNotifier.noteItem();
  166.                     itemValid = true;
  167.                     return true;
  168.                 }
  169.             } else {
  170.                 final var fileName = fileUpload.getFileName(headers);
  171.                 if (fileName != null) {
  172.                     currentItem = new FileItemInputImpl(this, fileName, currentFieldName, headers.getHeader(AbstractFileUpload.CONTENT_TYPE), false,
  173.                             getContentLength(headers));
  174.                     currentItem.setHeaders(headers);
  175.                     progressNotifier.noteItem();
  176.                     itemValid = true;
  177.                     return true;
  178.                 }
  179.             }
  180.             multi.discardBodyData();
  181.         }
  182.     }

  183.     private long getContentLength(final FileItemHeaders headers) {
  184.         try {
  185.             return Long.parseLong(headers.getHeader(AbstractFileUpload.CONTENT_LENGTH));
  186.         } catch (final Exception e) {
  187.             return -1;
  188.         }
  189.     }

  190.     @Override
  191.     public long getFileSizeMax() {
  192.         return fileSizeMax;
  193.     }

  194.     public MultipartInput getMultiPartInput() throws FileUploadException, IOException {
  195.         if (multiPartInput == null) {
  196.             init(fileUpload, requestContext);
  197.         }
  198.         return multiPartInput;
  199.     }

  200.     @Override
  201.     public long getSizeMax() {
  202.         return sizeMax;
  203.     }

  204.     /**
  205.      * Tests whether another instance of {@link FileItemInput} is available.
  206.      *
  207.      * @throws FileUploadException Parsing or processing the file item failed.
  208.      * @throws IOException         Reading the file item failed.
  209.      * @return True, if one or more additional file items are available, otherwise false.
  210.      */
  211.     @Override
  212.     public boolean hasNext() throws IOException {
  213.         if (eof) {
  214.             return false;
  215.         }
  216.         if (itemValid) {
  217.             return true;
  218.         }
  219.         return findNextItem();
  220.     }

  221.     protected void init(final AbstractFileUpload<?, ?, ?> fileUploadBase, final RequestContext initContext) throws FileUploadException, IOException {
  222.         final var contentType = requestContext.getContentType();
  223.         if (null == contentType || !contentType.toLowerCase(Locale.ROOT).startsWith(AbstractFileUpload.MULTIPART)) {
  224.             throw new FileUploadContentTypeException(String.format("the request doesn't contain a %s or %s stream, content type header is %s",
  225.                     AbstractFileUpload.MULTIPART_FORM_DATA, AbstractFileUpload.MULTIPART_MIXED, contentType), contentType);
  226.         }
  227.         final var contentLengthInt = requestContext.getContentLength();
  228.         // @formatter:off
  229.         final var requestSize = RequestContext.class.isAssignableFrom(requestContext.getClass())
  230.                                  // Inline conditional is OK here CHECKSTYLE:OFF
  231.                                  ? requestContext.getContentLength()
  232.                                  : contentLengthInt;
  233.                                  // CHECKSTYLE:ON
  234.         // @formatter:on
  235.         final InputStream inputStream; // This is eventually closed in MultipartInput processing
  236.         if (sizeMax >= 0) {
  237.             if (requestSize != -1 && requestSize > sizeMax) {
  238.                 throw new FileUploadSizeException(
  239.                         String.format("the request was rejected because its size (%s) exceeds the configured maximum (%s)", requestSize, sizeMax), sizeMax,
  240.                         requestSize);
  241.             }
  242.             // This is eventually closed in MultipartInput processing
  243.             // @formatter:off
  244.             inputStream = BoundedInputStream.builder()
  245.                 .setInputStream(requestContext.getInputStream())
  246.                 .setMaxCount(sizeMax)
  247.                 .setOnMaxCount((max, count) -> {
  248.                     throw new FileUploadSizeException(
  249.                         String.format("The request was rejected because its size (%s) exceeds the configured maximum (%s)", count, max), max, count);
  250.                 })
  251.                 .get();
  252.             // @formatter:on
  253.         } else {
  254.             inputStream = requestContext.getInputStream();
  255.         }

  256.         final var charset = Charsets.toCharset(fileUploadBase.getHeaderCharset(), requestContext.getCharset());
  257.         multiPartBoundary = fileUploadBase.getBoundary(contentType);
  258.         if (multiPartBoundary == null) {
  259.             IOUtils.closeQuietly(inputStream); // avoid possible resource leak
  260.             throw new FileUploadException("the request was rejected because no multipart boundary was found");
  261.         }

  262.         progressNotifier = new MultipartInput.ProgressNotifier(fileUploadBase.getProgressListener(), requestSize);
  263.         try {
  264.             multiPartInput = MultipartInput.builder().setInputStream(inputStream).setBoundary(multiPartBoundary).setProgressNotifier(progressNotifier).get();
  265.         } catch (final IllegalArgumentException e) {
  266.             IOUtils.closeQuietly(inputStream); // avoid possible resource leak
  267.             throw new FileUploadContentTypeException(String.format("The boundary specified in the %s header is too long", AbstractFileUpload.CONTENT_TYPE), e);
  268.         }
  269.         multiPartInput.setHeaderCharset(charset);
  270.     }

  271.     /**
  272.      * Returns the next available {@link FileItemInput}.
  273.      *
  274.      * @throws java.util.NoSuchElementException No more items are available. Use {@link #hasNext()} to prevent this exception.
  275.      * @throws FileUploadException              Parsing or processing the file item failed.
  276.      * @throws IOException                      Reading the file item failed.
  277.      * @return FileItemInput instance, which provides access to the next file item.
  278.      */
  279.     @Override
  280.     public FileItemInput next() throws IOException {
  281.         if (eof || !itemValid && !hasNext()) {
  282.             throw new NoSuchElementException();
  283.         }
  284.         itemValid = false;
  285.         return currentItem;
  286.     }

  287.     @Override
  288.     public void setFileSizeMax(final long fileSizeMax) {
  289.         this.fileSizeMax = fileSizeMax;
  290.     }

  291.     @Override
  292.     public void setSizeMax(final long sizeMax) {
  293.         this.sizeMax = sizeMax;
  294.     }

  295.     @Override
  296.     public Iterator<FileItemInput> unwrap() {
  297.         // TODO Something better?
  298.         return (Iterator<FileItemInput>) this;
  299.     }

  300. }