This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch merge/master-to-7xx-2024-04-07 in repository https://gitbox.apache.org/repos/asf/struts.git
commit e5e6145c361ec6e6c98d86f9aaeef7e4bdb14f30 Merge: 4939d3cef 0f6d5dbb2 Author: Lukasz Lenart <lukaszlen...@apache.org> AuthorDate: Sun Apr 7 07:58:50 2024 +0200 Merge remote-tracking branch 'origin/master' into merge/master-to-7xx-2024-04-07 # Conflicts: # pom.xml .asf.yaml | 8 +- README.md | 2 +- .../showcase/fileupload/FileUploadAction.java | 5 +- .../MultipleFileUploadUsingArrayAction.java | 82 ++++++------- .../MultipleFileUploadUsingListAction.java | 88 +++++++------- .../providers/XmlDocConfigurationProvider.java | 4 +- .../apache/struts2/action/UploadedFilesAware.java | 3 +- .../org/apache/struts2/dispatcher/Dispatcher.java | 13 ++- .../multipart/AbstractMultiPartRequest.java | 11 +- .../multipart/JakartaMultiPartRequest.java | 7 +- .../multipart/JakartaStreamMultiPartRequest.java | 6 +- .../dispatcher/multipart/MultiPartRequest.java | 2 +- .../multipart/MultiPartRequestWrapper.java | 3 +- .../dispatcher/multipart/StrutsUploadedFile.java | 4 +- .../struts2/dispatcher/multipart/UploadedFile.java | 4 +- .../interceptor/AbstractFileUploadInterceptor.java | 3 +- .../interceptor/ActionFileUploadInterceptor.java | 7 +- .../struts2/interceptor/ApplicationAware.java} | 16 +-- .../struts2/interceptor/HttpParametersAware.java} | 16 +-- .../struts2/interceptor/ParameterAware.java} | 20 ++-- .../struts2/interceptor/PrincipalAware.java} | 14 +-- .../apache/struts2/interceptor/RequestAware.java} | 23 +++- .../struts2/interceptor/ServletRequestAware.java} | 16 +-- .../struts2/interceptor/ServletResponseAware.java} | 16 +-- .../apache/struts2/interceptor/SessionAware.java} | 16 +-- .../apache/struts2/util/ServletContextAware.java} | 16 +-- .../multipart/AbstractMultiPartRequestTest.java | 8 +- .../multipart/JakartaMultiPartRequestTest.java | 4 +- .../ActionFileUploadInterceptorTest.java | 14 +-- .../interceptor/FileUploadInterceptorTest.java | 14 +-- plugins/jasperreports/pom.xml | 2 +- .../portlet/interceptor/PortletContextAware.java | 16 +-- .../interceptor/PortletPreferencesAware.java | 16 +-- .../portlet/interceptor/PortletRequestAware.java | 16 +-- .../portlet/interceptor/PortletResponseAware.java | 16 +-- plugins/tiles/pom.xml | 3 - .../struts2/tiles/StrutsTilesContainerFactory.java | 28 +++-- .../tiles/StrutsTilesContainerFactoryTest.java | 128 +++++++++++++++++++++ pom.xml | 47 +++----- 39 files changed, 436 insertions(+), 281 deletions(-) diff --cc core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java index 1c9148047,23d879ba4..630d2ca43 --- a/core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java @@@ -46,10 -34,8 +46,10 @@@ import java.util.Map * Abstract class with some helper methods, it should be used * when starting development of another implementation of {@link MultiPartRequest} */ - public abstract class AbstractMultiPartRequest<T> implements MultiPartRequest { + public abstract class AbstractMultiPartRequest implements MultiPartRequest { + protected static final String STRUTS_MESSAGES_UPLOAD_ERROR_PARAMETER_TOO_LONG_KEY = "struts.messages.upload.error.parameter.too.long"; + private static final Logger LOG = LogManager.getLogger(AbstractMultiPartRequest.class); /** @@@ -98,14 -76,9 +98,14 @@@ protected String defaultEncoding; /** - * Localization to be used regarding errors. + * Map between file fields and file data. */ - protected Map<String, List<UploadedFile<T>>> uploadedFiles = new HashMap<>(); - protected Locale defaultLocale = Locale.ENGLISH; ++ protected Map<String, List<UploadedFile>> uploadedFiles = new HashMap<>(); + + /** + * Map between non-file fields and values. + */ + protected Map<String, List<String>> parameters = new HashMap<>(); /** * @param bufferSize Sets the buffer size to be used. @@@ -296,109 -169,4 +296,108 @@@ return fileName; } + protected String sanitizeNewlines(String before) { + return before.replaceAll("\\R", "_"); + } + + /* (non-Javadoc) + * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getErrors() + */ + public List<LocalizedMessage> getErrors() { + return errors; + } + + /* (non-Javadoc) + * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileParameterNames() + */ + public Enumeration<String> getFileParameterNames() { + return Collections.enumeration(uploadedFiles.keySet()); + } + + /* (non-Javadoc) + * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getContentType(java.lang.String) + */ + public String[] getContentType(String fieldName) { + return uploadedFiles.getOrDefault(fieldName, Collections.emptyList()).stream() + .map(UploadedFile::getContentType) + .toArray(String[]::new); + } + + /* (non-Javadoc) + * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFile(java.lang.String) + */ - @SuppressWarnings("unchecked") - public UploadedFile<T>[] getFile(String fieldName) { ++ public UploadedFile[] getFile(String fieldName) { + return uploadedFiles.getOrDefault(fieldName, Collections.emptyList()) + .toArray(UploadedFile[]::new); + } + + /* (non-Javadoc) + * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileNames(java.lang.String) + */ + public String[] getFileNames(String fieldName) { + return uploadedFiles.getOrDefault(fieldName, Collections.emptyList()).stream() + .map(file -> getCanonicalName(file.getOriginalName())) + .toArray(String[]::new); + } + + /* (non-Javadoc) + * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFilesystemName(java.lang.String) + */ + public String[] getFilesystemName(String fieldName) { + return uploadedFiles.getOrDefault(fieldName, Collections.emptyList()).stream() + .map(UploadedFile::getAbsolutePath) + .toArray(String[]::new); + } + + /* (non-Javadoc) + * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameter(java.lang.String) + */ + public String getParameter(String name) { + List<String> paramValue = parameters.getOrDefault(name, Collections.emptyList()); + if (!paramValue.isEmpty()) { + return paramValue.get(0); + } + + return null; + } + + /* (non-Javadoc) + * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterNames() + */ + public Enumeration<String> getParameterNames() { + return Collections.enumeration(parameters.keySet()); + } + + /* (non-Javadoc) + * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterValues(java.lang.String) + */ + public String[] getParameterValues(String name) { + return parameters.getOrDefault(name, Collections.emptyList()) + .toArray(String[]::new); + } + + /* (non-Javadoc) + * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp() + */ + public void cleanUp() { + try { + LOG.debug("Performing File Upload temporary storage cleanup."); - for (List<UploadedFile<T>> uploadedFileList : uploadedFiles.values()) { - for (UploadedFile<T> uploadedFile : uploadedFileList) { ++ for (List<UploadedFile> uploadedFileList : uploadedFiles.values()) { ++ for (UploadedFile uploadedFile : uploadedFileList) { + if (uploadedFile.isFile()) { + LOG.debug("Deleting file: {}", uploadedFile.getName()); + if (!uploadedFile.delete()) { + LOG.warn("There was a problem attempting to delete file: {}", uploadedFile.getName()); + } + } else { + LOG.debug("File: {} already deleted", uploadedFile.getName()); + } + } + } + } finally { + uploadedFiles = new HashMap<>(); + parameters = new HashMap<>(); + } + } + } diff --cc core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java index c71a1ee25,1c0f458a0..b3227cb64 --- a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java @@@ -25,104 -29,344 +25,103 @@@ import org.apache.commons.fileupload2.j import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.struts2.dispatcher.LocalizedMessage; -import javax.servlet.http.HttpServletRequest; --import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.file.Path; import java.util.ArrayList; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Set; /** - * Multipart form data request adapter for Jakarta Commons Fileupload package. + * Multipart form data request adapter for Jakarta Commons FileUpload package. */ - public class JakartaMultiPartRequest extends AbstractMultiPartRequest<File> { + public class JakartaMultiPartRequest extends AbstractMultiPartRequest { - static final Logger LOG = LogManager.getLogger(JakartaMultiPartRequest.class); + private static final Logger LOG = LogManager.getLogger(JakartaMultiPartRequest.class); - // maps parameter name -> List of FileItem objects - protected Map<String, List<FileItem>> files = new HashMap<>(); + @Override + protected void processUpload(HttpServletRequest request, String saveDir) throws IOException { + Charset charset = readCharsetEncoding(request); - // maps parameter name -> List of param values - protected Map<String, List<String>> params = new HashMap<>(); + JakartaServletDiskFileUpload servletFileUpload = + prepareServletFileUpload(charset, Path.of(saveDir)); - /** - * Creates a new request wrapper to handle multi-part data using methods adapted from Jason Pell's - * multipart classes (see class description). - * - * @param saveDir the directory to save off the file - * @param request the request containing the multipart - * @throws java.io.IOException is thrown if encoding fails. - */ - public void parse(HttpServletRequest request, String saveDir) throws IOException { - try { - setLocale(request); - processUpload(request, saveDir); - } catch (FileUploadException e) { - LOG.debug("Request exceeded size limit!", e); - LocalizedMessage errorMessage; - if (e instanceof FileUploadBase.SizeLimitExceededException) { - FileUploadBase.SizeLimitExceededException ex = (FileUploadBase.SizeLimitExceededException) e; - errorMessage = buildErrorMessage(e, new Object[]{ex.getPermittedSize(), ex.getActualSize()}); - } else if (e instanceof FileUploadBase.FileSizeLimitExceededException) { - FileUploadBase.FileSizeLimitExceededException ex = (FileUploadBase.FileSizeLimitExceededException) e; - errorMessage = buildErrorMessage(e, new Object[]{ex.getFileName(), ex.getPermittedSize(), ex.getActualSize()}); - } else if (e instanceof FileCountLimitExceededException) { - FileCountLimitExceededException ex = (FileCountLimitExceededException) e; - errorMessage = buildErrorMessage(e, new Object[]{ex.getLimit()}); + for (DiskFileItem item : servletFileUpload.parseRequest(request)) { + LOG.debug(() -> "Processing a form field: " + sanitizeNewlines(item.getFieldName())); + if (item.isFormField()) { + processNormalFormField(item, charset); } else { - errorMessage = buildErrorMessage(e, new Object[]{}); + LOG.debug(() -> "Processing a file: " + sanitizeNewlines(item.getFieldName())); + processFileField(item); } - - if (!errors.contains(errorMessage)) { - errors.add(errorMessage); - } - } catch (Exception e) { - LOG.debug("Unable to parse request", e); - LocalizedMessage errorMessage = buildErrorMessage(e, new Object[]{}); - if (!errors.contains(errorMessage)) { - errors.add(errorMessage); - } - } - } - - protected void processUpload(HttpServletRequest request, String saveDir) throws FileUploadException, UnsupportedEncodingException { - if (ServletFileUpload.isMultipartContent(request)) { - for (FileItem item : parseRequest(request, saveDir)) { - LOG.debug("Found file item: [{}]", sanitizeNewlines(item.getFieldName())); - if (item.isFormField()) { - processNormalFormField(item, request.getCharacterEncoding()); - } else { - processFileField(item); - } - } - } - } - - protected void processFileField(FileItem item) { - LOG.debug("Item is a file upload"); - - // Skip file uploads that don't have a file name - meaning that no file was selected. - if (item.getName() == null || item.getName().trim().isEmpty()) { - LOG.debug("No file has been uploaded for the field: {}", sanitizeNewlines(item.getFieldName())); - return; - } - - List<FileItem> values; - if (files.get(item.getFieldName()) != null) { - values = files.get(item.getFieldName()); - } else { - values = new ArrayList<>(); - } - - values.add(item); - files.put(item.getFieldName(), values); - } - - protected void processNormalFormField(FileItem item, String charset) throws UnsupportedEncodingException { - try { - LOG.debug("Item is a normal form field"); - - List<String> values; - if (params.get(item.getFieldName()) != null) { - values = params.get(item.getFieldName()); - } else { - values = new ArrayList<>(); - } - - long size = item.getSize(); - if (size > maxStringLength) { - LOG.debug("Form field {} of size {} bytes exceeds limit of {}.", sanitizeNewlines(item.getFieldName()), size, maxStringLength); - String errorKey = "struts.messages.upload.error.parameter.too.long"; - LocalizedMessage localizedMessage = new LocalizedMessage(this.getClass(), errorKey, null, - new Object[]{item.getFieldName(), maxStringLength, size}); - if (!errors.contains(localizedMessage)) { - errors.add(localizedMessage); - } - return; - } - if (size == 0) { - values.add(StringUtils.EMPTY); - } else if (charset == null) { - values.add(item.getString()); // WW-633 - } else { - values.add(item.getString(charset)); - } - params.put(item.getFieldName(), values); - } finally { - item.delete(); } } - protected List<FileItem> parseRequest(HttpServletRequest servletRequest, String saveDir) throws FileUploadException { - DiskFileItemFactory fac = createDiskFileItemFactory(saveDir); - ServletFileUpload upload = createServletFileUpload(fac); - return upload.parseRequest(createRequestContext(servletRequest)); - } + protected JakartaServletDiskFileUpload createJakartaFileUpload(Charset charset, Path saveDir) { + DiskFileItemFactory.Builder builder = DiskFileItemFactory.builder(); - protected ServletFileUpload createServletFileUpload(DiskFileItemFactory fac) { - ServletFileUpload upload = new ServletFileUpload(fac); - if (maxSize != null) { - upload.setSizeMax(maxSize); - } - if (maxFiles != null) { - upload.setFileCountMax(maxFiles); - } - if (maxFileSize != null) { - upload.setFileSizeMax(maxFileSize); - } - return upload; - } - - protected DiskFileItemFactory createDiskFileItemFactory(String saveDir) { - DiskFileItemFactory fac = new DiskFileItemFactory(); - // Make sure that the data is written to file, even if the file is empty. - fac.setSizeThreshold(-1); - if (saveDir != null) { - fac.setRepository(new File(saveDir)); - } - return fac; - } + LOG.debug("Using file save directory: {}", saveDir); + builder.setPath(saveDir); - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileParameterNames() - */ - public Enumeration<String> getFileParameterNames() { - return Collections.enumeration(files.keySet()); - } - - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getContentType(java.lang.String) - */ - public String[] getContentType(String fieldName) { - List<FileItem> items = files.get(fieldName); + LOG.debug("Sets minimal buffer size to always write file to disk"); + builder.setBufferSize(1); - if (items == null) { - return null; - } - - List<String> contentTypes = new ArrayList<>(items.size()); - for (FileItem fileItem : items) { - contentTypes.add(fileItem.getContentType()); - } + LOG.debug("Using charset: {}", charset); + builder.setCharset(charset); - return contentTypes.toArray(new String[0]); + DiskFileItemFactory factory = builder.get(); + return new JakartaServletDiskFileUpload(factory); } - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFile(java.lang.String) - */ - public UploadedFile[] getFile(String fieldName) { - List<FileItem> items = files.get(fieldName); + protected void processNormalFormField(DiskFileItem item, Charset charset) throws IOException { + LOG.debug("Item: {} is a normal form field", item.getName()); - if (items == null) { - return null; - } - - List<UploadedFile> fileList = new ArrayList<>(items.size()); - for (FileItem fileItem : items) { - DiskFileItem diskFileItem = (DiskFileItem) fileItem; - File storeLocation = diskFileItem.getStoreLocation(); - - // Ensure file exists even if it is empty. - if (diskFileItem.getSize() == 0 && storeLocation != null && !storeLocation.exists()) { - try { - storeLocation.createNewFile(); - } catch (IOException e) { - LOG.error("Cannot write uploaded empty file to disk: {}", storeLocation.getAbsolutePath(), e); - } - } - UploadedFile uploadedFile = StrutsUploadedFile.Builder.create(storeLocation) - .withContentType(fileItem.getContentType()) - .withOriginalName(fileItem.getName()) - .build(); - fileList.add(uploadedFile); + List<String> values; + String fieldName = item.getFieldName(); + if (parameters.get(fieldName) != null) { + values = parameters.get(fieldName); + } else { + values = new ArrayList<>(); } - return fileList.toArray(new UploadedFile[0]); - } - - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileNames(java.lang.String) - */ - public String[] getFileNames(String fieldName) { - List<FileItem> items = files.get(fieldName); - - if (items == null) { - return null; + String fieldValue = item.getString(charset); + if (exceedsMaxStringLength(fieldName, fieldValue)) { + return; } - - List<String> fileNames = new ArrayList<>(items.size()); - for (FileItem fileItem : items) { - fileNames.add(getCanonicalName(fileItem.getName())); + if (item.getSize() == 0) { + values.add(StringUtils.EMPTY); + } else { + values.add(fieldValue); } - - return fileNames.toArray(new String[0]); + parameters.put(fieldName, values); } - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFilesystemName(java.lang.String) - */ - public String[] getFilesystemName(String fieldName) { - List<FileItem> items = files.get(fieldName); - - if (items == null) { - return null; - } - - List<String> fileNames = new ArrayList<>(items.size()); - for (FileItem fileItem : items) { - fileNames.add(((DiskFileItem) fileItem).getStoreLocation().getName()); + 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()) { + LOG.debug(() -> "No file has been uploaded for the field: " + sanitizeNewlines(item.getFieldName())); + return; } - List<UploadedFile<File>> values; - return fileNames.toArray(new String[0]); - } - - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameter(java.lang.String) - */ - public String getParameter(String name) { - List<String> v = params.get(name); - if (v != null && !v.isEmpty()) { - return v.get(0); ++ List<UploadedFile> values; + if (uploadedFiles.get(item.getFieldName()) != null) { + values = uploadedFiles.get(item.getFieldName()); + } else { + values = new ArrayList<>(); } - return null; - } - - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterNames() - */ - public Enumeration<String> getParameterNames() { - return Collections.enumeration(params.keySet()); - } - - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterValues(java.lang.String) - */ - public String[] getParameterValues(String name) { - List<String> v = params.get(name); - if (v != null && !v.isEmpty()) { - return v.toArray(new String[0]); + if (item.isInMemory()) { + LOG.warn("Storing uploaded files just in memory isn't supported currently, skipping file: {}!", item.getName()); + } else { - UploadedFile<File> uploadedFile = StrutsUploadedFile.Builder ++ UploadedFile uploadedFile = StrutsUploadedFile.Builder + .create(item.getPath().toFile()) + .withOriginalName(item.getName()) + .withContentType(item.getContentType()) + .build(); + values.add(uploadedFile); } - return null; + uploadedFiles.put(item.getFieldName(), values); } - /** - * Creates a RequestContext needed by Jakarta Commons Upload. - * - * @param req the request. - * @return a new request context. - */ - protected RequestContext createRequestContext(final HttpServletRequest req) { - return new RequestContext() { - public String getCharacterEncoding() { - return req.getCharacterEncoding(); - } - - public String getContentType() { - return req.getContentType(); - } - - public int getContentLength() { - return req.getContentLength(); - } - - public InputStream getInputStream() throws IOException { - InputStream in = req.getInputStream(); - if (in == null) { - throw new IOException("Missing content in the request"); - } - return req.getInputStream(); - } - }; - } - - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp() - */ - public void cleanUp() { - Set<String> names = files.keySet(); - for (String name : names) { - List<FileItem> items = files.get(name); - for (FileItem item : items) { - LOG.debug("Removing file {} {}", name, item); - if (!item.isInMemory()) { - item.delete(); - } - } - } - } - - private String sanitizeNewlines(String before) { - return before.replaceAll("[\n\r]", "_"); - } } diff --cc core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java index c6457c605,428ae112f..df7b07e22 --- a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java @@@ -50,105 -53,150 +50,105 @@@ import java.util.UUID * * @since 2.3.18 */ - public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest<File> { + public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { - static final Logger LOG = LogManager.getLogger(JakartaStreamMultiPartRequest.class); + private static final Logger LOG = LogManager.getLogger(JakartaStreamMultiPartRequest.class); /** - * Map between file fields and file data. - */ - protected Map<String, List<FileInfo>> fileInfos = new HashMap<>(); - - /** - * Map between non-file fields and values. - */ - protected Map<String, List<String>> parameters = new HashMap<>(); - - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp() + * Processes the upload. + * + * @param request the servlet request + * @param saveDir location of the save dir */ - public void cleanUp() { - LOG.debug("Performing File Upload temporary storage cleanup."); - for (List<FileInfo> fileInfoList : fileInfos.values()) { - for (FileInfo fileInfo : fileInfoList) { - File file = fileInfo.getFile(); - LOG.debug("Deleting file '{}'.", file.getName()); - if (!file.delete()) { - LOG.warn("There was a problem attempting to delete file '{}'.", file.getName()); - } + @Override + protected void processUpload(HttpServletRequest request, String saveDir) throws IOException { + Charset charset = readCharsetEncoding(request); + Path location = Path.of(saveDir); + + JakartaServletDiskFileUpload servletFileUpload = + prepareServletFileUpload(charset, location); + + LOG.debug("Using Jakarta Stream API to process request"); + servletFileUpload.getItemIterator(request).forEachRemaining(item -> { + if (item.isFormField()) { + LOG.debug(() -> "Processing a form field: " + sanitizeNewlines(item.getFieldName())); + processFileItemAsFormField(item); + } else { + LOG.debug(() -> "Processing a file: " + sanitizeNewlines(item.getFieldName())); + processFileItemAsFileField(item, location); } - } + }); } - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getContentType(java.lang.String) - */ - public String[] getContentType(String fieldName) { - List<FileInfo> infos = fileInfos.get(fieldName); - if (infos == null) { - return null; - } + protected JakartaServletDiskFileUpload createJakartaFileUpload(Charset charset, Path location) { + DiskFileItemFactory.Builder builder = DiskFileItemFactory.builder(); - List<String> types = new ArrayList<>(infos.size()); - for (FileInfo fileInfo : infos) { - types.add(fileInfo.getContentType()); - } + LOG.debug("Using file save directory: {}", location); + builder.setPath(location); - return types.toArray(new String[0]); - } + LOG.debug("Sets buffer size: {}", bufferSize); + builder.setBufferSize(bufferSize); - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFile(java.lang.String) - */ - public UploadedFile[] getFile(String fieldName) { - List<FileInfo> infos = fileInfos.get(fieldName); - if (infos == null) { - return null; - } + LOG.debug("Using charset: {}", charset); + builder.setCharset(charset); - return infos.stream().map(fileInfo -> - StrutsUploadedFile.Builder.create(fileInfo.getFile()) - .withContentType(fileInfo.contentType) - .withOriginalName(fileInfo.originalName) - .build() - ).toArray(UploadedFile[]::new); + DiskFileItemFactory factory = builder.get(); + return new JakartaServletDiskFileUpload(factory); } - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileNames(java.lang.String) - */ - public String[] getFileNames(String fieldName) { - List<FileInfo> infos = fileInfos.get(fieldName); - if (infos == null) { - return null; + private String readStream(InputStream inputStream) throws IOException { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + for (int length; (length = inputStream.read(buffer)) != -1; ) { + result.write(buffer, 0, length); } - - List<String> names = new ArrayList<>(infos.size()); - for (FileInfo fileInfo : infos) { - names.add(getCanonicalName(fileInfo.getOriginalName())); - } - - return names.toArray(new String[0]); + return result.toString(StandardCharsets.UTF_8); } - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileParameterNames() - */ - public Enumeration<String> getFileParameterNames() { - return Collections.enumeration(fileInfos.keySet()); - } - - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFilesystemName(java.lang.String) + /** + * Processes the FileItem as a normal form field. + * + * @param fileItemInput a form field item input */ - public String[] getFilesystemName(String fieldName) { - List<FileInfo> infos = fileInfos.get(fieldName); - if (infos == null) { - return null; - } + protected void processFileItemAsFormField(FileItemInput fileItemInput) throws IOException { + String fieldName = fileItemInput.getFieldName(); + String fieldValue = readStream(fileItemInput.getInputStream()); - List<String> names = new ArrayList<>(infos.size()); - for (FileInfo fileInfo : infos) { - names.add(fileInfo.getFile().getName()); + if (exceedsMaxStringLength(fieldName, fieldValue)) { + return; } - return names.toArray(new String[0]); - } - - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameter(java.lang.String) - */ - public String getParameter(String name) { - List<String> values = parameters.get(name); - if (values != null && !values.isEmpty()) { - return values.get(0); + List<String> values; + if (parameters.containsKey(fieldName)) { + values = parameters.get(fieldName); + } else { + values = new ArrayList<>(); + parameters.put(fieldName, values); } - return null; - } - - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterNames() - */ - public Enumeration<String> getParameterNames() { - return Collections.enumeration(parameters.keySet()); + values.add(fieldValue); } - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterValues(java.lang.String) + /** + * @return actual size of already uploaded files */ - public String[] getParameterValues(String name) { - List<String> values = parameters.get(name); - if (values != null && !values.isEmpty()) { - return values.toArray(new String[0]); - } - return null; + protected Long actualSizeOfUploadedFiles() { + return uploadedFiles.values().stream() + .map(files -> files.stream().map(UploadedFile::length).reduce(0L, Long::sum)) + .reduce(0L, Long::sum); } - /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#parse(javax.servlet.http.HttpServletRequest, java.lang.String) - */ - public void parse(HttpServletRequest request, String saveDir) throws IOException { - try { - setLocale(request); - processUpload(request, saveDir); - } catch (Exception e) { - LOG.debug("Error occurred during parsing of multi part request", e); - LocalizedMessage errorMessage = buildErrorMessage(e, new Object[]{}); + private boolean exceedsMaxFiles(FileItemInput fileItemInput) { + if (maxFiles != null && maxFiles == uploadedFiles.size()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Cannot accept another file: {} as it will exceed max files: {}", + sanitizeNewlines(fileItemInput.getName()), maxFiles); + } + LocalizedMessage errorMessage = buildErrorMessage( + FileUploadFileCountLimitException.class, + String.format("File %s exceeds allowed maximum number of files %s", + fileItemInput.getName(), maxFiles), + new Object[]{maxFiles, uploadedFiles.size()} + ); if (!errors.contains(errorMessage)) { errors.add(errorMessage); } @@@ -238,27 -410,76 +238,27 @@@ } /** - * Creates an internal <code>FileInfo</code> structure used to pass information - * to the <code>FileUploadInterceptor</code> during the interceptor stack - * invocation process. + * Create {@link UploadedFile} abstraction over uploaded file * - * @param itemStream file item stream - * @param file the file + * @param fileItemInput file item stream + * @param file the file */ - protected void createFileInfoFromItemStream(FileItemStream itemStream, File file) { - // gather attributes from file upload stream. - String fileName = itemStream.getName(); - String fieldName = itemStream.getFieldName(); - // create internal structure - FileInfo fileInfo = new FileInfo(file, itemStream.getContentType(), fileName); - // append or create new entry. - if (!fileInfos.containsKey(fieldName)) { - List<FileInfo> infos = new ArrayList<>(); - infos.add(fileInfo); - fileInfos.put(fieldName, infos); + protected void createUploadedFile(FileItemInput fileItemInput, File file) { + String fileName = fileItemInput.getName(); + String fieldName = fileItemInput.getFieldName(); + - UploadedFile<File> uploadedFile = StrutsUploadedFile.Builder ++ UploadedFile uploadedFile = StrutsUploadedFile.Builder + .create(file) + .withOriginalName(fileName) + .withContentType(fileItemInput.getContentType()) + .build(); + + if (uploadedFiles.containsKey(fieldName)) { + uploadedFiles.get(fieldName).add(uploadedFile); } else { - List<UploadedFile<File>> infos = new ArrayList<>(); - fileInfos.get(fieldName).add(fileInfo); - } - } - - /** - * Internal data structure used to store a reference to information needed - * to later pass post processing data to the <code>FileUploadInterceptor</code>. - * - * @since 7.0.0 - */ - public static class FileInfo implements Serializable { - - private static final long serialVersionUID = 1083158552766906037L; - - private final File file; - private final String contentType; - private final String originalName; - - /** - * Default constructor. - * - * @param file the file - * @param contentType content type - * @param originalName original file name - */ - public FileInfo(File file, String contentType, String originalName) { - this.file = file; - this.contentType = contentType; - this.originalName = originalName; - } - - /** - * @return the file - */ - public File getFile() { - return file; - } - - /** - * @return content type - */ - public String getContentType() { - return contentType; - } - - /** - * @return original file name - */ - public String getOriginalName() { - return originalName; ++ List<UploadedFile> infos = new ArrayList<>(); + infos.add(uploadedFile); + uploadedFiles.put(fieldName, infos); } } diff --cc core/src/main/java/org/apache/struts2/dispatcher/multipart/MultiPartRequestWrapper.java index 7ddadb8c5,d37eed143..532a5e45a --- a/core/src/main/java/org/apache/struts2/dispatcher/multipart/MultiPartRequestWrapper.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/MultiPartRequestWrapper.java @@@ -24,9 -24,7 +24,8 @@@ import org.apache.logging.log4j.Logger import org.apache.struts2.dispatcher.LocalizedMessage; import org.apache.struts2.dispatcher.StrutsRequestWrapper; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; + - import java.io.File; import java.io.IOException; import java.util.*; diff --cc core/src/main/java/org/apache/struts2/dispatcher/multipart/UploadedFile.java index c48054765,ada27ff6c..ddebac122 --- a/core/src/main/java/org/apache/struts2/dispatcher/multipart/UploadedFile.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/UploadedFile.java @@@ -21,48 -21,24 +21,48 @@@ package org.apache.struts2.dispatcher.m import java.io.Serializable; /** - * Virtual representation of a uploaded file used by {@link MultiPartRequest} + * Virtual representation of an uploaded file used by {@link MultiPartRequest} */ - public interface UploadedFile<T> extends Serializable { + public interface UploadedFile extends Serializable { + /** + * @return size of the content of file/stream/array + */ Long length(); + /** + * @return a local name of the file + */ String getName(); + /** + * @return original file name from upload source + */ String getOriginalName(); + /** + * @return indicates if this is a real file or maybe just in-memory stream + */ boolean isFile(); + /** + * @return removes a local copy of the uploaded file/stream + */ boolean delete(); + /** + * @return an absolute path of the file if possible + */ String getAbsolutePath(); + /** + * @return content of the upload file + */ - T getContent(); + Object getContent(); + /** + * @return content type of the uploaded file + */ String getContentType(); } diff --cc core/src/main/java/org/apache/struts2/interceptor/ActionFileUploadInterceptor.java index 72fe043da,c42d125af..3cd0529ae --- a/core/src/main/java/org/apache/struts2/interceptor/ActionFileUploadInterceptor.java +++ b/core/src/main/java/org/apache/struts2/interceptor/ActionFileUploadInterceptor.java @@@ -28,7 -27,7 +28,6 @@@ import org.apache.struts2.action.Upload import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper; import org.apache.struts2.dispatcher.multipart.UploadedFile; - import java.io.File; -import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; diff --cc core/src/main/java/org/apache/struts2/interceptor/RequestAware.java index e24dc133d,822f9ca45..5e24e8dfb --- a/core/src/main/java/org/apache/struts2/interceptor/RequestAware.java +++ b/core/src/main/java/org/apache/struts2/interceptor/RequestAware.java @@@ -16,15 -16,26 +16,26 @@@ * specific language governing permissions and limitations * under the License. */ - package org.apache.struts2.dispatcher.multipart; + package org.apache.struts2.interceptor; - import java.io.File; + import org.apache.struts2.dispatcher.RequestMap; - public class JakartaMultiPartRequestTest extends AbstractMultiPartRequestTest { -import javax.servlet.http.HttpServletRequest; ++import jakarta.servlet.http.HttpServletRequest; + import java.util.Map; + + @Deprecated + public interface RequestAware extends ServletRequestAware { + + @Override + default void setServletRequest(HttpServletRequest httpServletRequest) { + // default no-op + } @Override - protected AbstractMultiPartRequest<File> createMultipartRequest() { - return new JakartaMultiPartRequest(); + default void withServletRequest(HttpServletRequest request) { + ServletRequestAware.super.withServletRequest(request); + setRequest(new RequestMap(request)); } - } + void setRequest(Map<String, Object> request); + } diff --cc core/src/main/java/org/apache/struts2/interceptor/ServletRequestAware.java index e24dc133d,f02ac90d1..8f059d479 --- a/core/src/main/java/org/apache/struts2/interceptor/ServletRequestAware.java +++ b/core/src/main/java/org/apache/struts2/interceptor/ServletRequestAware.java @@@ -16,15 -16,17 +16,17 @@@ * specific language governing permissions and limitations * under the License. */ - package org.apache.struts2.dispatcher.multipart; + package org.apache.struts2.interceptor; - import java.io.File; -import javax.servlet.http.HttpServletRequest; ++import jakarta.servlet.http.HttpServletRequest; - public class JakartaMultiPartRequestTest extends AbstractMultiPartRequestTest { + @Deprecated + public interface ServletRequestAware extends org.apache.struts2.action.ServletRequestAware { + + void setServletRequest(HttpServletRequest httpServletRequest); @Override - protected AbstractMultiPartRequest<File> createMultipartRequest() { - return new JakartaMultiPartRequest(); + default void withServletRequest(HttpServletRequest request) { + setServletRequest(request); } - - } + } diff --cc core/src/main/java/org/apache/struts2/interceptor/ServletResponseAware.java index e24dc133d,d252621fe..de054878a --- a/core/src/main/java/org/apache/struts2/interceptor/ServletResponseAware.java +++ b/core/src/main/java/org/apache/struts2/interceptor/ServletResponseAware.java @@@ -16,15 -16,17 +16,17 @@@ * specific language governing permissions and limitations * under the License. */ - package org.apache.struts2.dispatcher.multipart; + package org.apache.struts2.interceptor; - import java.io.File; -import javax.servlet.http.HttpServletResponse; ++import jakarta.servlet.http.HttpServletResponse; - public class JakartaMultiPartRequestTest extends AbstractMultiPartRequestTest { + @Deprecated + public interface ServletResponseAware extends org.apache.struts2.action.ServletResponseAware { + + void setServletResponse(HttpServletResponse httpServletResponse); @Override - protected AbstractMultiPartRequest<File> createMultipartRequest() { - return new JakartaMultiPartRequest(); + default void withServletResponse(HttpServletResponse response) { + setServletResponse(response); } - - } + } diff --cc core/src/main/java/org/apache/struts2/util/ServletContextAware.java index e24dc133d,c17bd9a7e..593736643 --- a/core/src/main/java/org/apache/struts2/util/ServletContextAware.java +++ b/core/src/main/java/org/apache/struts2/util/ServletContextAware.java @@@ -16,15 -16,17 +16,17 @@@ * specific language governing permissions and limitations * under the License. */ - package org.apache.struts2.dispatcher.multipart; + package org.apache.struts2.util; - import java.io.File; -import javax.servlet.ServletContext; ++import jakarta.servlet.ServletContext; - public class JakartaMultiPartRequestTest extends AbstractMultiPartRequestTest { + @Deprecated + public interface ServletContextAware extends org.apache.struts2.action.ServletContextAware { + + void setServletContext(ServletContext context); @Override - protected AbstractMultiPartRequest<File> createMultipartRequest() { - return new JakartaMultiPartRequest(); + default void withServletContext(ServletContext context) { + setServletContext(context); } - - } + } diff --cc core/src/test/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequestTest.java index eaf7049ae,000000000..fb30bb289 mode 100644,000000..100644 --- a/core/src/test/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequestTest.java +++ b/core/src/test/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequestTest.java @@@ -1,496 -1,0 +1,496 @@@ +/* + * 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.struts2.dispatcher.multipart; + +import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload; +import org.apache.struts2.dispatcher.LocalizedMessage; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; + +abstract class AbstractMultiPartRequestTest { + + protected static String tempDir; + + protected MockHttpServletRequest mockRequest; + + protected final String boundary = "_boundary_"; + protected final String endline = "\r\n"; + - protected AbstractMultiPartRequest<File> multiPart; ++ protected AbstractMultiPartRequest multiPart; + - abstract protected AbstractMultiPartRequest<File> createMultipartRequest(); ++ abstract protected AbstractMultiPartRequest createMultipartRequest(); + + @BeforeClass + public static void beforeClass() throws IOException { + File tempFile = File.createTempFile("struts", "fileupload"); + assertThat(tempFile.delete()).isTrue(); + assertThat(tempFile.mkdirs()).isTrue(); + tempDir = tempFile.getAbsolutePath(); + } + + @Before + public void before() { + mockRequest = new MockHttpServletRequest(); + mockRequest.setCharacterEncoding(StandardCharsets.UTF_8.name()); + mockRequest.setMethod("post"); + mockRequest.setContentType("multipart/form-data; boundary=" + boundary); + + multiPart = createMultipartRequest(); + } + + @After + public void after() { + multiPart.cleanUp(); + } + + @Test + public void uploadedFilesToDisk() throws IOException { + // given + String content = formFile("file1", "test1.csv", "1,2,3,4") + + formFile("file2", "test2.csv", "5,6,7,8") + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue(); + + // when + multiPart.setBufferSize("1"); // always write files into disk + multiPart.parse(mockRequest, tempDir); + + // then + assertThat(multiPart.getErrors()) + .isEmpty(); + + assertThat(multiPart.getFileParameterNames().asIterator()).toIterable() + .asList() + .containsOnly("file1", "file2"); + assertThat(multiPart.getFile("file1")).allSatisfy(file -> { + assertThat(file.isFile()) + .isTrue(); + assertThat(file.getOriginalName()) + .isEqualTo("test1.csv"); + assertThat(file.getContentType()) + .isEqualTo("text/csv"); + assertThat(file.getContent()).asInstanceOf(InstanceOfAssertFactories.FILE) + .exists() + .content() + .isEqualTo("1,2,3,4"); + }); + assertThat(multiPart.getFile("file2")).allSatisfy(file -> { + assertThat(file.isFile()) + .isTrue(); + assertThat(file.getOriginalName()) + .isEqualTo("test2.csv"); + assertThat(file.getContentType()) + .isEqualTo("text/csv"); + assertThat(file.getContent()) + .asInstanceOf(InstanceOfAssertFactories.FILE) + .exists() + .content() + .isEqualTo("5,6,7,8"); + }); + } + + @Test + public void uploadedMultipleFilesToDisk() throws IOException { + // given + String content = formFile("file1", "test1.csv", "1,2,3,4") + + formFile("file1", "test2.csv", "5,6,7,8") + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue(); + + // when + multiPart.setBufferSize("1"); // always write files into disk + multiPart.parse(mockRequest, tempDir); + + // then + assertThat(multiPart.getErrors()) + .isEmpty(); + + assertThat(multiPart.getFileParameterNames().asIterator()).toIterable() + .asList() + .containsOnly("file1"); + assertThat(multiPart.getFile("file1")).allSatisfy(file -> { + if (Objects.equals(file.getName(), "test1.csv")) { + assertThat(file.isFile()) + .isTrue(); + assertThat(file.getOriginalName()) + .isEqualTo("test1.csv"); + assertThat(file.getContentType()) + .isEqualTo("text/csv"); + assertThat(file.getContent()).asInstanceOf(InstanceOfAssertFactories.FILE) + .exists() + .content() + .isEqualTo("1,2,3,4"); + } + if (Objects.equals(file.getName(), "test2.csv")) { + assertThat(file.isFile()) + .isTrue(); + assertThat(file.getOriginalName()) + .isEqualTo("test2.csv"); + assertThat(file.getContentType()) + .isEqualTo("text/csv"); + assertThat(file.getContent()) + .asInstanceOf(InstanceOfAssertFactories.FILE) + .exists() + .content() + .isEqualTo("5,6,7,8"); + } + }); + } + + @Test + public void uploadedFilesWithLargeBuffer() throws IOException { + // given + String content = formFile("file1", "test1.csv", "1,2,3,4") + + formFile("file2", "test2.csv", "5,6,7,8") + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue(); + + // when + multiPart.setBufferSize("8192"); // streams files into disk using larger buffer + multiPart.parse(mockRequest, tempDir); + + // then + assertThat(multiPart.getErrors()) + .isEmpty(); + + assertThat(multiPart.getFileParameterNames().asIterator()).toIterable() + .asList() + .containsOnly("file1", "file2"); + assertThat(multiPart.getFile("file1")).allSatisfy(file -> { + assertThat(file.isFile()) + .isTrue(); + assertThat(file.getOriginalName()) + .isEqualTo("test1.csv"); + assertThat(file.getContentType()) + .isEqualTo("text/csv"); + assertThat(file.getContent()) + .asInstanceOf(InstanceOfAssertFactories.FILE) + .exists() + .content() + .isEqualTo("1,2,3,4"); + }); + assertThat(multiPart.getFile("file2")).allSatisfy(file -> { + assertThat(file.isFile()) + .isTrue(); + assertThat(file.getOriginalName()) + .isEqualTo("test2.csv"); + assertThat(file.getContent()) + .asInstanceOf(InstanceOfAssertFactories.FILE) + .exists() + .content() + .isEqualTo("5,6,7,8"); + }); + } + + @Test + public void cleanUp() throws IOException { + // given + String content = formFile("file1", "test1.csv", "1,2,3,4") + + formFile("file2", "test2.csv", "5,6,7,8") + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue(); + + // when + multiPart.parse(mockRequest, tempDir); + + // then + assertThat(multiPart.getErrors()) + .isEmpty(); + + assertThat(multiPart.getFileParameterNames().asIterator()).toIterable() + .asList() + .containsOnly("file1", "file2"); + assertThat(multiPart.getFile("file1")).allSatisfy(file -> { + assertThat(file.isFile()) + .isTrue(); + assertThat(file.getOriginalName()) + .isEqualTo("test1.csv"); + assertThat(file.getContentType()) + .isEqualTo("text/csv"); + assertThat(file.getContent()).asInstanceOf(InstanceOfAssertFactories.FILE) + .exists() + .content() + .isEqualTo("1,2,3,4"); + }); + assertThat(multiPart.getFile("file2")).allSatisfy(file -> { + assertThat(file.isFile()) + .isTrue(); + assertThat(file.getOriginalName()) + .isEqualTo("test2.csv"); + assertThat(file.getContentType()) + .isEqualTo("text/csv"); + assertThat(file.getContent()) + .asInstanceOf(InstanceOfAssertFactories.FILE) + .exists() + .content() + .isEqualTo("5,6,7,8"); + }); + - List<UploadedFile<File>> uploadedFiles = new ArrayList<>(); - for (Map.Entry<String, List<UploadedFile<File>>> entry : multiPart.uploadedFiles.entrySet()) { ++ List<UploadedFile> uploadedFiles = new ArrayList<>(); ++ for (Map.Entry<String, List<UploadedFile>> entry : multiPart.uploadedFiles.entrySet()) { + uploadedFiles.addAll(entry.getValue()); + } + + // when + multiPart.cleanUp(); + + // then + assertThat(multiPart.uploadedFiles) + .isEmpty(); + assertThat(multiPart.parameters) + .isEmpty(); + assertThat(uploadedFiles).allSatisfy(file -> + assertThat(file.getContent()).asInstanceOf(InstanceOfAssertFactories.FILE) + .doesNotExist() + ); + } + + @Test + public void nonMultiPartUpload() throws IOException { + String content = formFile("file1", "test1.csv", "1,2,3,4") + + formFile("file2", "test2.csv", "5,6,7,8") + + endline + "--" + boundary + "--"; + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + // given + mockRequest.setContentType(""); + + // when + multiPart.parse(mockRequest, tempDir); + + // then + assertThat(multiPart.getErrors()) + .map(LocalizedMessage::getTextKey) + .containsExactly("struts.messages.upload.error.FileUploadContentTypeException"); + + assertThat(multiPart.getFileParameterNames().asIterator()).toIterable() + .asList() + .isEmpty(); + } + + @Test + public void maxSize() throws IOException { + // given + String content = formFile("file1", "test1.csv", "1,2,3,4") + + formFile("file2", "test2.csv", "5,6,7,8") + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue(); + + // when + multiPart.setMaxSize("1"); + multiPart.parse(mockRequest, tempDir); + + // then + assertThat(multiPart.uploadedFiles) + .isEmpty(); + + assertThat(multiPart.getErrors()) + .map(LocalizedMessage::getTextKey) + .containsExactly("struts.messages.upload.error.FileUploadSizeException"); + } + + @Test + public void maxFilesSize() throws IOException { + String content = formFile("file1", "test1.csv", "1,2,3,4") + + formFile("file2", "test2.csv", "5,6,7,8") + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue(); + + multiPart.setMaxFileSize("1"); + multiPart.parse(mockRequest, tempDir); + + assertThat(multiPart.getErrors()) + .map(LocalizedMessage::getTextKey) + .containsExactly("struts.messages.upload.error.FileUploadByteCountLimitException"); + } + + @Test + public void maxFiles() throws IOException { + String content = formFile("file1", "test1.csv", "1,2,3,4") + + formFile("file2", "test2.csv", "5,6,7,8") + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.US_ASCII)); + + assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue(); + + multiPart.setMaxFiles("1"); + multiPart.parse(mockRequest, tempDir); + + assertThat(multiPart.errors) + .map(LocalizedMessage::getTextKey) + .containsExactly("struts.messages.upload.error.FileUploadFileCountLimitException"); + } + + @Test + public void maxStringLength() throws IOException { + String content = formFile("file1", "test1.csv", "1,2,3,4") + + formFile("file2", "test2.csv", "5,6,7,8") + + formField("longText", "very long text") + + formField("shortText", "short text") + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue(); + + multiPart.setMaxStringLength("10"); + multiPart.parse(mockRequest, tempDir); + + assertThat(multiPart.getErrors()) + .map(LocalizedMessage::getTextKey) + .containsExactly("struts.messages.upload.error.parameter.too.long"); + } + + @Test + public void mismatchCharset() throws IOException { + // give + String content = formFile("file1", "test1.csv", "Ł,Ś,Ż,Ó") + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue(); + + // when + mockRequest.setCharacterEncoding(null); + multiPart.setDefaultEncoding(StandardCharsets.ISO_8859_1.name()); + multiPart.parse(mockRequest, tempDir); + + // then + assertThat(multiPart.getErrors()) + .isEmpty(); + + assertThat(multiPart.getFileParameterNames().asIterator()).toIterable() + .asList() + .containsOnly("file1"); + assertThat(multiPart.getFile("file1")).allSatisfy(file -> { + assertThat(file.isFile()) + .isTrue(); + assertThat(file.getOriginalName()) + .isEqualTo("test1.csv"); + assertThat(file.getContentType()) + .isEqualTo("text/csv"); + assertThat(file.getContent()) + .asInstanceOf(InstanceOfAssertFactories.FILE) + .exists() + .content() + .isEqualTo("Ł,Ś,Ż,Ó"); + }); + } + + @Test + public void normalFields() throws IOException { + String content = formFile("file1", "test1.csv", "1,2,3,4") + + formFile("file2", "test2.csv", "5,6,7,8") + + formField("longText", "very long text") + + formField("shortText", "short text") + + formField("multi", "multi1") + + formField("multi", "multi2") + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue(); + + multiPart.parse(mockRequest, tempDir); + + assertThat(multiPart.getErrors()) + .isEmpty(); + + assertThat(multiPart.getParameterNames().asIterator()).toIterable() + .containsOnly("longText", "shortText", "multi"); + assertThat(multiPart.getParameterValues("longText")) + .contains("very long text"); + assertThat(multiPart.getParameterValues("shortText")) + .contains("short text"); + assertThat(multiPart.getParameter("longText")) + .isEqualTo("very long text"); + assertThat(multiPart.getParameter("shortText")) + .isEqualTo("short text"); + assertThat(multiPart.getParameterValues("multi")) + .containsOnly("multi1", "multi2"); + } + + @Test + public void unableParseRequest() throws IOException { + String content = formFile("file1", "test1.csv", "1,2,3,4"); + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue(); + + multiPart.parse(mockRequest, tempDir); + + assertThat(multiPart.getErrors()) + .map(LocalizedMessage::getTextKey) + .containsExactly("struts.messages.upload.error.FileUploadException"); + } + + protected String formFile(String fieldName, String filename, String content) { + return endline + + "--" + boundary + endline + + "Content-Disposition: form-data; name=\"" + fieldName + "\"; filename=\"" + filename + "\"" + + endline + + "Content-Type: text/csv" + + endline + + endline + + content; + } + + protected String formField(String fieldName, String content) { + return endline + + "--" + boundary + endline + + "Content-Disposition: form-data; name=\"" + fieldName + "\"" + + endline + + endline + + content; + } +} diff --cc core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequestTest.java index e24dc133d,5e8f31e4e..c5fe42b1c --- a/core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequestTest.java +++ b/core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequestTest.java @@@ -16,15 -16,14 +16,13 @@@ * specific language governing permissions and limitations * under the License. */ -package org.apache.struts2.components; +package org.apache.struts2.dispatcher.multipart; - import java.io.File; - -import com.opensymphony.xwork2.Action; +public class JakartaMultiPartRequestTest extends AbstractMultiPartRequestTest { -public class PortletAction { - - public String execute() { - return Action.SUCCESS; + @Override - protected AbstractMultiPartRequest<File> createMultipartRequest() { ++ protected AbstractMultiPartRequest createMultipartRequest() { + return new JakartaMultiPartRequest(); } -} +} diff --cc core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java index 0c383e024,51747b147..5b63c7147 --- a/core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java @@@ -51,7 -51,7 +51,7 @@@ import static org.assertj.core.api.Asse */ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase { - private static final UploadedFile<File> EMPTY_FILE = new UploadedFile<>() { - public static final UploadedFile EMPTY_FILE = new UploadedFile() { ++ private static final UploadedFile EMPTY_FILE = new UploadedFile() { @Override public Long length() { return 0L; diff --cc core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java index 57f4650cc,14bb23c36..8dbd173e5 --- a/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java @@@ -53,7 -55,7 +53,7 @@@ import static org.assertj.core.api.Asse */ public class FileUploadInterceptorTest extends StrutsInternalTestCase { - private static final UploadedFile<File> EMPTY_FILE = new UploadedFile<>() { - public static final UploadedFile EMPTY_FILE = new UploadedFile() { ++ private static final UploadedFile EMPTY_FILE = new UploadedFile() { @Override public Long length() { return 0L; @@@ -204,11 -208,7 +204,11 @@@ URL url = ClassLoaderUtil.getResource("log4j2.xml", FileUploadInterceptorTest.class); File file = new File(new URI(url.toString())); assertTrue("log4j2.xml should be in src/test folder", file.exists()); - UploadedFile<File> uploadedFile = StrutsUploadedFile.Builder.create(file) - UploadedFile uploadedFile = StrutsUploadedFile.Builder.create(file).withContentType("text/html").withOriginalName("filename").build(); ++ UploadedFile uploadedFile = StrutsUploadedFile.Builder.create(file) + .withContentType("text/html") + .withOriginalName("filename") + .build(); + boolean notOk = interceptor.acceptFile(validation, uploadedFile, "filename", "text/html", "inputName"); assertFalse(notOk); @@@ -639,19 -612,11 +639,19 @@@ } public static class MyFileupAction extends ActionSupport { + } - private static final long serialVersionUID = 6255238895447968889L; + public static class MyFileUploadAction extends ActionSupport implements UploadedFilesAware { - private List<UploadedFile<File>> uploadedFiles; ++ private List<UploadedFile> uploadedFiles; - // no methods - } + @Override - public void withUploadedFiles(List<UploadedFile<File>> uploadedFiles) { ++ public void withUploadedFiles(List<UploadedFile> uploadedFiles) { + this.uploadedFiles = uploadedFiles; + } - public List<UploadedFile<File>> getUploadFiles() { ++ public List<UploadedFile> getUploadFiles() { + return this.uploadedFiles; + } + } } diff --cc plugins/tiles/src/main/java/org/apache/struts2/tiles/StrutsTilesContainerFactory.java index 8ef7cc561,ed03f8268..def7aab96 --- a/plugins/tiles/src/main/java/org/apache/struts2/tiles/StrutsTilesContainerFactory.java +++ b/plugins/tiles/src/main/java/org/apache/struts2/tiles/StrutsTilesContainerFactory.java @@@ -57,18 -52,26 +52,26 @@@ import org.apache.tiles.ognl.PropertyAc import org.apache.tiles.ognl.ScopePropertyAccessor; import org.apache.tiles.ognl.TilesApplicationContextNestedObjectExtractor; import org.apache.tiles.ognl.TilesContextPropertyAccessorDelegateFactory; + import org.apache.tiles.request.ApplicationContext; + import org.apache.tiles.request.ApplicationResource; + import org.apache.tiles.request.Request; + import org.apache.tiles.request.render.BasicRendererFactory; + import org.apache.tiles.request.render.ChainedDelegateRenderer; import org.apache.tiles.request.render.Renderer; -import javax.el.ArrayELResolver; -import javax.el.BeanELResolver; -import javax.el.CompositeELResolver; -import javax.el.ELResolver; -import javax.el.ListELResolver; -import javax.el.MapELResolver; -import javax.el.ResourceBundleELResolver; -import javax.servlet.jsp.JspFactory; +import jakarta.el.ArrayELResolver; +import jakarta.el.BeanELResolver; +import jakarta.el.CompositeELResolver; +import jakarta.el.ELResolver; +import jakarta.el.ListELResolver; +import jakarta.el.MapELResolver; +import jakarta.el.ResourceBundleELResolver; +import jakarta.servlet.jsp.JspFactory; import java.util.ArrayList; + import java.util.Arrays; import java.util.Collection; + import java.util.Collections; + import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; diff --cc plugins/tiles/src/test/java/org/apache/struts2/tiles/StrutsTilesContainerFactoryTest.java index 000000000,122bfe51d..dbadffa7a mode 000000,100644..100644 --- a/plugins/tiles/src/test/java/org/apache/struts2/tiles/StrutsTilesContainerFactoryTest.java +++ b/plugins/tiles/src/test/java/org/apache/struts2/tiles/StrutsTilesContainerFactoryTest.java @@@ -1,0 -1,128 +1,128 @@@ + /* + * 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.struts2.tiles; + + import org.apache.tiles.api.TilesContainer; + import org.apache.tiles.core.evaluator.AttributeEvaluatorFactory; + import org.apache.tiles.core.evaluator.impl.DirectAttributeEvaluator; + import org.apache.tiles.core.locale.LocaleResolver; + import org.apache.tiles.core.prepare.factory.BasicPreparerFactory; + import org.apache.tiles.core.prepare.factory.PreparerFactory; + import org.apache.tiles.ognl.OGNLAttributeEvaluator; + import org.apache.tiles.request.ApplicationContext; + import org.apache.tiles.request.ApplicationResource; + import org.apache.tiles.request.locale.URLApplicationResource; + import org.apache.tiles.request.render.BasicRendererFactory; + import org.apache.tiles.request.render.ChainedDelegateRenderer; + import org.apache.tiles.request.render.Renderer; + import org.junit.Before; + import org.junit.Test; + -import javax.servlet.ServletContext; -import javax.servlet.jsp.JspFactory; ++import jakarta.servlet.ServletContext; ++import jakarta.servlet.jsp.JspFactory; + import java.util.Collections; + import java.util.List; + import java.util.Objects; + + import static org.junit.Assert.assertEquals; + import static org.junit.Assert.assertTrue; + import static org.mockito.Mockito.mock; + import static org.mockito.Mockito.verify; + import static org.mockito.Mockito.when; + + public class StrutsTilesContainerFactoryTest { + + private StrutsTilesContainerFactory factory; + private ApplicationContext applicationContext; + + @Before + public void setUp() throws Exception { + applicationContext = mock(ApplicationContext.class); + factory = new StrutsTilesContainerFactory(); + } + + @Test + public void getSources() { + ApplicationResource pathResource = new URLApplicationResource( + "/org/apache/tiles/core/config/tiles-defs.xml", + Objects.requireNonNull(getClass().getResource("/org/apache/tiles/core/config/tiles-defs.xml")) + ); + ApplicationResource classpathResource = new URLApplicationResource( + "/org/apache/tiles/core/config/defs1.xml", + Objects.requireNonNull(getClass().getResource("/org/apache/tiles/core/config/defs1.xml")) + ); + when(applicationContext.getInitParams()).thenReturn(Collections.emptyMap()); + when(applicationContext.getResources("/WEB-INF/**/tiles*.xml")).thenReturn(Collections.singleton(pathResource)); + when(applicationContext.getResources("classpath*:META-INF/**/tiles*.xml")).thenReturn(Collections.singleton(classpathResource)); + + List<ApplicationResource> resources = factory.getSources(applicationContext); + assertEquals("The urls list is not two-sized", 2, resources.size()); + assertEquals("The URL is not correct", pathResource, resources.get(0)); + assertEquals("The URL is not correct", classpathResource, resources.get(1)); + } + + @Test + public void createAttributeEvaluatorFactory() { + LocaleResolver resolver = factory.createLocaleResolver(applicationContext); + // explicitly disables support for EL + JspFactory.setDefaultFactory(null); + + AttributeEvaluatorFactory attributeEvaluatorFactory = factory.createAttributeEvaluatorFactory(applicationContext, resolver); + assertTrue("The class of the evaluator is not correct", + attributeEvaluatorFactory.getAttributeEvaluator((String) null) instanceof DirectAttributeEvaluator); + assertTrue("The class of the evaluator is not correct", + attributeEvaluatorFactory.getAttributeEvaluator("S2") instanceof StrutsAttributeEvaluator); + assertTrue("The class of the evaluator is not correct", + attributeEvaluatorFactory.getAttributeEvaluator("OGNL") instanceof OGNLAttributeEvaluator); + assertTrue("The class of the evaluator is not correct", + attributeEvaluatorFactory.getAttributeEvaluator("I18N") instanceof I18NAttributeEvaluator); + assertTrue("The class of the evaluator is not correct", + attributeEvaluatorFactory.getAttributeEvaluator("EL") instanceof DirectAttributeEvaluator); + } + + @Test + public void createPreparerFactory() { + PreparerFactory preparerFactory = factory.createPreparerFactory(applicationContext); + assertTrue("The class of the preparer factory is not correct", preparerFactory instanceof BasicPreparerFactory); + } + + @Test + public void createDefaultAttributeRenderer() { + TilesContainer container = mock(TilesContainer.class); + AttributeEvaluatorFactory attributeEvaluatorFactory = mock(AttributeEvaluatorFactory.class); + BasicRendererFactory rendererFactory = mock(BasicRendererFactory.class); + Renderer stringRenderer = mock(Renderer.class); + Renderer templateRenderer = mock(Renderer.class); + Renderer definitionRenderer = mock(Renderer.class); + + when(rendererFactory.getRenderer("string")).thenReturn(stringRenderer); + when(rendererFactory.getRenderer("template")).thenReturn(templateRenderer); + when(rendererFactory.getRenderer("definition")).thenReturn(definitionRenderer); + when(rendererFactory.getRenderer("freemarker")).thenReturn(definitionRenderer); + + Renderer renderer = factory.createDefaultAttributeRenderer(rendererFactory, applicationContext, container, attributeEvaluatorFactory); + + assertTrue("The default renderer class is not correct", renderer instanceof ChainedDelegateRenderer); + verify(rendererFactory).getRenderer("string"); + verify(rendererFactory).getRenderer("template"); + verify(rendererFactory).getRenderer("definition"); + verify(rendererFactory).getRenderer("freemarker"); + } + + } diff --cc pom.xml index 4de073e0d,79824373b..963b80b1a --- a/pom.xml +++ b/pom.xml @@@ -82,9 -82,9 +82,9 @@@ <modules> <module>bom</module> - <module>jakarta</module> ++ <!--<module>jakarta</module>--> <module>core</module> <module>plugins</module> - <module>bundles</module> <module>apps</module> </modules> @@@ -111,20 -110,16 +111,20 @@@ <!-- dependency versions in alphanumeric order --> <asm.version>9.6</asm.version> - <jackson.version>2.16.1</jackson.version> + <byte-buddy.version>1.14.11</byte-buddy.version> + <freemarker.version>2.3.32</freemarker.version> + <hibernate-validator.version>8.0.1.Final</hibernate-validator.version> + <jackson.version>2.16.0</jackson.version> - <log4j2.version>2.22.1</log4j2.version> + <log4j2.version>2.23.1</log4j2.version> + <maven-surefire-plugin.version>3.2.5</maven-surefire-plugin.version> + <mockito.version>5.8.0</mockito.version> <ognl.version>3.3.4</ognl.version> + <sitemesh.version>2.5.0</sitemesh.version> - <slf4j.version>2.0.11</slf4j.version> + <slf4j.version>2.0.12</slf4j.version> - <spring.platformVersion>5.3.31</spring.platformVersion> + <spring.platformVersion>6.0.13</spring.platformVersion> <tiles.version>3.0.8</tiles.version> <tiles-request.version>1.0.7</tiles-request.version> - <maven-surefire-plugin.version>3.2.5</maven-surefire-plugin.version> - <hibernate-validator.version>6.2.4.Final</hibernate-validator.version> - <freemarker.version>2.3.32</freemarker.version> + <velocity-tools.version>3.1</velocity-tools.version> <!-- Site generation --> <fluido-skin.version>1.9</fluido-skin.version>