Adds missing JavaDocs

This commit is contained in:
Lukasz Lenart
2025-07-19 09:48:22 +02:00
parent 359b6549ef
commit 63f2c8bdec
2 changed files with 260 additions and 49 deletions
@@ -38,6 +38,37 @@ import static org.apache.commons.lang3.StringUtils.normalizeSpace;
/**
* Multipart form data request adapter for Jakarta Commons FileUpload package.
*
* <p>This implementation provides secure handling of multipart requests with proper
* resource management and cleanup. It tracks all temporary files created during
* the upload process and ensures they are properly cleaned up to prevent
* resource leaks and security vulnerabilities.</p>
*
* <p>Key features:</p>
* <ul>
* <li>Automatic tracking and cleanup of temporary files</li>
* <li>Proper error handling with user-friendly error messages</li>
* <li>Support for both in-memory and disk-based file uploads</li>
* <li>Extensible cleanup mechanisms for customization</li>
* </ul>
*
* <p>Usage example:</p>
* <pre>
* JakartaMultiPartRequest multipartRequest = new JakartaMultiPartRequest();
* try {
* multipartRequest.parse(request, "/tmp/uploads");
* // Process uploaded files
* for (String fieldName : multipartRequest.getFileParameterNames()) {
* List&lt;UploadedFile&gt; files = multipartRequest.getFile(fieldName);
* // Handle files
* }
* } finally {
* multipartRequest.cleanUp(); // Always clean up resources
* }
* </pre>
*
* @see AbstractMultiPartRequest
* @see org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload
*/
public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
@@ -53,6 +84,27 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
*/
private final List<File> temporaryFiles = new ArrayList<>();
/**
* Processes the multipart upload request using Jakarta Commons FileUpload.
*
* <p>This method handles the core upload processing by:</p>
* <ol>
* <li>Reading the character encoding from the request</li>
* <li>Preparing the Jakarta servlet file upload handler</li>
* <li>Creating a request context for processing</li>
* <li>Iterating through all form items (fields and files)</li>
* <li>Processing each item appropriately based on its type</li>
* </ol>
*
* <p>All {@link org.apache.commons.fileupload2.core.DiskFileItem} instances
* are automatically tracked for proper cleanup.</p>
*
* @param request the HTTP servlet request containing the multipart data
* @param saveDir the directory where uploaded files will be stored
* @throws IOException if an error occurs during upload processing
* @see #processNormalFormField(DiskFileItem, Charset)
* @see #processFileField(DiskFileItem)
*/
@Override
protected void processUpload(HttpServletRequest request, String saveDir) throws IOException {
Charset charset = readCharsetEncoding(request);
@@ -63,29 +115,51 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
RequestContext requestContext = createRequestContext(request);
for (DiskFileItem item : servletFileUpload.parseRequest(requestContext)) {
// Track all DiskFileItem instances for cleanup
// Track all DiskFileItem instances for cleanup - this is critical for security
// as it ensures temporary files are properly cleaned up even if processing fails
diskFileItems.add(item);
LOG.debug(() -> "Processing a form field: " + normalizeSpace(item.getFieldName()));
if (item.isFormField()) {
// Process regular form fields (text inputs, checkboxes, etc.)
processNormalFormField(item, charset);
} else {
// Process file upload fields
LOG.debug(() -> "Processing a file: " + normalizeSpace(item.getFieldName()));
processFileField(item);
}
}
}
/**
* Processes a normal form field (non-file) from the multipart request.
*
* <p>This method handles text form fields by:</p>
* <ol>
* <li>Validating the field name is not null</li>
* <li>Extracting the field value using the specified charset</li>
* <li>Checking if the field value exceeds maximum string length</li>
* <li>Adding the value to the parameters map</li>
* </ol>
*
* <p>Fields with null names are skipped with a warning log message.</p>
* <p>Empty form fields are stored as empty strings.</p>
*
* @param item the disk file item representing the form field
* @param charset the character set to use for decoding the field value
* @throws IOException if an error occurs reading the field value
* @see #exceedsMaxStringLength(String, String)
*/
protected void processNormalFormField(DiskFileItem item, Charset charset) throws IOException {
LOG.debug("Item: {} is a normal form field", normalizeSpace(item.getName()));
List<String> values;
String fieldName = item.getFieldName();
if (parameters.get(fieldName) != null) {
values = parameters.get(fieldName);
} else {
values = new ArrayList<>();
if (fieldName == null) {
LOG.warn("Form field has null fieldName, skipping");
return;
}
List<String> values = parameters.computeIfAbsent(fieldName, k -> new ArrayList<>());
String fieldValue = item.getString(charset);
if (exceedsMaxStringLength(fieldName, fieldValue)) {
@@ -99,6 +173,26 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
parameters.put(fieldName, values);
}
/**
* Processes a file field from the multipart request.
*
* <p>This method handles file uploads by:</p>
* <ol>
* <li>Validating the file name and field name are not null/empty</li>
* <li>Determining if the file is stored in memory or on disk</li>
* <li>For in-memory files: creating a temporary file and copying content</li>
* <li>For disk files: using the existing file directly</li>
* <li>Creating an {@link UploadedFile} abstraction</li>
* <li>Adding the file to the uploaded files collection</li>
* </ol>
*
* <p>Temporary files created for in-memory uploads are automatically
* tracked for cleanup. Any errors during temporary file creation are
* logged and added to the error list for user feedback.</p>
*
* @param item the disk file item representing the uploaded file
* @see #cleanUpTemporaryFiles()
*/
protected void processFileField(DiskFileItem item) {
// Skip file uploads that don't have a file name - meaning that no file was selected.
if (item.getName() == null || item.getName().trim().isEmpty()) {
@@ -106,18 +200,18 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
return;
}
List<UploadedFile> values;
if (uploadedFiles.get(item.getFieldName()) != null) {
values = uploadedFiles.get(item.getFieldName());
} else {
values = new ArrayList<>();
String fieldName = item.getFieldName();
if (fieldName == null) {
LOG.warn("File field has null fieldName, skipping");
return;
}
List<UploadedFile> values = uploadedFiles.computeIfAbsent(fieldName, k -> new ArrayList<>());
if (item.isInMemory()) {
LOG.debug("Creating temporary file representing in-memory uploaded item: {}", normalizeSpace(item.getFieldName()));
try {
File tempFile = File.createTempFile("struts_upload_", "_" + item.getName());
tempFile.deleteOnExit(); // Ensure cleanup on JVM exit as fallback
// Track the temporary file for explicit cleanup
temporaryFiles.add(tempFile);
@@ -157,12 +251,26 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
values.add(uploadedFile);
}
uploadedFiles.put(item.getFieldName(), values);
uploadedFiles.put(fieldName, values);
}
/**
* Cleans up disk file items by deleting associated temporary files.
* This method can be overridden by subclasses to customize cleanup behavior.
*
* <p>This method iterates through all tracked {@link DiskFileItem} instances
* and performs cleanup operations:</p>
* <ul>
* <li>For in-memory items: logs cleanup (no files to delete)</li>
* <li>For disk items: deletes the associated temporary file</li>
* </ul>
*
* <p>This method is called automatically during {@link #cleanUp()} but can
* be overridden by subclasses to customize cleanup behavior. All exceptions
* are caught and logged to prevent cleanup failures from affecting the
* overall cleanup process.</p>
*
* @see #cleanUp()
* @see #cleanUpTemporaryFiles()
*/
protected void cleanUpDiskFileItems() {
LOG.debug("Clean up all DiskFileItem instances (both form fields and file uploads");
@@ -171,10 +279,12 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
if (item.isInMemory()) {
LOG.debug("Cleaning up in-memory item: {}", normalizeSpace(item.getFieldName()));
} else {
LOG.debug("Cleaning up disk item: {} at {}", normalizeSpace(item.getFieldName()), item.getPath());
if (item.getPath() != null && item.getPath().toFile().exists()) {
if (!item.getPath().toFile().delete()) {
LOG.warn("There was a problem attempting to delete temporary file: {}", item.getPath());
Path itemPath = item.getPath();
LOG.debug("Cleaning up disk item: {} at {}", normalizeSpace(item.getFieldName()), itemPath);
if (itemPath != null) {
File itemFile = itemPath.toFile();
if (itemFile.exists() && !itemFile.delete()) {
LOG.warn("There was a problem attempting to delete temporary file: {}", itemPath);
}
}
}
@@ -186,7 +296,26 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
/**
* Cleans up temporary files created for in-memory uploads.
* This method can be overridden by subclasses to customize cleanup behavior.
*
* <p>This method deletes all temporary files that were created when
* processing in-memory uploads. These files are created in
* {@link #processFileField(DiskFileItem)} when an uploaded file is
* stored in memory and needs to be written to disk.</p>
*
* <p>The cleanup process:</p>
* <ol>
* <li>Iterates through all tracked temporary files</li>
* <li>Checks if each file still exists</li>
* <li>Attempts to delete existing files</li>
* <li>Logs warnings for files that cannot be deleted</li>
* </ol>
*
* <p>This method can be overridden by subclasses to customize cleanup
* behavior. All exceptions are caught and logged to ensure cleanup
* continues even if individual file deletions fail.</p>
*
* @see #cleanUp()
* @see #cleanUpDiskFileItems()
*/
protected void cleanUpTemporaryFiles() {
LOG.debug("Cleaning up {} temporary files created for in-memory uploads", temporaryFiles.size());
@@ -207,7 +336,28 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
}
/**
* Override cleanUp to ensure all DiskFileItem instances and temporary files are properly cleaned up
* Performs complete cleanup of all resources associated with this request.
*
* <p>This method extends the parent cleanup functionality to ensure proper
* cleanup of Jakarta-specific resources:</p>
* <ol>
* <li>Calls parent cleanup to handle base class resources</li>
* <li>Cleans up all tracked {@link DiskFileItem} instances</li>
* <li>Cleans up all temporary files created for in-memory uploads</li>
* <li>Clears internal tracking collections</li>
* </ol>
*
* <p>This method is designed to be safe to call multiple times and will
* not throw exceptions even if cleanup operations fail. All errors are
* logged for debugging purposes.</p>
*
* <p><strong>Important:</strong> This method should always be called in a
* finally block to ensure resources are properly released, even if
* exceptions occur during request processing.</p>
*
* @see #cleanUpDiskFileItems()
* @see #cleanUpTemporaryFiles()
* @see AbstractMultiPartRequest#cleanUp()
*/
@Override
public void cleanUp() {
@@ -81,36 +81,62 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
});
}
/**
* Reads the entire contents of an input stream into a string.
*
* <p>This method uses a buffered approach to efficiently read the stream
* content without loading the entire stream into memory at once. It uses
* try-with-resources to ensure proper cleanup of resources.</p>
*
* @param inputStream the input stream to read from
* @return the stream contents as a UTF-8 string
* @throws IOException if an error occurs reading the stream
*/
private String readStream(InputStream inputStream) throws IOException {
// Use try-with-resources to ensure ByteArrayOutputStream is properly closed
try (ByteArrayOutputStream result = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
byte[] buffer = new byte[1024]; // 1KB buffer for efficient reading
// Read the stream in chunks to avoid loading everything into memory at once
for (int length; (length = inputStream.read(buffer)) != -1; ) {
result.write(buffer, 0, length);
}
// Convert to string using UTF-8 encoding
return result.toString(StandardCharsets.UTF_8);
}
}
/**
* Processes the FileItem as a normal form field.
*
* @param fileItemInput a form field item input
* Processes a normal form field (non-file) from the multipart request using streaming API.
*
* <p>This method handles text form fields by:</p>
* <ol>
* <li>Validating the field name is not null</li>
* <li>Reading the field value from the input stream</li>
* <li>Checking if the field value exceeds maximum string length</li>
* <li>Adding the value to the parameters collection</li>
* </ol>
*
* <p>Fields with null names are skipped with a warning log message.</p>
* <p>The streaming approach is more memory-efficient for large form data.</p>
*
* @param fileItemInput a form field item input from the streaming API
* @throws IOException if an error occurs reading the input stream
* @see #readStream(InputStream)
* @see #exceedsMaxStringLength(String, String)
*/
protected void processFileItemAsFormField(FileItemInput fileItemInput) throws IOException {
String fieldName = fileItemInput.getFieldName();
if (fieldName == null) {
LOG.warn("Form field has null fieldName, skipping");
return;
}
String fieldValue = readStream(fileItemInput.getInputStream());
if (exceedsMaxStringLength(fieldName, fieldValue)) {
return;
}
List<String> values;
if (parameters.containsKey(fieldName)) {
values = parameters.get(fieldName);
} else {
values = new ArrayList<>();
parameters.put(fieldName, values);
}
List<String> values = parameters.computeIfAbsent(fieldName, k -> new ArrayList<>());
values.add(fieldValue);
}
@@ -165,10 +191,28 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
}
/**
* Processes the FileItem as a file field.
*
* @param fileItemInput file item representing upload file
* @param location location
* Processes a file field from the multipart request using streaming API.
*
* <p>This method handles file uploads by:</p>
* <ol>
* <li>Validating the file name and field name are not null/empty</li>
* <li>Checking if the upload exceeds maximum file count</li>
* <li>Creating a temporary file in the specified location</li>
* <li>Streaming the file content directly to disk</li>
* <li>Checking if the total size exceeds maximum allowed size</li>
* <li>Creating an {@link UploadedFile} abstraction or cleaning up on size exceeded</li>
* </ol>
*
* <p>Files with null names or field names are skipped with appropriate logging.</p>
* <p>The streaming approach is more memory-efficient for large file uploads
* as it writes directly to disk rather than loading into memory first.</p>
*
* @param fileItemInput file item representing upload file from streaming API
* @param location the directory where temporary files will be created
* @throws IOException if an error occurs during file processing
* @see #createTemporaryFile(String, Path)
* @see #streamFileToDisk(FileItemInput, File)
* @see #createUploadedFile(FileItemInput, File)
*/
protected void processFileItemAsFileField(FileItemInput fileItemInput, Path location) throws IOException {
// Skip file uploads that don't have a file name - meaning that no file was selected.
@@ -176,6 +220,12 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
LOG.debug(() -> "No file has been uploaded for the field: " + normalizeSpace(fileItemInput.getFieldName()));
return;
}
// Skip file uploads that don't have a field name
if (fileItemInput.getFieldName() == null) {
LOG.warn("File upload has null fieldName, skipping");
return;
}
if (exceedsMaxFiles(fileItemInput)) {
return;
@@ -224,29 +274,40 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
}
/**
* Create {@link UploadedFile} abstraction over uploaded file
*
* @param fileItemInput file item stream
* @param file the file
* Creates an {@link UploadedFile} abstraction over an uploaded file.
*
* <p>This method creates a wrapper around the uploaded file that provides
* a consistent interface for accessing file information and content.
* The created {@link UploadedFile} instance contains:</p>
* <ul>
* <li>The original filename as provided by the client</li>
* <li>The content type (MIME type) if available</li>
* <li>The form field name that contained the file</li>
* <li>A reference to the temporary file on disk</li>
* </ul>
*
* <p>The file is automatically added to the uploaded files collection,
* grouped by field name to support multiple file uploads per field.</p>
*
* @param fileItemInput file item stream containing file metadata
* @param file the temporary file containing the uploaded content
* @see UploadedFile
* @see StrutsUploadedFile
*/
protected void createUploadedFile(FileItemInput fileItemInput, File file) {
String fileName = fileItemInput.getName();
String fieldName = fileItemInput.getFieldName();
// fieldName null check already done in processFileItemAsFileField
UploadedFile uploadedFile = StrutsUploadedFile.Builder
.create(file)
.withOriginalName(fileName)
.withContentType(fileItemInput.getContentType())
.withInputName(fileItemInput.getFieldName())
.withInputName(fieldName)
.build();
if (uploadedFiles.containsKey(fieldName)) {
uploadedFiles.get(fieldName).add(uploadedFile);
} else {
List<UploadedFile> infos = new ArrayList<>();
infos.add(uploadedFile);
uploadedFiles.put(fieldName, infos);
}
List<UploadedFile> infos = uploadedFiles.computeIfAbsent(fieldName, key -> new ArrayList<>());
infos.add(uploadedFile);
}
}