This is an automated email from the ASF dual-hosted git repository. reiern70 pushed a commit to branch reiern70/WICKET-7154-native-tomcat-upload in repository https://gitbox.apache.org/repos/asf/wicket.git
commit 5c3ca182cd0ef5423cbd05564cb53b511327fcd4 Author: reiern70 <[email protected]> AuthorDate: Sun May 18 09:25:10 2025 -0500 [WICKET-7154] provide a way to hook into tomcat native multipart processing and at the same time do upload progress reporting. This is needed because with tomcat 11.x tomcat will parse multipart whenever getParameters is called and logic using fileupload2 is very error-prone (it can be rather easily broken on applications if "someone" calls getParameters before wicket form processing takes place). --- pom.xml | 50 +++- wicket-core/pom.xml | 8 + wicket-core/src/main/java/module-info.java | 4 +- .../org/apache/wicket/markup/html/form/Form.java | 43 +++- .../markup/html/form/upload/FileUploadField.java | 4 + .../upload/resource/FileUploadToResourceField.java | 44 +++- .../upload/resource/FileUploadToResourceField.js | 3 +- .../protocol/http/BufferedHttpServletResponse.java | 14 +- .../wicket/protocol/http/WebApplication.java | 22 ++ .../http/mock/MockHttpServletResponse.java | 11 + .../servlet/MultipartServletWebRequestImpl.java | 21 +- .../protocol/http/servlet/ServletPartFileItem.java | 5 +- ...omcatNativeMultipartServletWebRequestImpl.java} | 261 +++------------------ .../TomcatUploadProgressListenerFactory.java | 156 ++++++++++++ .../wicket/protocol/http/servlet/UploadInfo.java | 16 +- .../wicket/settings/ApplicationSettings.java | 25 ++ .../extensions/ajax/AjaxFileDropBehavior.java | 3 +- .../markup/html/form/upload/UploadProgressBar.java | 9 +- .../wicket/migration/MigrateToWicket10Test.java | 2 + 19 files changed, 436 insertions(+), 265 deletions(-) diff --git a/pom.xml b/pom.xml index 9d5db18021..6347656bc9 100644 --- a/pom.xml +++ b/pom.xml @@ -137,8 +137,8 @@ <maven.compiler.showDeprecation>true</maven.compiler.showDeprecation> <maven.compiler.showWarnings>true</maven.compiler.showWarnings> - <maven.compiler.source>17</maven.compiler.source> - <maven.compiler.target>17</maven.compiler.target> + <maven.compiler.source>24</maven.compiler.source> + <maven.compiler.target>24</maven.compiler.target> <!-- Project Versions --> <asm.version>9.8</asm.version> @@ -151,6 +151,7 @@ <commons-fileupload.version>2.0.0-M2</commons-fileupload.version> <commons-io.version>2.19.0</commons-io.version> <commons-lang3.version>3.17.0</commons-lang3.version> + <tomcat.version>12.0.0-M1-SNAPSHOT</tomcat.version> <guice.version>7.0.0</guice.version> <el-impl.version>2.2.1-b05</el-impl.version> <error_prone_annotations.version>2.38.0</error_prone_annotations.version> @@ -338,6 +339,16 @@ <artifactId>commons-lang3</artifactId> <version>${commons-lang3.version}</version> </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-api</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-coyote</artifactId> + <version>${tomcat.version}</version> + </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> @@ -1462,6 +1473,32 @@ </plugins> </build> </profile> + <profile> + <id>java24</id> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-toolchains-plugin</artifactId> + <version>${maven-toolchains-plugin.version}</version> + <executions> + <execution> + <goals> + <goal>toolchain</goal> + </goals> + </execution> + </executions> + <configuration> + <toolchains> + <jdk> + <version>24</version> + </jdk> + </toolchains> + </configuration> + </plugin> + </plugins> + </build> + </profile> <profile> <id>on-jdk-11-or-12</id> <activation> @@ -1471,15 +1508,6 @@ <javadoc.additionalJOption>--no-module-directories</javadoc.additionalJOption> </properties> </profile> - <profile> - <id>on-jdk-early-access</id> - <activation> - <jdk>[24,)</jdk> - </activation> - <properties> - <javadoc.jdk.apidocs.link>https://download.java.net/java/early_access/jdk${java.specification.version}/docs/api/</javadoc.jdk.apidocs.link> - </properties> - </profile> </profiles> <reporting> <plugins> diff --git a/wicket-core/pom.xml b/wicket-core/pom.xml index c961759296..446176d547 100644 --- a/wicket-core/pom.xml +++ b/wicket-core/pom.xml @@ -152,6 +152,14 @@ org.apache.wicket.validation.validator;-noimport:=true <groupId>com.github.openjson</groupId> <artifactId>openjson</artifactId> </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-api</artifactId> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-coyote</artifactId> + </dependency> <dependency> <groupId>org.apache.wicket</groupId> <artifactId>wicket-request</artifactId> diff --git a/wicket-core/src/main/java/module-info.java b/wicket-core/src/main/java/module-info.java index e7bd7b3a4b..b9c9598b00 100644 --- a/wicket-core/src/main/java/module-info.java +++ b/wicket-core/src/main/java/module-info.java @@ -30,8 +30,10 @@ module org.apache.wicket.core { requires org.danekja.jdk.serializable.functional; requires com.github.openjson; requires static org.bouncycastle.provider; + requires org.apache.tomcat.coyote; + requires org.apache.tomcat.api; - provides org.apache.wicket.IInitializer with org.apache.wicket.Initializer; + provides org.apache.wicket.IInitializer with org.apache.wicket.Initializer; provides org.apache.wicket.resource.FileSystemPathService with org.apache.wicket.resource.FileSystemJarPathService; uses org.apache.wicket.IInitializer; diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/Form.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/Form.java index 0a9326ad7c..c6a4a3faca 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/Form.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/Form.java @@ -29,6 +29,7 @@ import org.apache.commons.fileupload2.core.FileUploadException; import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException; import org.apache.commons.fileupload2.core.FileUploadSizeException; import org.apache.commons.fileupload2.core.FileUploadFileCountLimitException; +import org.apache.wicket.Application; import org.apache.wicket.Component; import org.apache.wicket.IGenericComponent; import org.apache.wicket.IRequestListener; @@ -50,6 +51,7 @@ import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.protocol.http.servlet.MultipartServletWebRequest; import org.apache.wicket.protocol.http.servlet.ServletWebRequest; +import org.apache.wicket.protocol.http.servlet.TomcatUploadProgressListenerFactory; import org.apache.wicket.request.IRequestParameters; import org.apache.wicket.request.Request; import org.apache.wicket.request.Response; @@ -278,6 +280,11 @@ public class Form<T> extends WebMarkupContainer /** True if the form has enctype of multipart/form-data */ private short multiPart = 0; + /** + * The ID of the file upload. + */ + private String uploadId; + /** * A user has explicitly called {@link #setMultiPart(boolean)} with value {@code true} forcing * it to be true @@ -1451,7 +1458,7 @@ public class Form<T> extends WebMarkupContainer { ServletWebRequest request = (ServletWebRequest)getRequest(); final MultipartServletWebRequest multipartWebRequest = request.newMultipartWebRequest( - getMaxSize(), getPage().getId()); + getMaxSize(), getUploadId()); multipartWebRequest.setFileMaxSize(getFileMaxSize()); multipartWebRequest.setFileCountMax(getFileCountMax()); multipartWebRequest.parseFileParts(); @@ -1477,6 +1484,37 @@ public class Form<T> extends WebMarkupContainer return true; } + /** + * + * @return The upload ID. + */ + public final String getUploadId() + { + if (uploadId != null) + { + return uploadId; + } + uploadId = computeUploadId(getPage()); + return uploadId; + } + + /** + * Computes the upload ID. + * + * @param page The {@link Page} + * @return the upload ID. + */ + public static String computeUploadId(Page page) { + if (Application.get().getApplicationSettings().isUseTomcatNativeFileUpload()) { + String uploadId = TomcatUploadProgressListenerFactory.getUploadId(); + if (uploadId != null) { + return uploadId; + } + throw new WicketRuntimeException("If you are using Tomcat for uploading files you should have registered a TomcatUploadProgressListenerFactory"); + } + return page.getId(); + } + /** * The default message may look like ".. may not exceed 10240 Bytes..". Which is ok, but * sometimes you may want something like "10KB". By subclassing this method you may replace @@ -1669,6 +1707,9 @@ public class Form<T> extends WebMarkupContainer */ protected CharSequence getActionUrl() { + if (isMultiPart()) { + return urlForListener(new PageParameters().add("uploadId", getUploadId())); + } return urlForListener(new PageParameters()); } diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/FileUploadField.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/FileUploadField.java index 3fc87d9b8c..d39abc2ae8 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/FileUploadField.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/FileUploadField.java @@ -159,6 +159,10 @@ public class FileUploadField extends FormComponent<List<FileUpload>> return getFileUploads(); } + public String getUploadId() { + return getMarkupId(); + } + @Override public boolean isMultiPart() { diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.java index fbff4d91e0..b6de730a80 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.java @@ -27,6 +27,8 @@ import java.util.Map; import java.util.Objects; import java.util.UUID; import org.apache.commons.io.IOUtils; +import org.apache.wicket.Application; +import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.AjaxUtils; @@ -38,6 +40,7 @@ import org.apache.wicket.markup.html.form.upload.FileUpload; import org.apache.wicket.markup.html.form.upload.FileUploadField; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; +import org.apache.wicket.protocol.http.servlet.TomcatUploadProgressListenerFactory; import org.apache.wicket.request.Request; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.mapper.parameter.PageParameters; @@ -59,6 +62,8 @@ public abstract class FileUploadToResourceField extends FileUploadField { private static final Logger LOGGER = LoggerFactory.getLogger(FileUploadToResourceField.class); + private String uploadId; + /** * Info regarding an upload. */ @@ -180,7 +185,7 @@ public abstract class FileUploadToResourceField extends FileUploadField // at this point files were stored at the server side by resource // UploadFieldId acts as a discriminator at application level // so that uploaded files are isolated. - uploadInfo.setFile(fileManager().getFile(getUploadFieldId(), uploadInfo.clientFileName)); + uploadInfo.setFile(fileManager().getFile(getUploadId(), uploadInfo.clientFileName)); } return fileInfos; } @@ -194,9 +199,9 @@ public abstract class FileUploadToResourceField extends FileUploadField protected abstract IUploadsFileManager fileManager(); /* - This is an application unique ID assigned to upload field. + This is an application wide unique ID assigned to upload field. */ - protected abstract String getUploadFieldId(); + protected abstract String getUploadId(); protected abstract List<UploadInfo> getFileUploadInfos(); } @@ -235,9 +240,9 @@ public abstract class FileUploadToResourceField extends FileUploadField } @Override - protected String getUploadFieldId() + protected String getUploadId() { - return FileUploadToResourceField.this.getMarkupId(); + return FileUploadToResourceField.this.getUploadId(); } @Override @@ -357,13 +362,13 @@ public abstract class FileUploadToResourceField extends FileUploadField return "WRFUF_" + UUID.randomUUID().toString().replace("-", "_"); } - @Override public void renderHead(IHeaderResponse response) { CoreLibrariesContributor.contributeAjax(getApplication(), response); response.render(JavaScriptHeaderItem.forReference(JS)); JSONObject jsonObject = new JSONObject(); jsonObject.put("inputName", getMarkupId()); + jsonObject.put("uploadId", getUploadId()); jsonObject.put("resourceUrl", urlFor(getFileUploadResourceReference(), new PageParameters()).toString()); jsonObject.put("ajaxCallBackUrl", ajaxBehavior.getCallbackUrl()); jsonObject.put("maxSize", getMaxSize().bytes()); @@ -382,6 +387,33 @@ public abstract class FileUploadToResourceField extends FileUploadField + getClientSideUploadErrorCallBack() + ");")); } + /** + * @return the unique upload ID. + */ + public final String getUploadId() { + if (uploadId != null) + { + return uploadId; + } + uploadId = computeUploadId(); + return uploadId; + } + + /** + * Comoputes the upload ID. + * @return + */ + private String computeUploadId() { + if (Application.get().getApplicationSettings().isUseTomcatNativeFileUpload()) { + String uploadId = TomcatUploadProgressListenerFactory.getUploadId(); + if (uploadId != null) { + return uploadId; + } + throw new WicketRuntimeException("If you are using Tomcat for uploading files you should have registered a TomcatUploadProgressListenerFactory"); + } + return getMarkupId(); + } + /** * Sets maximum size of each file in upload request. * diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.js b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.js index 815e66912f..85b0621f89 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.js +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.js @@ -27,8 +27,9 @@ { this.settings = settings; this.inputName = settings.inputName; + this.uploadId = settings.uploadId; this.input = document.getElementById(this.inputName); - this.resourceUrl = settings.resourceUrl + "?uploadId=" + this.inputName + "&maxSize=" + this.settings.maxSize; + this.resourceUrl = settings.resourceUrl + "?uploadId=" + this.uploadId + "&maxSize=" + this.settings.maxSize; if (this.settings.fileMaxSize != null) { this.resourceUrl = this.resourceUrl + "&fileMaxSize=" + this.settings.fileMaxSize; } diff --git a/wicket-core/src/main/java/org/apache/wicket/protocol/http/BufferedHttpServletResponse.java b/wicket-core/src/main/java/org/apache/wicket/protocol/http/BufferedHttpServletResponse.java index a7a971b496..16e2f8d5f4 100644 --- a/wicket-core/src/main/java/org/apache/wicket/protocol/http/BufferedHttpServletResponse.java +++ b/wicket-core/src/main/java/org/apache/wicket/protocol/http/BufferedHttpServletResponse.java @@ -81,7 +81,7 @@ class BufferedHttpServletResponse implements HttpServletResponse } /** - * @see jakarta.servlet.http.HttpServletResponse#addCookie(javax.servlet.http.Cookie) + * @see jakarta.servlet.http.HttpServletResponse#addCookie(jakarta.servlet.http.Cookie) */ @Override public void addCookie(Cookie cookie) @@ -158,6 +158,18 @@ class BufferedHttpServletResponse implements HttpServletResponse redirect = location; } + @Override + public void sendRedirect(String location, int sc, boolean clearBuffer) throws IOException { + isOpen(); + realResponse.sendRedirect(location); + } + + @Override + public void sendEarlyHints() { + isOpen(); + realResponse.sendEarlyHints(); + } + /** * @return The redirect url */ diff --git a/wicket-core/src/main/java/org/apache/wicket/protocol/http/WebApplication.java b/wicket-core/src/main/java/org/apache/wicket/protocol/http/WebApplication.java index 687ae3bfb9..b0a661181e 100644 --- a/wicket-core/src/main/java/org/apache/wicket/protocol/http/WebApplication.java +++ b/wicket-core/src/main/java/org/apache/wicket/protocol/http/WebApplication.java @@ -24,6 +24,8 @@ import java.util.LinkedList; import java.util.Locale; import java.util.function.Function; +import org.apache.commons.fileupload2.core.FileItemFactory; +import org.apache.commons.fileupload2.core.FileUploadException; import org.apache.wicket.Application; import org.apache.wicket.Page; import org.apache.wicket.RuntimeConfigurationType; @@ -56,8 +58,10 @@ import org.apache.wicket.markup.html.pages.PageExpiredErrorPage; import org.apache.wicket.markup.resolver.AutoLinkResolver; import org.apache.wicket.protocol.http.servlet.AbstractRequestWrapperFactory; import org.apache.wicket.protocol.http.servlet.FilterFactoryManager; +import org.apache.wicket.protocol.http.servlet.MultipartServletWebRequest; import org.apache.wicket.protocol.http.servlet.ServletWebRequest; import org.apache.wicket.protocol.http.servlet.ServletWebResponse; +import org.apache.wicket.protocol.http.servlet.TomcatNativeMultipartServletWebRequestImpl; import org.apache.wicket.request.IRequestHandler; import org.apache.wicket.request.IRequestMapper; import org.apache.wicket.request.Request; @@ -79,6 +83,7 @@ import org.apache.wicket.util.file.FileCleaner; import org.apache.wicket.util.file.IFileCleaner; import org.apache.wicket.util.file.Path; import org.apache.wicket.util.lang.Args; +import org.apache.wicket.util.lang.Bytes; import org.apache.wicket.util.lang.PackageName; import org.apache.wicket.util.string.Strings; import org.apache.wicket.util.watch.IModificationWatcher; @@ -561,6 +566,23 @@ public abstract class WebApplication extends Application */ public WebRequest newWebRequest(HttpServletRequest servletRequest, final String filterPath) { + if (getApplicationSettings().isUseTomcatNativeFileUpload()) + { + return new ServletWebRequest(servletRequest, filterPath) + { + @Override + public MultipartServletWebRequest newMultipartWebRequest(Bytes maxSize, String upload) throws FileUploadException + { + return new TomcatNativeMultipartServletWebRequestImpl(getContainerRequest(), getFilterPrefix(), maxSize); + } + + @Override + public MultipartServletWebRequest newMultipartWebRequest(Bytes maxSize, String upload, FileItemFactory factory) throws FileUploadException + { + return new TomcatNativeMultipartServletWebRequestImpl(getContainerRequest(), getFilterPrefix(), maxSize); + } + }; + } return new ServletWebRequest(servletRequest, filterPath); } diff --git a/wicket-core/src/main/java/org/apache/wicket/protocol/http/mock/MockHttpServletResponse.java b/wicket-core/src/main/java/org/apache/wicket/protocol/http/mock/MockHttpServletResponse.java index 684dde00a8..9b122ede07 100755 --- a/wicket-core/src/main/java/org/apache/wicket/protocol/http/mock/MockHttpServletResponse.java +++ b/wicket-core/src/main/java/org/apache/wicket/protocol/http/mock/MockHttpServletResponse.java @@ -554,6 +554,17 @@ public class MockHttpServletResponse implements HttpServletResponse, IMetaDataBu status = HttpServletResponse.SC_FOUND; } + @Override + public void sendRedirect(String location, int sc, boolean clearBuffer) throws IOException { + redirectLocation = location; + status = sc; + } + + @Override + public void sendEarlyHints() { + + } + /** * Method ignored. * diff --git a/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/MultipartServletWebRequestImpl.java b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/MultipartServletWebRequestImpl.java index 6ae48f25a6..6ba2070a63 100644 --- a/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/MultipartServletWebRequestImpl.java +++ b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/MultipartServletWebRequestImpl.java @@ -386,10 +386,15 @@ public class MultipartServletWebRequestImpl extends MultipartServletWebRequest * @param totalBytes */ protected void onUploadStarted(int totalBytes) + { + onUploadStarted(getContainerRequest(), upload, totalBytes); + } + + public static void onUploadStarted(HttpServletRequest request, String upload, long totalBytes) { UploadInfo info = new UploadInfo(totalBytes); - setUploadInfo(getContainerRequest(), upload, info); + setUploadInfo(request, upload, info); } /** @@ -400,15 +405,18 @@ public class MultipartServletWebRequestImpl extends MultipartServletWebRequest */ protected void onUploadUpdate(int bytesUploaded, int total) { - HttpServletRequest request = getContainerRequest(); + onUploadUpdate(getContainerRequest(), upload, bytesUploaded, total); + } + + public static void onUploadUpdate(HttpServletRequest request, String upload, long bytesUploaded, long total) + { UploadInfo info = getUploadInfo(request, upload); if (info == null) { throw new IllegalStateException( - "could not find UploadInfo object in session which should have been set when uploaded started"); + "could not find UploadInfo object in session which should have been set when uploaded started"); } info.setBytesUploaded(bytesUploaded); - setUploadInfo(request, upload, info); } @@ -420,6 +428,11 @@ public class MultipartServletWebRequestImpl extends MultipartServletWebRequest clearUploadInfo(getContainerRequest(), upload); } + public static void onUploadCompleted(HttpServletRequest request, String upload) + { + clearUploadInfo(request, upload); + } + /** * An {@link InputStream} that updates total number of bytes read * diff --git a/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/ServletPartFileItem.java b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/ServletPartFileItem.java index e09f7dd07f..bc83c2cb0f 100644 --- a/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/ServletPartFileItem.java +++ b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/ServletPartFileItem.java @@ -16,7 +16,6 @@ */ package org.apache.wicket.protocol.http.servlet; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -65,6 +64,10 @@ class ServletPartFileItem implements FileItem return part.getInputStream(); } + public Part getPart() { + return part; + } + @Override public String getContentType() { diff --git a/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/MultipartServletWebRequestImpl.java b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/TomcatNativeMultipartServletWebRequestImpl.java similarity index 58% copy from wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/MultipartServletWebRequestImpl.java copy to wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/TomcatNativeMultipartServletWebRequestImpl.java index 6ae48f25a6..aab1d0bcdd 100644 --- a/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/MultipartServletWebRequestImpl.java +++ b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/TomcatNativeMultipartServletWebRequestImpl.java @@ -17,43 +17,41 @@ package org.apache.wicket.protocol.http.servlet; import java.io.IOException; -import java.io.InputStream; import java.nio.charset.Charset; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.Part; -import org.apache.commons.fileupload2.core.AbstractFileUpload; import org.apache.commons.fileupload2.core.FileItem; import org.apache.commons.fileupload2.core.FileItemFactory; import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException; import org.apache.commons.fileupload2.core.FileUploadException; -import org.apache.commons.fileupload2.core.DiskFileItemFactory; import org.apache.commons.fileupload2.jakarta.servlet5.JakartaServletFileUpload; -import org.apache.commons.fileupload2.jakarta.servlet5.JakartaServletRequestContext; import org.apache.wicket.Application; import org.apache.wicket.WicketRuntimeException; -import org.apache.wicket.util.file.FileCleanerTrackerAdapter; import org.apache.wicket.util.lang.Args; import org.apache.wicket.util.lang.Bytes; import org.apache.wicket.util.string.StringValue; import org.apache.wicket.util.value.ValueMap; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.Part; /** - * Servlet specific WebRequest subclass for multipart content uploads. + * Servlet-specific WebRequest subclass for multipart content uploads. Aimed to be used with tomcat 11+. This in + * combination with {@link TomcatUploadProgressListenerFactory} and the setting {@link org.apache.wicket.settings.ApplicationSettings#setUseTomcatNativeFileUpload(boolean)} + * allows to use tomcat's native multipart processing with progress reporting. * * @author Jonathan Locke * @author Eelco Hillenius * @author Cameron Braid * @author Ate Douma * @author Igor Vaynberg (ivaynberg) + * @author Ernesto Reinaldo Barreiro (reiern70) */ -public class MultipartServletWebRequestImpl extends MultipartServletWebRequest +public class TomcatNativeMultipartServletWebRequestImpl extends MultipartServletWebRequest { /** Map of file items. */ private final Map<String, List<FileItem>> files; @@ -61,44 +59,6 @@ public class MultipartServletWebRequestImpl extends MultipartServletWebRequest /** Map of parameters. */ private final ValueMap parameters; - private final String upload; - private final FileItemFactory fileItemFactory; - - /** - * total bytes uploaded (downloaded from server's pov) so far. used for upload notifications - */ - private int bytesUploaded; - - /** content length cache, used for upload notifications */ - private int totalBytes; - - /** - * Constructor. - * - * This constructor will use {@link DiskFileItemFactory} to store uploads. - * - * @param request - * the servlet request - * @param filterPrefix - * prefix to wicket filter mapping - * @param maxSize - * the maximum size allowed for this request - * @param upload - * upload identifier for {@link UploadInfo} - * @throws FileUploadException - * Thrown if something goes wrong with upload - */ - public MultipartServletWebRequestImpl(HttpServletRequest request, String filterPrefix, - Bytes maxSize, String upload) throws FileUploadException - { - this(request, filterPrefix, maxSize, upload, - DiskFileItemFactory.builder() - .setFileCleaningTracker(new FileCleanerTrackerAdapter(Application.get() - .getResourceSettings() - .getFileCleaner())) - .get()); - } - /** * Constructor * @@ -108,22 +68,12 @@ public class MultipartServletWebRequestImpl extends MultipartServletWebRequest * prefix to wicket filter mapping * @param maxSize * the maximum size allowed for this request - * @param upload - * upload identifier for {@link UploadInfo} - * @param factory - * {@link DiskFileItemFactory} to use when creating file items used to represent - * uploaded files - * @throws FileUploadException - * Thrown if something goes wrong with upload */ - public MultipartServletWebRequestImpl(HttpServletRequest request, String filterPrefix, - Bytes maxSize, String upload, FileItemFactory factory) throws FileUploadException + public TomcatNativeMultipartServletWebRequestImpl(HttpServletRequest request, String filterPrefix, + Bytes maxSize) { super(request, filterPrefix); - Args.notNull(upload, "upload"); - this.upload = upload; - this.fileItemFactory = factory; parameters = new ValueMap(); files = new HashMap<>(); @@ -156,43 +106,8 @@ public class MultipartServletWebRequestImpl extends MultipartServletWebRequest encoding = Application.get().getRequestCycleSettings().getResponseRequestEncoding(); } - AbstractFileUpload fileUpload = newFileUpload(encoding); - List<FileItem> items; - - if (wantUploadProgressUpdates()) - { - JakartaServletRequestContext ctx = new JakartaServletRequestContext(request) - { - @Override - public InputStream getInputStream() throws IOException - { - return new CountingInputStream(super.getInputStream()); - } - }; - totalBytes = request.getContentLength(); - - onUploadStarted(totalBytes); - try - { - items = fileUpload.parseRequest(ctx); - } - finally - { - onUploadCompleted(); - } - } - else - { - // try to parse the file uploads by using Apache Commons FileUpload APIs - // because they are feature richer (e.g. progress updates, cleaner) - items = fileUpload.parseRequest(new JakartaServletRequestContext(request)); - if (items.isEmpty()) - { - // fallback to Servlet 3.0 APIs - items = readServlet3Parts(request); - } - } + List<FileItem> items = readServletParts(request); // Loop through items for (final FileItem item : items) @@ -236,17 +151,19 @@ public class MultipartServletWebRequestImpl extends MultipartServletWebRequest } /** - * Reads the uploads' parts by using Servlet 3.0 APIs. + * Reads the uploads' parts by using Servlet APIs. This is meant to be used with tomcat 11+; * - * <strong>Note</strong>: By using Servlet 3.0 APIs the application won't be able to use - * upload progress updates. + * <strong>Note</strong>: Mind that in to get file upload with prpgres working you need to: + * + * 1) register a {@link TomcatUploadProgressListenerFactory} + * 2) set to true {@link org.apache.wicket.settings.ApplicationSettings#setUseTomcatNativeFileUpload} * * @param request * The http request with the upload data * @return A list of {@link FileItem}s * @throws FileUploadException */ - private List<FileItem> readServlet3Parts(HttpServletRequest request) throws FileUploadException + private List<FileItem> readServletParts(HttpServletRequest request) throws FileUploadException { List<FileItem> itemsFromParts = new ArrayList<>(); try @@ -256,7 +173,15 @@ public class MultipartServletWebRequestImpl extends MultipartServletWebRequest { for (Part part : parts) { - FileItem fileItem = new ServletPartFileItem(part); + FileItem fileItem = new ServletPartFileItem(part) { + @Override + public ServletPartFileItem write(Path path) throws IOException { + // we need to override this because supper method only uses file name and file is + // not stored. + getPart().write(path.toFile().getAbsolutePath()); + return this; + } + }; itemsFromParts.add(fileItem); } } @@ -267,36 +192,6 @@ public class MultipartServletWebRequestImpl extends MultipartServletWebRequest return itemsFromParts; } - /** - * Factory method for creating new instances of AbstractFileUpload - * - * @param encoding - * The encoding to use while reading the data - * @return A new instance of AbstractFileUpload - */ - protected AbstractFileUpload newFileUpload(String encoding) { - // Configure the factory here, if desired. - JakartaServletFileUpload fileUpload = new JakartaServletFileUpload(fileItemFactory); - - // set encoding specifically when we found it - if (encoding != null) - { - Charset charset = Charset.forName(encoding); - fileUpload.setHeaderCharset(charset); - } - - fileUpload.setSizeMax(getMaxSize().bytes()); - - Bytes fileMaxSize = getFileMaxSize(); - if (fileMaxSize != null) { - fileUpload.setFileSizeMax(fileMaxSize.bytes()); - } - - fileUpload.setFileCountMax(getFileCountMax()); - - return fileUpload; - } - /** * Adds a parameter to the parameters value map * @@ -369,106 +264,6 @@ public class MultipartServletWebRequestImpl extends MultipartServletWebRequest return res; } - /** - * Subclasses that want to receive upload notifications should return true. By default, it takes - * the value from {@link org.apache.wicket.settings.ApplicationSettings#isUploadProgressUpdatesEnabled()}. - * - * @return true if upload status update event should be invoked - */ - protected boolean wantUploadProgressUpdates() - { - return Application.get().getApplicationSettings().isUploadProgressUpdatesEnabled(); - } - - /** - * Upload start callback - * - * @param totalBytes - */ - protected void onUploadStarted(int totalBytes) - { - UploadInfo info = new UploadInfo(totalBytes); - - setUploadInfo(getContainerRequest(), upload, info); - } - - /** - * Upload status update callback - * - * @param bytesUploaded - * @param total - */ - protected void onUploadUpdate(int bytesUploaded, int total) - { - HttpServletRequest request = getContainerRequest(); - UploadInfo info = getUploadInfo(request, upload); - if (info == null) - { - throw new IllegalStateException( - "could not find UploadInfo object in session which should have been set when uploaded started"); - } - info.setBytesUploaded(bytesUploaded); - - setUploadInfo(request, upload, info); - } - - /** - * Upload completed callback - */ - protected void onUploadCompleted() - { - clearUploadInfo(getContainerRequest(), upload); - } - - /** - * An {@link InputStream} that updates total number of bytes read - * - * @author Igor Vaynberg (ivaynberg) - */ - private class CountingInputStream extends InputStream - { - - private final InputStream in; - - /** - * Constructs a new CountingInputStream. - * - * @param in - * InputStream to delegate to - */ - public CountingInputStream(InputStream in) - { - this.in = in; - } - - @Override - public int read() throws IOException - { - int read = in.read(); - bytesUploaded += (read < 0) ? 0 : 1; - onUploadUpdate(bytesUploaded, totalBytes); - return read; - } - - @Override - public int read(byte[] b) throws IOException - { - int read = in.read(b); - bytesUploaded += (read < 0) ? 0 : read; - onUploadUpdate(bytesUploaded, totalBytes); - return read; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException - { - int read = in.read(b, off, len); - bytesUploaded += (read < 0) ? 0 : read; - onUploadUpdate(bytesUploaded, totalBytes); - return read; - } - - } @Override public MultipartServletWebRequest newMultipartWebRequest(Bytes maxSize, String upload) @@ -505,7 +300,7 @@ public class MultipartServletWebRequestImpl extends MultipartServletWebRequest return this; } - private static final String SESSION_KEY = MultipartServletWebRequestImpl.class.getName(); + private static final String SESSION_KEY = TomcatNativeMultipartServletWebRequestImpl.class.getName(); private static String getSessionKey(String upload) { diff --git a/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/TomcatUploadProgressListenerFactory.java b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/TomcatUploadProgressListenerFactory.java new file mode 100644 index 0000000000..1150b18a5f --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/TomcatUploadProgressListenerFactory.java @@ -0,0 +1,156 @@ +/* + * 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.wicket.protocol.http.servlet; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.tomcat.util.http.fileupload.ProgressListener; +import org.apache.tomcat.util.http.fileupload.ProgressListenerFactory; +import org.apache.wicket.Application; +import org.apache.wicket.request.Url; +import org.apache.wicket.util.lang.Args; +import jakarta.servlet.http.HttpServletRequest; + +/** + * A {@link ProgressListenerFactory} that allows reporting upload progress but uses tomcat native multipart machinery. + */ +public class TomcatUploadProgressListenerFactory implements ProgressListenerFactory +{ + /** + * Interface used to generate upload IDs. These IDs connect Wicket UI with tomcat progress reporting + */ + public interface IUploadIdGenerator + { + /** + * @return The unique ID for the upload. + */ + String newUploadId(); + } + + private static class AppUploadIdGenerator implements IUploadIdGenerator + { + + private static final AppUploadIdGenerator instance = new AppUploadIdGenerator(); + + public static AppUploadIdGenerator getInstance() + { + return instance; + } + + private final AtomicLong counter = new AtomicLong(); + + private AppUploadIdGenerator() + { + } + + @Override + public String newUploadId() { + return "upload-" + counter.incrementAndGet(); + } + } + + /** + * Progress listener to be called by Tomcat to report multipart (file upload) progress.- + */ + public static class WicketProgressListener implements ProgressListener + { + + private final String uploadId; + private final HttpServletRequest servletRequest; + private final long totalBytes; + + private WicketProgressListener(String uploadId, HttpServletRequest servletRequest) + { + Args.notEmpty(uploadId, "uploadId"); + this.uploadId = uploadId; + Args.notNull(servletRequest, "servletRequest"); + this.servletRequest = servletRequest; + this.totalBytes = servletRequest.getContentLength(); + } + + @Override + public void uploadStarted() + { + MultipartServletWebRequestImpl.onUploadStarted(servletRequest, this.uploadId, this.totalBytes); + } + + @Override + public void update(long pBytesRead, long pContentLength, int pItems) + { + MultipartServletWebRequestImpl.onUploadUpdate(servletRequest, uploadId, pBytesRead, pContentLength); + } + + @Override + public void uploadFinished() + { + MultipartServletWebRequestImpl.onUploadCompleted(servletRequest, this.uploadId); + } + } + + private static IUploadIdGenerator iUploadIdGenerator = AppUploadIdGenerator.getInstance(); + + + public TomcatUploadProgressListenerFactory() + { + // constructor for reflection-based instantiation + } + + + @Override + public ProgressListener newProgressListener(HttpServletRequest servletRequest) { + // there is no need to check if we are in multipart request + // we are because tomcat will only call this in the context of a + // multipart request. + if (wantUploadProgressUpdates()) + { + // we extract the uploadId from the request + Url url = Url.parse(servletRequest.getRequestURL() + "?" + servletRequest.getQueryString()); + Optional<Url.QueryParameter> queryParameter = url.getQueryParameters().stream().filter( + queryParameter1 -> queryParameter1.getName().equals("uploadId")).findFirst(); + if (queryParameter.isPresent()) + { + String uploadId = queryParameter.get().getValue(); + return new WicketProgressListener(uploadId, servletRequest); + } + } + return null; + } + + protected boolean wantUploadProgressUpdates() + { + return Application.get().getApplicationSettings().isUploadProgressUpdatesEnabled(); + } + + public static String getUploadId() { + if (Application.get().getApplicationSettings().isUseTomcatNativeFileUpload()) + { + return iUploadIdGenerator.newUploadId(); + } + return null; + } + + /** + * Allows setting the {@link IUploadIdGenerator} + * + * @param iUploadIdGenerator {@link IUploadIdGenerator} + */ + public static void setUploadIdGenerator(IUploadIdGenerator iUploadIdGenerator) + { + Args.notNull(iUploadIdGenerator, "iUploadIdGenerator"); + TomcatUploadProgressListenerFactory.iUploadIdGenerator = iUploadIdGenerator; + } +} diff --git a/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/UploadInfo.java b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/UploadInfo.java index c52831659d..bada6573fc 100644 --- a/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/UploadInfo.java +++ b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/UploadInfo.java @@ -39,6 +39,7 @@ public class UploadInfo implements IClusterable /** * @param totalBytes + * @deprecated We need to keep it for backwards compatibility */ public UploadInfo(final int totalBytes) { @@ -46,6 +47,15 @@ public class UploadInfo implements IClusterable this.totalBytes = totalBytes; } + /** + * @param totalBytes + */ + public UploadInfo(final long totalBytes) + { + timeStarted = System.currentTimeMillis(); + this.totalBytes = totalBytes; + } + /** * @return bytes uploaded so far */ @@ -56,8 +66,8 @@ public class UploadInfo implements IClusterable /** * Sets bytes uploaded so far - * - * @param bytesUploaded + * + * @param bytesUploaded The number of bytes uploaded */ public void setBytesUploaded(final long bytesUploaded) { @@ -65,7 +75,7 @@ public class UploadInfo implements IClusterable } /** - * @return human readable string of bytes uploaded so far + * @return human-readable string of bytes uploaded so far */ public String getBytesUploadedString() { diff --git a/wicket-core/src/main/java/org/apache/wicket/settings/ApplicationSettings.java b/wicket-core/src/main/java/org/apache/wicket/settings/ApplicationSettings.java index 3d5141cc38..b5fa075770 100644 --- a/wicket-core/src/main/java/org/apache/wicket/settings/ApplicationSettings.java +++ b/wicket-core/src/main/java/org/apache/wicket/settings/ApplicationSettings.java @@ -63,6 +63,8 @@ public class ApplicationSettings private boolean uploadProgressUpdatesEnabled = false; + private boolean useTomcatNativeFileUpload = false; + private IFeedbackMessageFilter feedbackMessageCleanupFilter = new DefaultCleanupFeedbackMessageFilter(); /** @@ -126,6 +128,16 @@ public class ApplicationSettings return uploadProgressUpdatesEnabled; } + /** + * Gets whether wicket is using Tomcat 11+ native upload machinery or not. + * + * @return if true, Wicket will use tomcat native upload machinery + */ + public boolean isUseTomcatNativeFileUpload() + { + return useTomcatNativeFileUpload; + } + /** * Sets the access denied page class. The class must be bookmarkable and must extend Page. * @@ -224,6 +236,19 @@ public class ApplicationSettings return this; } + + /** + * Sets whether wicket should use Tomcat (11+) native file upload + * + * @param useTomcatNativeFileUpload + * if true, Wicket will use tomcat native file upload + * @return {@code this} object for chaining + */ + public ApplicationSettings setUseTomcatNativeFileUpload(boolean useTomcatNativeFileUpload) { + this.useTomcatNativeFileUpload = useTomcatNativeFileUpload; + return this; + } + /** * Throws an IllegalArgumentException if the given class is not a subclass of Page. * diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/AjaxFileDropBehavior.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/AjaxFileDropBehavior.java index b8a76b0020..47ae6898a2 100644 --- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/AjaxFileDropBehavior.java +++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/AjaxFileDropBehavior.java @@ -31,6 +31,7 @@ import org.apache.wicket.ajax.attributes.AjaxRequestAttributes.Method; import org.apache.wicket.core.util.string.CssUtils; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.JavaScriptHeaderItem; +import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.form.upload.FileUpload; import org.apache.wicket.protocol.http.servlet.MultipartServletWebRequest; import org.apache.wicket.protocol.http.servlet.ServletWebRequest; @@ -121,7 +122,7 @@ public class AjaxFileDropBehavior extends AjaxEventBehavior { ServletWebRequest request = (ServletWebRequest)getComponent().getRequest(); final MultipartServletWebRequest multipartWebRequest = request - .newMultipartWebRequest(getMaxSize(), getComponent().getPage().getId()); + .newMultipartWebRequest(getMaxSize(), Form.computeUploadId(getComponent().getPage())); multipartWebRequest.setFileMaxSize(getFileMaxSize()); multipartWebRequest.setFileCountMax(getFileCountMax()); multipartWebRequest.parseFileParts(); diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/form/upload/UploadProgressBar.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/form/upload/UploadProgressBar.java index 52e4d29a55..16b7c44dba 100644 --- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/form/upload/UploadProgressBar.java +++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/form/upload/UploadProgressBar.java @@ -21,6 +21,7 @@ import java.util.Formatter; import org.apache.wicket.Application; import org.apache.wicket.IInitializer; import org.apache.wicket.MarkupContainer; +import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.core.request.handler.IPartialPageRequestHandler; import org.apache.wicket.markup.head.CssHeaderItem; import org.apache.wicket.markup.head.IHeaderResponse; @@ -259,8 +260,12 @@ public class UploadProgressBar extends Panel final String status = new StringResourceModel(RESOURCE_STARTING, this, null).getString(); - CharSequence url = form != null ? urlFor(ref, UploadStatusResource.newParameter(getPage().getId())) : - urlFor(ref, UploadStatusResource.newParameter(uploadField.getMarkupId())); + if (form == null && uploadField == null) { + throw new WicketRuntimeException("Either form or uploadField must be set"); + } + + CharSequence url = (form != null && form.isMultiPart()) ? urlFor(ref, UploadStatusResource.newParameter(form.getUploadId())) : + urlFor(ref, UploadStatusResource.newParameter(uploadField.getUploadId())); StringBuilder builder = new StringBuilder(128); Formatter formatter = new Formatter(builder); diff --git a/wicket-migration/src/test/java/org/apache/wicket/migration/MigrateToWicket10Test.java b/wicket-migration/src/test/java/org/apache/wicket/migration/MigrateToWicket10Test.java index 6bce2f1c57..451d50c90a 100644 --- a/wicket-migration/src/test/java/org/apache/wicket/migration/MigrateToWicket10Test.java +++ b/wicket-migration/src/test/java/org/apache/wicket/migration/MigrateToWicket10Test.java @@ -34,6 +34,7 @@ import static org.openrewrite.maven.Assertions.pomXml; class MigrateToWicket10Test implements RewriteTest { @Override + @Disabled public void defaults(RecipeSpec spec) { spec .parser(JavaParser.fromJavaVersion() @@ -46,6 +47,7 @@ class MigrateToWicket10Test implements RewriteTest { } @Test + @Disabled void migrateImports() { //language=java rewriteRun(
