Skip to content

Make uploads to a resource fail cleanly, keep files of one name apart, and add a drop zone and a file list - #1671

Open
reiern70 wants to merge 2 commits into
masterfrom
upload-to-resource
Open

reiern70 wants to merge 2 commits into
masterfrom
upload-to-resource

Conversation

@reiern70

@reiern70 reiern70 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1670
Closes #1669

Two commits, one per issue; the second builds on the first. The follow-ups from review are folded into the commit they belong to.

Make uploads to a resource fail cleanly and keep files of one name apart (#1670)

  • A failed upload calls onUploadFailure() with "The upload failed." (new key uploadToResourceFailed) instead of ending on the error page.
  • The field only accepts the resource keys of the limits, of no files selected and of a lost connection. An HTTP status text or an answer that is not JSON reads as a failed upload, instead of throwing MissingResourceException or claiming the file is too large.
  • A connection that breaks off before the server answers is reported as uploadConnectionLost, worded "The upload failed: the connection to the server was lost." The key check is the protected getUploadErrorMessage(String), so a subclass can word keys its own script reports.
  • The resource no longer sends exception messages to the browser. It logs them as errors, which it did not do at all before.
  • The script checks the limits of the field before sending. Files beyond them get the message of the limit and are not uploaded, instead of the server resetting the connection.
  • A second upload of the same field is refused while one runs. Before, the two requests shared the progress information in the session, and one of them failed.
  • The received files are reported to the field by POST. Before, beyond some 65 files the GET URL exceeded the request line limit of Jetty and Tomcat and onUploadSuccess() was never called.
  • FolderUploadsFileManager stores files under random names, so two files with the same name no longer overwrite each other.
  • cancel() does nothing once the upload has ended, and a before-send callback returning false keeps the upload from being sent.

Add a drop zone and a file list to uploads to a resource (#1669)

  • FileDropZoneBehavior makes any component a drop target for the files of a field. It respects the accept and multiple attributes of the input, leaves folders out, and sets wicket-upload-dragover while files are dragged over it.
  • FileListBehavior, on a <ul> or <ol>, lists the files chosen for the next upload, each with a Remove button (key uploadToResourceRemove). With a list and a multiple input, files chosen or dropped add up instead of replacing each other. Uploaded files leave the list, failed ones stay.
  • A field without these behaviors works as before.
  • The upload-to-resource example shows both. Its styles never applied, because they were in a <head> the page ignores and inline, which the CSP of the examples blocks, so they are in UploadToResourcePage.css now. Its guard against leaving the page during an upload used jQuery and failed under the plain JavaScript engine. Its explanation also says that the limits are checked before anything is sent and that files of one name are kept apart.

API changes and migration

Keeping files of one name apart needs a stored name that travels from the file manager through the browser back to the field, so on master:

  • IUploadsFileManager gets store(FileUpload, String), which returns the stored name. save() is deprecated for removal, and Wicket no longer calls it. The default store() calls save(), so an implementation written for save() keeps working, with the old behaviour for files of one name. The default save() throws an UnsupportedOperationException, so a manager that implements neither method fails with a clear message at its first upload rather than with a StackOverflowError.
  • AbstractFileUploadResource.saveFiles() returns the stored names, and prepareResponse() and generateJsonResponse() receive them. FileUploadResourceReference.generateFileInfo() receives the stored name as well. These are hooks for subclasses, so the signatures change rather than keeping deprecated ones that would no longer be called: an override fails to compile instead of being skipped silently.
  • UploadInfo carries the stored name (getFileName(), new constructor, the old one is deprecated). equals() and hashCode() use it.
  • New, additive: FileUploadToResourceField.getUploadErrorMessage(String) is protected, and the constants UPLOAD_FAILED_RESOURCE_KEY and CONNECTION_LOST_RESOURCE_KEY name the keys of a failed upload and of a lost connection.

The full before and after of every change, who has to act and how, is in migration-to-11.md.

There is no OpenRewrite recipe, because none of this is mechanical. Like the other Wicket 11 changes that need manual migration, the note is in the comment of the MigrateToWicket11 recipe in wicket.yml. There is no "Migration to Wicket 11.0" page on the wiki yet. Draft wording for it:

Uploads to a resource (FileUploadToResourceField): implement IUploadsFileManager#store(FileUpload, String) instead of the deprecated save(). It stores the file under a name of its own and returns that name, which getFile(uploadFieldId, fileName) then receives. The name goes to the browser and back, so getFile() must not return a file of another upload field. FolderUploadsFileManager stores files under random names: find them with UploadInfo#getFile() or UploadInfo#getFileName(), not by client file name. Subclasses of AbstractFileUploadResource receive the stored names in saveFiles(), prepareResponse() and generateJsonResponse(), subclasses of FileUploadResourceReference in generateFileInfo(). A failed upload reads "The upload failed." (resource key uploadToResourceFailed) instead of ending on the error page.

No backport: wicket-10.x stays as it is.

Tests

  • WicketTester: FileUploadToResourceFieldTest, FileListBehaviorTest, FileDropZoneBehaviorTest, IUploadsFileManagerTest for the defaults of store() and save(), and store() tests in FolderUploadsFileManagerTest.
  • QUnit: upload-to-resource.js, run against both the jQuery and the plain JavaScript engine.
  • By hand, on the upload-to-resource example in Chrome and Firefox under both engines: files of one name, the limits, a second upload while one runs, cancel, server failures, a connection reset, picks adding up, remove, a real file drop (Chrome) and the leave-page prompt.
  • mvn clean install -Pjs-test is green on each of the two commits.

@codecov-commenter

codecov-commenter commented Oct 15, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 62.17%. Comparing base (5cbd566) to head (e57325b).

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1671      +/-   ##
============================================
+ Coverage     61.96%   62.17%   +0.21%     
- Complexity    11285    11311      +26     
============================================
  Files          1250     1253       +3     
  Lines         48437    48455      +18     
  Branches       6796     6796              
============================================
+ Hits          30014    30128     +114     
+ Misses        15729    15632      -97     
- Partials       2694     2695       +1     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

GitHub issue #1670

FileUploadToResourceField and the resource it uploads to had several
problems:

- A failed upload ended on the error page instead of in
  onUploadFailure(). For a failure other than a limit the resource answers
  the key uploadFailed, whose text expects an exception in
  ${exception.localizedMessage}; the field put the key, a String, in its
  place, and the interpolation threw. Such failures now have a text of
  their own, uploadToResourceFailed ("The upload failed.").

- The field took whatever the browser reported as a resource key. The
  status text of an HTTP error, or the exception message the resource sent
  with debug logging on, threw MissingResourceException, and an empty one
  (HTTP/2, a network failure) read "File must be less than ...". The field
  now accepts the keys of the limits, of no files selected and of a lost
  connection, and reads anything else as a failed upload. The script
  reports HTTP errors and answers that are not JSON as uploadFailed, and
  a request that got no answer as uploadConnectionLost, which the field
  words "The upload failed: the connection to the server was lost.". The
  check is the protected getUploadErrorMessage(String), so a subclass
  whose own script reports other keys can word them; the field still
  never looks up a key it does not know, since the browser can send any.
  The resource no longer sends exception messages to the browser; it logs
  the exception as an error, which it did not do at all before.

- The script checks the limits of the field before sending: files beyond
  them are reported with the key of the limit and not sent. Before, the
  server refused them, often by resetting the connection, which the
  browser saw as a network failure.

- A second upload started while one ran sent a second request, which
  shared the progress information of the first in the session. Whichever
  ended first removed it, and the other failed with an
  IllegalStateException. The script now refuses to start an upload while
  one of the field runs.

- The script reported the received files to the field by GET, with their
  JSON in the URL. Beyond some 65 files that exceeded the 8 KB request line
  of Jetty and Tomcat: the server answered 414 and onUploadSuccess() was
  never called, although the files were stored. They are reported by POST
  now.

- FolderUploadsFileManager stored each file under its client file name, so
  two files of one name, in one upload or in two, overwrote each other
  while the field was told of both. Files are now stored under a random
  name, which the resource sends to the browser with the client file name
  and UploadInfo carries back to the field.

- cancel() after an upload had ended still claimed to cancel it, the
  return value of the before-send callback was ignored, and an answer that
  was not JSON left the field waiting. Now cancel() does nothing once the
  upload has ended, false from the before-send callback keeps the upload
  from being sent and calls onUploadCanceled(), and an answer that is not
  JSON fails the upload.

The Javadoc of AbstractFileUploadResource said the field uploads a single
file with jQuery; it uploads all the files of its input with a plain
XMLHttpRequest.

Keeping files of one name apart needs a name for each stored file that
travels from the file manager through the browser back to the field, so
the API changes:

- IUploadsFileManager gets store(FileUpload, String), which returns the
  name the file was stored under, and getFile(String, String) takes that
  name. save() is deprecated, and Wicket no longer calls it. The default
  store() calls save() and returns the client file name, so an
  implementation written for save() keeps working, with the old behaviour
  for files of one name. The default save() throws an
  UnsupportedOperationException saying to implement store(): save() used
  to be abstract, and with two defaults calling each other an
  implementation overriding neither would compile and then fail with a
  StackOverflowError.
- AbstractFileUploadResource.saveFiles() returns the stored names, and
  prepareResponse() and generateJsonResponse() receive them. These are
  hooks for subclasses: an old signature kept as deprecated would no
  longer be called, so the signatures change and an override fails to
  compile rather than being skipped silently.
- FileUploadResourceReference.generateFileInfo() receives the stored name
  and puts it into the JSON as fileName.
- UploadInfo carries the stored name (getFileName() and a new
  constructor), and equals() and hashCode() use it instead of the client
  file name. The old constructor is deprecated. A JSON answer without
  fileName, from a custom resource, falls back to the client file name.
- FileUploadToResourceField gets the constants UPLOAD_FAILED_RESOURCE_KEY
  and CONNECTION_LOST_RESOURCE_KEY, and getUploadErrorMessage(String)
  becomes protected. Both are additions.

What an application sees after the change: files stored by
FolderUploadsFileManager no longer have their client file name on disk,
so code looking them up with getFile(fieldId, clientFileName) has to use
UploadInfo#getFile() or #getFileName() instead. A failed upload calls
onUploadFailure() with "The upload failed."; an application that had
changed the text of uploadFailed to work around the error page changes
uploadToResourceFailed instead. An upload whose connection breaks off
reads "The upload failed: the connection to the server was lost.", and
so does one the server refuses by resetting the connection, for a limit
the field does not check before sending. The client-side error callback
receives only resource keys as errorMessage, uploadConnectionLost among
them. A before-send callback returning false now keeps the upload from
being sent. Code calling the deprecated save() on a manager that
implements only store() gets an UnsupportedOperationException, and calls
store() instead.

migration-to-11.md describes the migration in detail: for each change,
what it was before, what it is now, who has to change something and how.
The comment of the MigrateToWicket11 recipe has the short version, as for
the other changes of Wicket 11 that need manual migration. Draft for the
migration guide on the wiki:

  Uploads to a resource (FileUploadToResourceField): implement
  IUploadsFileManager#store(FileUpload, String) instead of the deprecated
  save(). It stores the file under a name of its own and returns that
  name, which getFile(uploadFieldId, fileName) then receives; the name
  goes to the browser and back, so getFile() must not return a file of
  another upload field. FolderUploadsFileManager stores files under random
  names: find them with UploadInfo#getFile() or UploadInfo#getFileName(),
  not by client file name. Subclasses of AbstractFileUploadResource
  receive the stored names in saveFiles(), prepareResponse() and
  generateJsonResponse(), subclasses of FileUploadResourceReference in
  generateFileInfo(). A failed upload reads "The upload failed." (resource
  key uploadToResourceFailed) instead of ending on the error page.
GitHub issue #1669

FileUploadToResourceField uploaded the files of its input as the
browser's dialog left them: each choice replaced the one before, files
could not be dropped, and a file could not be taken out again before the
upload.

Two behaviors add that:

- FileDropZoneBehavior makes a component a zone files are dropped on.
  The input receives the dropped files it accepts by its accept
  attribute, all of them if it is multiple, else the first; folders are
  left out. It then fires its input and change events, as for files
  chosen in the dialog. While files are dragged over the zone, it has the
  CSS class wicket-upload-dragover.

- FileListBehavior, on a <ul> or <ol>, lists the files chosen for the
  next upload, each with a button removing it, whose text is the resource
  uploadToResourceRemove. With a list the field collects files: if its
  input is multiple, files chosen or dropped are added to the listed ones
  instead of replacing them. The files an upload sent leave the list once
  it succeeded, while files chosen meanwhile stay; during an upload the
  remove buttons are disabled. A file name is written as text, never as
  markup.

The script keeps the chosen files and writes them back into the input
through a DataTransfer, so the upload, the progress bar and other
listeners of the input see all of them. It hears of a choice in the
capture phase, before the input's other change listeners run.

A field without these behaviors works as before: a choice replaces the
files of the input, and the input keeps them after the upload.

The script's guard against being evaluated twice tested for an object,
but Wicket.FileUploadToResourceField is a function, so a second
evaluation replaced it. It tests for a function now, as the registry of
file lists lives in the script. A test runs the script a second time and
checks that the field and its lists survive it.

The upload-to-resource example shows a drop zone and a file list. Its
styles never applied: they were in a <head> the page ignores, since it
extends WicketExamplePage, and inline, which the Content Security Policy
of the examples blocks. They are in UploadToResourcePage.css now, under
a class name that does not clash with the drop-zone of the examples'
style.css. Its guard against leaving the page during an upload used
jQuery, which the plain JavaScript engine does not load; it uses
Wicket.Event now. Its explanation also says that the limits of the field
are checked before anything is sent, and that files of the same name are
kept apart, each stored under a name of its own.

migration-to-11.md lists the two behaviors among the changes of Wicket
11 that need no migration.
'use strict';

if (typeof(Wicket.FileUploadToResourceField) === 'object') {
if (typeof(Wicket.FileUploadToResourceField) === 'function') {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for reviewers: this guard keeps the script from setting itself up a second time when it reaches the page twice, for example once on its own and once in an aggregated bundle.

On master it compared with 'object', which never matches: Wicket.FileUploadToResourceField is a constructor, so typeof gives 'function'. A second run therefore replaced the constructor and all its methods. That did no visible harm while the script kept no state of its own.

This PR gives the script state: lists, the file lists of each field, which FileListBehavior registers through addList(). With the old guard, a second run would start a new, empty lists. Lists registered after it would be invisible to fields created before it, and those fields would stop collecting files. With 'function', the second run returns at once and everything shares one registry.

The 'object' check looks copied from scripts whose namespace is a plain object, such as Wicket.FilesSelected and Wicket.Head, where it is right.

The QUnit test "a second run of the script keeps the field and its lists" in upload-to-resource.js loads the script again. With 'object', all three of its assertions fail.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

problems with upload to resource multiple file upload to resource + drag and drop files and upload to resource.

2 participants