FileItemInputIteratorImpl.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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 org.apache.commons.fileupload2.core;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.Objects;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.BoundedInputStream;
/**
* The iterator returned by {@link AbstractFileUpload#getItemIterator(RequestContext)}.
*/
class FileItemInputIteratorImpl implements FileItemInputIterator {
/**
* The file uploads processing utility.
*
* @see AbstractFileUpload
*/
private final AbstractFileUpload<?, ?, ?> fileUpload;
/**
* The request context.
*
* @see RequestContext
*/
private final RequestContext requestContext;
/**
* The maximum allowed size of a complete request.
*/
private long sizeMax;
/**
* The maximum allowed size of a single uploaded file.
*/
private long fileSizeMax;
/**
* The multi part stream to process.
*/
private MultipartInput multiPartInput;
/**
* The notifier, which used for triggering the {@link ProgressListener}.
*/
private MultipartInput.ProgressNotifier progressNotifier;
/**
* The boundary, which separates the various parts.
*/
private byte[] multiPartBoundary;
/**
* The item, which we currently process.
*/
private FileItemInputImpl currentItem;
/**
* The current items field name.
*/
private String currentFieldName;
/**
* Whether we are currently skipping the preamble.
*/
private boolean skipPreamble;
/**
* Whether the current item may still be read.
*/
private boolean itemValid;
/**
* Whether we have seen the end of the file.
*/
private boolean eof;
/**
* Is the Request of type {@code multipart/related}.
*/
private final boolean multipartRelated;
/**
* Constructs a new instance.
*
* @param fileUploadBase Main processor.
* @param requestContext The request context.
* @throws FileUploadException An error occurred while parsing the request.
* @throws IOException An I/O error occurred.
*/
FileItemInputIteratorImpl(final AbstractFileUpload<?, ?, ?> fileUploadBase, final RequestContext requestContext) throws FileUploadException, IOException {
this.fileUpload = fileUploadBase;
this.sizeMax = fileUploadBase.getSizeMax();
this.fileSizeMax = fileUploadBase.getFileSizeMax();
this.requestContext = Objects.requireNonNull(requestContext, "requestContext");
this.multipartRelated = this.requestContext.isMultipartRelated();
this.skipPreamble = true;
findNextItem();
}
/**
* Finds the next item, if any.
*
* @return True, if an next item was found, otherwise false.
* @throws IOException An I/O error occurred.
*/
private boolean findNextItem() throws FileUploadException, IOException {
if (eof) {
return false;
}
if (currentItem != null) {
currentItem.close();
currentItem = null;
}
final var multi = getMultiPartInput();
for (;;) {
final boolean nextPart;
if (skipPreamble) {
nextPart = multi.skipPreamble();
} else {
nextPart = multi.readBoundary();
}
if (!nextPart) {
if (currentFieldName == null) {
// Outer multipart terminated -> No more data
eof = true;
return false;
}
// Inner multipart terminated -> Return to parsing the outer
multi.setBoundary(multiPartBoundary);
currentFieldName = null;
continue;
}
final var headers = fileUpload.getParsedHeaders(multi.readHeaders());
if (multipartRelated) {
currentFieldName = "";
currentItem = new FileItemInputImpl(
this, null, null, headers.getHeader(AbstractFileUpload.CONTENT_TYPE),
false, getContentLength(headers));
currentItem.setHeaders(headers);
progressNotifier.noteItem();
itemValid = true;
return true;
}
if (currentFieldName == null) {
// We're parsing the outer multipart
final var fieldName = fileUpload.getFieldName(headers);
if (fieldName != null) {
final var subContentType = headers.getHeader(AbstractFileUpload.CONTENT_TYPE);
if (subContentType != null && subContentType.toLowerCase(Locale.ROOT).startsWith(AbstractFileUpload.MULTIPART_MIXED)) {
currentFieldName = fieldName;
// Multiple files associated with this field name
final var subBoundary = fileUpload.getBoundary(subContentType);
multi.setBoundary(subBoundary);
skipPreamble = true;
continue;
}
final var fileName = fileUpload.getFileName(headers);
currentItem = new FileItemInputImpl(this, fileName, fieldName, headers.getHeader(AbstractFileUpload.CONTENT_TYPE), fileName == null,
getContentLength(headers));
currentItem.setHeaders(headers);
progressNotifier.noteItem();
itemValid = true;
return true;
}
} else {
final var fileName = fileUpload.getFileName(headers);
if (fileName != null) {
currentItem = new FileItemInputImpl(this, fileName, currentFieldName, headers.getHeader(AbstractFileUpload.CONTENT_TYPE), false,
getContentLength(headers));
currentItem.setHeaders(headers);
progressNotifier.noteItem();
itemValid = true;
return true;
}
}
multi.discardBodyData();
}
}
private long getContentLength(final FileItemHeaders headers) {
try {
return Long.parseLong(headers.getHeader(AbstractFileUpload.CONTENT_LENGTH));
} catch (final Exception e) {
return -1;
}
}
@Override
public long getFileSizeMax() {
return fileSizeMax;
}
public MultipartInput getMultiPartInput() throws FileUploadException, IOException {
if (multiPartInput == null) {
init(fileUpload, requestContext);
}
return multiPartInput;
}
@Override
public long getSizeMax() {
return sizeMax;
}
/**
* Tests whether another instance of {@link FileItemInput} is available.
*
* @throws FileUploadException Parsing or processing the file item failed.
* @throws IOException Reading the file item failed.
* @return True, if one or more additional file items are available, otherwise false.
*/
@Override
public boolean hasNext() throws IOException {
if (eof) {
return false;
}
if (itemValid) {
return true;
}
return findNextItem();
}
protected void init(final AbstractFileUpload<?, ?, ?> fileUploadBase, final RequestContext initContext) throws FileUploadException, IOException {
final var contentType = requestContext.getContentType();
if (null == contentType || !contentType.toLowerCase(Locale.ROOT).startsWith(AbstractFileUpload.MULTIPART)) {
throw new FileUploadContentTypeException(String.format("the request doesn't contain a %s or %s stream, content type header is %s",
AbstractFileUpload.MULTIPART_FORM_DATA, AbstractFileUpload.MULTIPART_MIXED, contentType), contentType);
}
final var contentLengthInt = requestContext.getContentLength();
// @formatter:off
final var requestSize = RequestContext.class.isAssignableFrom(requestContext.getClass())
// Inline conditional is OK here CHECKSTYLE:OFF
? requestContext.getContentLength()
: contentLengthInt;
// CHECKSTYLE:ON
// @formatter:on
final InputStream inputStream; // This is eventually closed in MultipartInput processing
if (sizeMax >= 0) {
if (requestSize != -1 && requestSize > sizeMax) {
throw new FileUploadSizeException(
String.format("the request was rejected because its size (%s) exceeds the configured maximum (%s)", requestSize, sizeMax), sizeMax,
requestSize);
}
// This is eventually closed in MultipartInput processing
// @formatter:off
inputStream = BoundedInputStream.builder()
.setInputStream(requestContext.getInputStream())
.setMaxCount(sizeMax)
.setOnMaxCount((max, count) -> {
throw new FileUploadSizeException(
String.format("The request was rejected because its size (%s) exceeds the configured maximum (%s)", count, max), max, count);
})
.get();
// @formatter:on
} else {
inputStream = requestContext.getInputStream();
}
final var charset = Charsets.toCharset(fileUploadBase.getHeaderCharset(), requestContext.getCharset());
multiPartBoundary = fileUploadBase.getBoundary(contentType);
if (multiPartBoundary == null) {
IOUtils.closeQuietly(inputStream); // avoid possible resource leak
throw new FileUploadException("the request was rejected because no multipart boundary was found");
}
progressNotifier = new MultipartInput.ProgressNotifier(fileUploadBase.getProgressListener(), requestSize);
try {
multiPartInput = MultipartInput.builder().setInputStream(inputStream).setBoundary(multiPartBoundary).setProgressNotifier(progressNotifier).get();
} catch (final IllegalArgumentException e) {
IOUtils.closeQuietly(inputStream); // avoid possible resource leak
throw new FileUploadContentTypeException(String.format("The boundary specified in the %s header is too long", AbstractFileUpload.CONTENT_TYPE), e);
}
multiPartInput.setHeaderCharset(charset);
}
/**
* Returns the next available {@link FileItemInput}.
*
* @throws java.util.NoSuchElementException No more items are available. Use {@link #hasNext()} to prevent this exception.
* @throws FileUploadException Parsing or processing the file item failed.
* @throws IOException Reading the file item failed.
* @return FileItemInput instance, which provides access to the next file item.
*/
@Override
public FileItemInput next() throws IOException {
if (eof || !itemValid && !hasNext()) {
throw new NoSuchElementException();
}
itemValid = false;
return currentItem;
}
@Override
public void setFileSizeMax(final long fileSizeMax) {
this.fileSizeMax = fileSizeMax;
}
@Override
public void setSizeMax(final long sizeMax) {
this.sizeMax = sizeMax;
}
@Override
public Iterator<FileItemInput> unwrap() {
// TODO Something better?
return (Iterator<FileItemInput>) this;
}
}