This is an automated email from the ASF dual-hosted git repository.

reiern70 pushed a commit to branch bugix/reiern70/WICKET-7037-master
in repository https://gitbox.apache.org/repos/asf/wicket.git

commit 0b36a50428a417e933988435599f987f3c78452d
Author: reiern70 <[email protected]>
AuthorDate: Thu Apr 6 10:20:25 2023 +0300

    [WICKET-7033] implementation of: 1) an upload field that allows uploading 
files into to a mounted resource, 2) a reusable mounted resource that process 
uploads and 3) adapting upload progress bar to work in such use case
---
 .../java/org/apache/wicket/ajax/AjaxUtils.java     |  55 +++
 .../resource/AbstractFileUploadResource.java       | 223 +++++++++++
 .../resource/FileUploadResourceReference.java      | 132 ++++++
 .../upload/resource/FileUploadToResourceField.java | 446 +++++++++++++++++++++
 .../upload/resource/FileUploadToResourceField.js   | 104 +++++
 .../resource/FileUploadToResourceField.properties  |  16 +
 .../upload/resource/FolderUploadsFileManager.java  |  83 ++++
 .../form/upload/resource/IUploadsFileManager.java  |  52 +++
 .../wicket/examples/upload/UploadApplication.java  |  19 +
 .../apache/wicket/examples/upload/UploadPage.java  |   5 +-
 .../examples/upload/UploadToResourcePage.html      |  44 ++
 .../examples/upload/UploadToResourcePage.java      | 263 ++++++++++++
 .../apache/wicket/examples/homepage/HomePage.html  |   1 +
 .../resources/org/apache/wicket/examples/style.css |   2 +-
 .../markup/html/form/upload/UploadProgressBar.java |  78 +++-
 .../ajax/markup/html/form/upload/progressbar.js    |  50 ++-
 16 files changed, 1543 insertions(+), 30 deletions(-)

diff --git a/wicket-core/src/main/java/org/apache/wicket/ajax/AjaxUtils.java 
b/wicket-core/src/main/java/org/apache/wicket/ajax/AjaxUtils.java
new file mode 100644
index 0000000000..7ed0253e1a
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/ajax/AjaxUtils.java
@@ -0,0 +1,55 @@
+/*
+ * 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.ajax;
+
+import java.util.Optional;
+import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
+import org.apache.wicket.request.cycle.RequestCycle;
+import org.danekja.java.util.function.serializable.SerializableConsumer;
+
+/**
+ * Some AJAX related utility functions.
+ */
+public class AjaxUtils
+{
+
+    /**
+     * Runs action if current request is of type "AJAX". Otherwise, nothing is 
done.
+     *
+     * @param ajaxAction
+     *            The action to run a {@link SerializableConsumer}
+     */
+    public static void executeIfAjax(SerializableConsumer<AjaxRequestTarget> 
ajaxAction)
+    {
+        Optional<AjaxRequestTarget> target = 
RequestCycle.get().find(AjaxRequestTarget.class);
+        target.ifPresent(ajaxAction);
+    }
+
+
+    /**
+     * Runs action if current request is of type "AJAX" or a Websockets 
request. Otherwise, nothing is done.
+     *
+     * @param ajaxAction
+     *            The action to run a {@link SerializableConsumer}
+     */
+    public static void 
executeIfAjaxOrWebSockets(SerializableConsumer<IPartialPageRequestHandler> 
ajaxAction)
+    {
+        Optional<IPartialPageRequestHandler> target = 
RequestCycle.get().find(IPartialPageRequestHandler.class);
+        target.ifPresent(ajaxAction);
+    }
+
+}
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/AbstractFileUploadResource.java
 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/AbstractFileUploadResource.java
new file mode 100644
index 0000000000..6bbce26384
--- /dev/null
+++ 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/AbstractFileUploadResource.java
@@ -0,0 +1,223 @@
+/*
+ * 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.markup.html.form.upload.resource;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.fileupload2.FileItem;
+import org.apache.commons.fileupload2.pub.FileUploadSizeException;
+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;
+import org.apache.wicket.request.cycle.RequestCycle;
+import org.apache.wicket.request.resource.AbstractResource;
+import org.apache.wicket.util.lang.Bytes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.github.openjson.JSONObject;
+import jakarta.servlet.http.HttpServletResponse;
+
+/**
+ * The resource that handles the file uploads.
+ * Reads the file items from the request parameters and uses {@link 
IUploadsFileManager}
+ * to store them.
+ * Additionally, cares about the response's content type and body.
+ * <p>
+ * This code was adapted from
+ * <p>
+ * <a 
href="https://github.com/martin-g/blogs/blob/master/file-upload/src/main/java/com/mycompany/fileupload/AbstractFileUploadResource.java";>AbstractFileUploadResource.java</a>
+ * <p>
+ * The main difference is that there some JQuery plugin is used at client side 
(and it supports multiple uploads +
+ * some UI allowing to delete/preview files and so on).
+ * Here we are just using plain jQuery code at client side to upload a single 
file.
+ */
+public abstract class AbstractFileUploadResource extends AbstractResource
+{
+       private static final Logger LOG = 
LoggerFactory.getLogger(AbstractFileUploadResource.class);
+
+       public static final String PARAM_NAME = "WICKET-FILE-UPLOAD";
+
+       /**
+        * This resource is usually an application singleton. Thus, client side 
pass
+        * to the resource a unique ID identifying the upload field performing 
the upload.
+        * The upload file makes sure this is a unique identifier at 
application level, See
+        * {@link 
FileUploadToResourceField#generateAUniqueApplicationWiseId()}. So that, there 
are no clashes between
+        * different users/pages/sessions performing an upload.
+        */
+       public static final String UPLOAD_ID = "uploadId";
+       /**
+        * i18n key for case no files were selected.
+        */
+       public static final String NO_FILE_SELECTED = 
"wicket.no.files.selected";
+       /**
+        * i18n key for the case selcted files exceed size limit.
+        */
+       public static final String REQUEST_SIZE_LIMIT_EXCEEDED = 
"wicket.multipart.size.exceeded";
+
+       private final IUploadsFileManager fileManager;
+
+       public AbstractFileUploadResource(IUploadsFileManager fileManager)
+       {
+               this.fileManager = fileManager;
+       }
+
+       /**
+        * Reads and stores the uploaded files
+        *
+        * @param attributes
+        *            Attributes
+        * @return ResourceResponse
+        */
+       @Override
+       protected ResourceResponse newResourceResponse(Attributes attributes)
+       {
+               final ResourceResponse resourceResponse = new 
ResourceResponse();
+
+               final ServletWebRequest webRequest = (ServletWebRequest) 
attributes.getRequest();
+
+               // get the ID of the upload field (it should be unique per 
application)
+               String uploadId = 
webRequest.getRequestParameters().getParameterValue(UPLOAD_ID).toString("resource");
+
+               try
+               {
+                       MultipartServletWebRequest multiPartRequest = 
webRequest.newMultipartWebRequest(getMaxSize(), uploadId);
+                       multiPartRequest.parseFileParts();
+
+                       RequestCycle.get().setRequest(multiPartRequest);
+
+                       // retrieve the files.
+                       Map<String, List<FileItem>> files = 
multiPartRequest.getFiles();
+                       List<FileItem> fileItems = files.get(PARAM_NAME);
+
+                       if (fileItems != null)
+                       {
+                               List<FileUpload> fileUploads = new 
ArrayList<>();
+                               for (FileItem fileItem : fileItems)
+                               {
+                                       fileUploads.add(new 
FileUpload(fileItem));
+                               }
+                               saveFiles(fileUploads, uploadId);
+                               prepareResponse(resourceResponse, webRequest, 
fileUploads);
+                       }
+                       else
+                       {
+                               
resourceResponse.setContentType("application/json");
+                               resourceResponse.setWriteCallback(new 
WriteCallback()
+                               {
+                                       @Override
+                                       public void writeData(Attributes 
attributes) throws IOException
+                                       {
+                                               JSONObject json = new 
JSONObject();
+                                               json.put("error", true);
+                                               json.put("errorMessage", 
NO_FILE_SELECTED);
+                                               String error = json.toString();
+                                               
attributes.getResponse().write(error);
+                                       }
+                               });
+                       }
+
+               }
+               catch (Exception fux)
+               {
+                       LOG.error("An error occurred while uploading a file", 
fux);
+                       if (fux instanceof FileUploadSizeException)
+                       {
+                               
resourceResponse.setStatusCode(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
+                               return resourceResponse;
+                       }
+                       resourceResponse.setContentType("application/json");
+                       JSONObject json = new JSONObject();
+                       json.put("error", true);
+                       String errorMessage = LOG.isDebugEnabled() ? 
fux.getMessage() : "An error occurred while uploading files";
+                       json.put("errorMessage", errorMessage);
+                       String error = json.toString();
+                       resourceResponse.setWriteCallback(new WriteCallback()
+                       {
+                               @Override
+                               public void writeData(Attributes attributes) 
throws IOException
+                               {
+                                       attributes.getResponse().write(error);
+                               }
+                       });
+               }
+
+               return resourceResponse;
+       }
+
+       /**
+        * Sets the response's content type and body
+        *
+        * @param resourceResponse
+        *            ResourceResponse
+        * @param webRequest
+        *            ServletWebRequest
+        * @param fileItems
+        *            List<FileUpload>
+        */
+       protected void prepareResponse(ResourceResponse resourceResponse, 
ServletWebRequest webRequest, List<FileUpload> fileItems)
+       {
+               resourceResponse.setContentType("application/json");
+               final String responseContent = 
generateJsonResponse(resourceResponse, webRequest, fileItems);
+
+               resourceResponse.setWriteCallback(new WriteCallback()
+               {
+                       @Override
+                       public void writeData(Attributes attributes) throws 
IOException
+                       {
+                               attributes.getResponse().write(responseContent);
+                       }
+               });
+       }
+
+       /**
+        * Delegates to FileManager to store the uploaded files
+        *
+        * @param fileItems
+        *            List<FileUpload>
+        */
+       protected void saveFiles(List<FileUpload> fileItems, String uploadId)
+       {
+               for (FileUpload fileItem : fileItems)
+               {
+                       fileManager.save(fileItem, uploadId);
+               }
+       }
+
+       /**
+        * Defines what is the maximum size of the uploaded files.
+        *
+        * @return Bytes
+        */
+       protected abstract Bytes getMaxSize();
+
+       /**
+        * Should generate the response's body in JSON format
+        *
+        * @param resourceResponse
+        *            ResourceResponse
+        * @param webRequest
+        *            ServletWebRequest
+        * @param files
+        *            List<FileUpload>
+        * @return The generated JSON
+        */
+       protected abstract String generateJsonResponse(ResourceResponse 
resourceResponse,
+                       ServletWebRequest webRequest, List<FileUpload> files);
+
+}
\ No newline at end of file
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadResourceReference.java
 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadResourceReference.java
new file mode 100644
index 0000000000..802ea5df88
--- /dev/null
+++ 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadResourceReference.java
@@ -0,0 +1,132 @@
+/*
+ * 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.markup.html.form.upload.resource;
+
+import java.util.List;
+import org.apache.wicket.markup.html.form.upload.FileUpload;
+import org.apache.wicket.protocol.http.servlet.ServletWebRequest;
+import org.apache.wicket.request.resource.IResource;
+import org.apache.wicket.request.resource.ResourceReference;
+import org.apache.wicket.util.lang.Args;
+import org.apache.wicket.util.lang.Bytes;
+import org.danekja.java.util.function.serializable.SerializableSupplier;
+import com.github.openjson.JSONArray;
+import com.github.openjson.JSONException;
+import com.github.openjson.JSONObject;
+
+/**
+ * A resource reference that provides default implementation of 
AbstractFileUploadResource.
+ * The implementation generates JSON response with data from the upload (this 
data is
+ * re-routed to the page for things like getting the client file name and file 
size).
+ */
+public class FileUploadResourceReference extends ResourceReference
+{
+
+       private final IUploadsFileManager uploadFileManager;
+
+       private static FileUploadResourceReference instance;
+
+       /**
+        * This method assumes {@link #createNewInstance(IUploadsFileManager, 
SerializableSupplier)} was called before
+        *
+        * @return FileUploadResourceReference
+        */
+       public static FileUploadResourceReference getInstance()
+       {
+               if (instance == null)
+               {
+                       throw new IllegalStateException("An instance should be 
created via the createNewInstance method");
+               }
+               return instance;
+       }
+
+       /**
+        * Use this method in order to create an instance to be mounted at 
application level.
+        *
+        * @param fileManager The {@link IUploadsFileManager}
+        * @param maxSize The maxSize of the multipart request.
+        * @return FileUploadResourceReference
+        */
+       public static FileUploadResourceReference 
createNewInstance(IUploadsFileManager fileManager, SerializableSupplier<Bytes> 
maxSize)
+       {
+               if (instance == null)
+               {
+                       instance = new FileUploadResourceReference(fileManager, 
maxSize);
+               }
+               return instance;
+       }
+
+       private final SerializableSupplier<Bytes> maxSize;
+
+       protected FileUploadResourceReference(IUploadsFileManager 
uploadFileManager, SerializableSupplier<Bytes> maxSize)
+       {
+               super(FileUploadResourceReference.class, "file-uploads");
+
+               Args.notNull(uploadFileManager, "uploadFileManager");
+
+               this.uploadFileManager = uploadFileManager;
+               this.maxSize = maxSize;
+       }
+
+       @Override
+       public IResource getResource()
+       {
+               return new AbstractFileUploadResource(uploadFileManager)
+               {
+                       @Override
+                       protected Bytes getMaxSize()
+                       {
+                               return maxSize.get();
+                       }
+
+                       @Override
+                       protected String generateJsonResponse(ResourceResponse 
resourceResponse, ServletWebRequest webRequest, List<FileUpload> files)
+                       {
+                               JSONArray json = new JSONArray();
+
+                               for (FileUpload fileItem : files)
+                               {
+                                       JSONObject fileJson = new JSONObject();
+
+                                       try
+                                       {
+                                               generateFileInfo(fileJson, 
fileItem);
+                                               json.put(fileJson);
+                                       }
+                                       catch (JSONException e)
+                                       {
+                                               throw new RuntimeException(e);
+                                       }
+                               }
+                               return json.toString();
+                       }
+
+               };
+       }
+
+       public IUploadsFileManager getUploadFileManager() {
+               return uploadFileManager;
+       }
+
+       protected void generateFileInfo(JSONObject fileJson, FileUpload 
fileItem)
+       {
+               fileJson.put("clientFileName", fileItem.getClientFileName());
+               fileJson.put("size", fileItem.getSize());
+               fileJson.put("contentType", fileItem.getContentType());
+       }
+
+}
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
new file mode 100644
index 0000000000..9f1054fbaf
--- /dev/null
+++ 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.java
@@ -0,0 +1,446 @@
+/*
+ * 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.markup.html.form.upload.resource;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import org.apache.commons.io.IOUtils;
+import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.AjaxUtils;
+import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.head.JavaScriptHeaderItem;
+import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
+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.request.Request;
+import org.apache.wicket.request.cycle.RequestCycle;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.request.resource.JavaScriptResourceReference;
+import org.apache.wicket.resource.CoreLibrariesContributor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.github.openjson.JSONArray;
+import com.github.openjson.JSONObject;
+
+/**
+ * Implementation of FileUploadField capable to uploading a file into a wicket 
mounted resource.
+ * This field does not require a {@link 
org.apache.wicket.markup.html.form.Form}!
+ * The upload of the file id done via the {@link 
#startUpload(IPartialPageRequestHandler)} method.
+ */
+public abstract class FileUploadToResourceField extends FileUploadField
+{
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(FileUploadToResourceField.class);
+
+    /**
+     * Info regarding an upload.
+     */
+    public static final class UploadInfo
+    {
+        private File file;
+        private final String clientFileName;
+
+        private final long size;
+
+        private final String contentType;
+
+        public UploadInfo(String clientFileName, long size, String contentType)
+        {
+            this.clientFileName = clientFileName;
+            this.size = size;
+            this.contentType = contentType;
+        }
+
+        public File getFile()
+        {
+            return file;
+        }
+
+        public void setFile(File file)
+        {
+            this.file = file;
+        }
+
+        public String getClientFileName()
+        {
+            return clientFileName;
+        }
+
+        public long getSize()
+        {
+            return size;
+        }
+
+        public String getContentType()
+        {
+            return contentType;
+        }
+
+        @Override
+        public boolean equals(Object o)
+        {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            UploadInfo fileInfo = (UploadInfo) o;
+            return Objects.equals(clientFileName, fileInfo.clientFileName);
+        }
+
+        /**
+         * @return the bytes associated with the upload. Return null in case 
no file is present
+         * or some other error condition happens.
+         */
+        public byte[] get()
+        {
+            if (file == null)
+            {
+               return null;
+            }
+
+            byte[] fileData = new byte[(int) getSize()];
+            InputStream fis = null;
+
+            try
+            {
+                fis = new FileInputStream(file);
+                IOUtils.readFully(fis, fileData);
+            }
+            catch (IOException e)
+            {
+                LOGGER.debug("IOException at get", e);
+                fileData = null;
+            }
+            finally
+            {
+                IOUtils.closeQuietly(fis);
+            }
+
+            return fileData;
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return Objects.hash(clientFileName);
+        }
+
+        public static List<UploadInfo> fromJson(String json)
+        {
+            List<UploadInfo> infos = new ArrayList<>();
+            JSONArray jsonArray = new JSONArray(json);
+            for (int i = 0; i < jsonArray.length(); i++)
+            {
+                JSONObject jsonObject = jsonArray.getJSONObject(i);
+                infos.add(new 
UploadInfo(jsonObject.getString("clientFileName"), jsonObject.getLong("size"), 
jsonObject.getString("contentType")));
+            }
+            return infos;
+        }
+    }
+
+    private static final JavaScriptResourceReference JS = new 
JavaScriptResourceReference(FileUploadToResourceField.class, 
"FileUploadToResourceField.js");
+
+    public static String UPLOAD_CANCELED = "upload.canceled";
+
+
+    private static abstract class FileModel implements IModel<List<UploadInfo>>
+    {
+
+        @Override
+        public List<UploadInfo> getObject()
+        {
+            List<UploadInfo> fileInfos = getFileUploadInfos();
+            for (UploadInfo uploadInfo : fileInfos)
+            {
+                // 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));
+            }
+            return fileInfos;
+        }
+
+        @Override
+        public void setObject(List<UploadInfo> object)
+        {
+            throw new UnsupportedOperationException("setObject not supported");
+        }
+
+        protected abstract IUploadsFileManager fileManager();
+
+        /*
+            This is an application unique ID assigned to upload field.
+         */
+        protected abstract String getUploadFieldId();
+
+        protected abstract List<UploadInfo> getFileUploadInfos();
+    }
+
+    private final AbstractDefaultAjaxBehavior ajaxBehavior;
+
+    private transient List<UploadInfo> fileUploadInfos;
+
+    public FileUploadToResourceField(String id) {
+        super(id);
+        setOutputMarkupId(true);
+        // generate a unique ID
+        setMarkupId(generateAUniqueApplicationWiseId());
+        setDefaultModel(new FileModel() {
+            @Override
+            protected IUploadsFileManager fileManager() {
+                return FileUploadToResourceField.this.fileManager();
+            }
+
+            @Override
+            protected String getUploadFieldId()
+            {
+                return FileUploadToResourceField.this.getMarkupId();
+            }
+
+            @Override
+            protected List<UploadInfo> getFileUploadInfos()
+            {
+                return fileUploadInfos;
+            }
+        });
+        ajaxBehavior = new AbstractDefaultAjaxBehavior()
+        {
+            @Override
+            protected void respond(AjaxRequestTarget target)
+            {
+                Request request = RequestCycle.get().getRequest();
+                boolean error = 
request.getRequestParameters().getParameterValue("error").toBoolean(true);
+                if (!error) {
+                    String filesIfo = 
request.getRequestParameters().getParameterValue("filesInfo").toString();
+                    fileUploadInfos = UploadInfo.fromJson(filesIfo);
+                    onUploadSuccess(target, getFileUploadInfos());
+                }
+                else
+                {
+                    String errorMessage = 
request.getRequestParameters().getParameterValue("errorMessage").toString(null);
+                    if (UPLOAD_CANCELED.equals(errorMessage))
+                    {
+                        onUploadCanceled(target);
+                    }
+                    else
+                    {
+                        if 
(AbstractFileUploadResource.NO_FILE_SELECTED.equals(errorMessage))
+                        {
+                            errorMessage = 
getString(AbstractFileUploadResource.NO_FILE_SELECTED);
+                        }
+                        else if (isUploadTooBig(errorMessage))
+                        {
+                            errorMessage = 
getString(AbstractFileUploadResource.REQUEST_SIZE_LIMIT_EXCEEDED);
+                        }
+                        onUploadFailure(target, errorMessage);
+                    }
+                }
+            }
+        };
+        add(ajaxBehavior);
+    }
+
+    /**
+     *
+     * @param errorMessage The error message
+     * @return true iff errorMessage is because upload exceeded max allowed 
size.
+     */
+    protected boolean isUploadTooBig(String errorMessage)
+    {
+        return "Payload Too Large".equals(errorMessage);
+    }
+
+    private List<UploadInfo> getFileUploadInfos()
+    {
+        // mind that this makes files to be added.
+        return (List<UploadInfo>)getDefaultModel().getObject();
+    }
+
+    @Override
+    public FileUpload getFileUpload()
+    {
+        throw new UnsupportedOperationException("FileUploadToResourceField 
does not support working with FileUpload");
+    }
+
+    @Override
+    public List<FileUpload> getFileUploads()
+    {
+        throw new UnsupportedOperationException("FileUploadToResourceField 
does not support working with FileUpload");
+    }
+
+    @Override
+    protected void onRemove()
+    {
+        super.onRemove();
+        // we clean any client side mess if component is removed via "partial" 
page replacement
+        AjaxUtils.executeIfAjaxOrWebSockets(target -> 
target.appendJavaScript("delete Wicket.Timer." + getMarkupId() + ";"));
+
+    }
+
+    /**
+     * Override to do something on a successful upload.
+     *
+     * @param target         The {@link AjaxRequestTarget}
+     * @param fileInfos      The List<FileInfo
+     */
+    protected abstract void onUploadSuccess(AjaxRequestTarget target, 
List<UploadInfo> fileInfos);
+
+
+    /**
+     *  Override to do something on a non successful upload
+     *
+     * @param target The {@link AjaxRequestTarget}
+     * @param errorInfo The cause of the failure
+     */
+    protected void onUploadFailure(AjaxRequestTarget target, String errorInfo)
+    {
+        // nothing by default
+    }
+
+    /**
+     *  Override to do something in case user canceled upload
+     *
+     * @param target The {@link AjaxRequestTarget}
+     */
+    protected void onUploadCanceled(AjaxRequestTarget target)
+    {
+        // nothing by default
+    }
+
+    /**
+     * @return  a unique application wise ID (it should be a valid HTML id).
+     */
+    protected String generateAUniqueApplicationWiseId()
+    {
+        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("resourceUrl", urlFor(getFileUploadResourceReference(), 
new PageParameters()).toString());
+        jsonObject.put("ajaxCallBackUrl", ajaxBehavior.getCallbackUrl());
+        response.render(OnDomReadyHeaderItem.forScript("Wicket.Timer."
+                + getMarkupId() + " = new Wicket.FileUploadToResourceField("
+                + jsonObject + ","
+                + getClientBeforeSendCallBack() + ","
+                + getClientSideSuccessCallBack() + ","
+                + getClientSideCancelCallBack() + ","
+                + getClientSideUploadErrorCallBack() + ");"));
+    }
+
+    /**
+     * Override if you need to return a different instance of 
FileUploadResourceReference
+     * @return FileUploadResourceReference
+     */
+    protected FileUploadResourceReference getFileUploadResourceReference()
+    {
+        return FileUploadResourceReference.getInstance();
+    }
+
+    /**
+     * @return The JavaScript expression starting the upload.
+     */
+    public String getTriggerUploadScript()
+    {
+        return "Wicket.Timer." + getMarkupId() + ".upload();";
+    }
+
+    /**
+     * Starts the upload via an AJAX request.
+     *
+     * @param target The {@link AjaxRequestTarget}
+     */
+    public void startUpload(IPartialPageRequestHandler target)
+    {
+        target.appendJavaScript(getTriggerUploadScript());
+    }
+
+    /**
+     * @return The JavaScript expression canceling the upload.
+     */
+    public String getTriggerCancelUploadScript()
+    {
+        return "Wicket.Timer." + getMarkupId() + ".cancel();";
+    }
+    /**
+     * Cancels the upload via an AJAX request.
+     *
+     * @param target The {@link AjaxRequestTarget}
+     */
+    public void cancelUpload(IPartialPageRequestHandler target)
+    {
+        target.appendJavaScript(getTriggerCancelUploadScript());
+    }
+
+    /**
+     * See jQuery.ajax documentation.
+     *
+     * @return A JavaScript function to be executed on beforeSend the upload 
request.
+     */
+    protected CharSequence getClientBeforeSendCallBack()
+    {
+        return "function (xhr, settings) { return true; }";
+    }
+
+    /**
+     * @return A JavaScript function to be executed on successful upload. This 
is, besides the normal wicket
+     * AJAX request (see {@link #onUploadSuccess(AjaxRequestTarget, List)}).
+     */
+    protected CharSequence getClientSideSuccessCallBack()
+    {
+        return "function () {}";
+    }
+
+    /**
+     * @return A JavaScript function to be executed on upload canceled. This 
is, besides the normal wicket
+     * AJAX request (see {@link #onUploadFailure(AjaxRequestTarget, String)}).
+     */
+    protected CharSequence getClientSideCancelCallBack()
+    {
+        return "function () {}";
+    }
+
+    /**
+     * @return A JavaScript function to be executed on upload canceled. This 
is, besides the normal wicket
+     * AJAX request (see {@link #onUploadFailure(AjaxRequestTarget, String)}). 
It receives as parameter a JSON object
+     * like <code>{'error': true, errorMessage: 'xxx'}</code>.
+     */
+    protected CharSequence getClientSideUploadErrorCallBack()
+    {
+        return "function (res) {}";
+    }
+
+    /**
+     * @return The IUploadsFileManager
+     */
+    protected IUploadsFileManager fileManager()
+    {
+        return getFileUploadResourceReference().getUploadFileManager();
+    }
+}
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
new file mode 100644
index 0000000000..05a5349167
--- /dev/null
+++ 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.js
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+
+;(function (undefined) {
+
+    'use strict';
+
+    if (typeof(Wicket.FileUploadToResourceField) === 'object') {
+        return;
+    }
+
+    Wicket.FileUploadToResourceField = function (settings, 
clientBeforeSendCallBack, clientSideSuccessCallBack, clientSideCancelCallBack, 
uploadErrorCallBack)
+    {
+        this.inputName = settings.inputName;
+        this.input = document.getElementById(this.inputName);
+        this.resourceUrl = settings.resourceUrl + "?uploadId=" + 
this.inputName;
+        this.ajaxCallBackUrl = settings.ajaxCallBackUrl;
+        this.clientBeforeSendCallBack = clientBeforeSendCallBack;
+        this.clientSideSuccessCallBack = clientSideSuccessCallBack;
+        this.clientSideCancelCallBack = clientSideCancelCallBack;
+        this.uploadErrorCallBack = uploadErrorCallBack;
+    }
+
+    Wicket.FileUploadToResourceField.prototype.upload = function()
+    {
+        // get a fresh reference to input
+        this.input = document.getElementById(this.inputName);
+        // we add the files to a FormData object.
+        var formData = new FormData();
+        var totalfiles = this.input.files.length;
+        for (var index = 0; index < totalfiles; index++) {
+            formData.append("WICKET-FILE-UPLOAD",this.input.files[index]);
+        }
+        // pass the input name to know where to store files at server side.
+        formData.append("uploadId", this.inputName);
+        var self = this;
+        // we use jQuery to post the files to the resource (this.resourceUrl)
+        // and we keep a reference to the request in order to be able
+        // to cancel the upload
+        this.xhr = $.ajax({
+            url: this.resourceUrl,
+            type: "POST",
+            data: formData,
+            processData: false,
+            contentType: false,
+            success: function (res) {
+                // do clean up on success
+                if (res.error) {
+                    Wicket.Ajax.get({"u": self.ajaxCallBackUrl, "ep": res});
+                    self.uploadErrorCallBack(res);
+                } else {
+                    self.clientSideSuccessCallBack();
+                    var ep = {'error': false, 'filesInfo': 
JSON.stringify(res)};
+                    Wicket.Ajax.get({"u": self.ajaxCallBackUrl, "ep": ep});
+                }
+            },
+            beforeSend: function (xhr) {
+                self.clientBeforeSendCallBack(xhr);
+            },
+            error: function (jqXHR, textStatus, errorThrown) {
+                if (textStatus === "abort") {
+                    // user aborted the upload.
+                    var ep = {'error': true, 'errorMessage': 
'upload.canceled'};
+                    Wicket.Ajax.get({"u": self.ajaxCallBackUrl, "ep": ep});
+                } else if (textStatus === "error"){
+                    var ep = {'error': true, "errorMessage": errorThrown};
+                    self.uploadErrorCallBack(ep);
+                    Wicket.Ajax.get({"u": self.ajaxCallBackUrl, "ep": ep});
+                } else if (textStatus === "parsererror"){
+                    // this error will only happen is generated JSON at server 
side is faulty
+                    var data = jqXHR.responseText;
+                    Wicket.Log.log(data);
+                }
+            }
+        });
+    }
+
+    // cancel the upload
+    Wicket.FileUploadToResourceField.prototype.cancel = function () {
+        // we have a reference to the request we can cancel it.
+        if (this.xhr) {
+            this.xhr.abort();
+            this.clientSideCancelCallBack();
+            Wicket.Log.log("The upload associated with field '" + 
this.inputName + "' has been canceled!");
+            delete (this.xhr);
+        } else {
+            Wicket.Log.log("Too late to cancel upload for field '"  + 
this.inputName +  "': the upload has already finished.");
+        }
+    }
+})();
\ No newline at end of file
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.properties
 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.properties
new file mode 100644
index 0000000000..4399cf5805
--- /dev/null
+++ 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.properties
@@ -0,0 +1,16 @@
+#  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.
+wicket.no.files.selected = Nothing to upload: no files were selected.
+wicket.multipart.size.exceeded = The total size of the selected files exceeds 
configured limits.
\ No newline at end of file
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FolderUploadsFileManager.java
 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FolderUploadsFileManager.java
new file mode 100644
index 0000000000..0146b82099
--- /dev/null
+++ 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FolderUploadsFileManager.java
@@ -0,0 +1,83 @@
+/*
+ * 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.markup.html.form.upload.resource;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import org.apache.wicket.WicketRuntimeException;
+import org.apache.wicket.markup.html.form.upload.FileUpload;
+import org.apache.wicket.util.file.File;
+import org.apache.wicket.util.io.IOUtils;
+import org.apache.wicket.util.lang.Args;
+
+/**
+ * Implementation of {@link IUploadsFileManager} that stores files in 
sub-folders of a given folder.
+ * Sub-folders are named using the input field ID (and those are unique per 
application). Thus, there
+ * is no possibility that uploads get mixed up.
+ */
+public class FolderUploadsFileManager implements IUploadsFileManager
+{
+
+    private final File folder;
+
+    public FolderUploadsFileManager(File folder)
+    {
+        Args.notNull(folder, "folder");
+        if (!folder.exists())
+        {
+            try
+            {
+                Files.createDirectories(folder.toPath());
+            }
+            catch (IOException e)
+            {
+                throw new WicketRuntimeException(e);
+            }
+        }
+        else if (folder.exists() && !folder.isDirectory())
+        {
+            throw new IllegalArgumentException("Not a folder : " + 
folder.getAbsolutePath());
+        }
+        this.folder = folder;
+    }
+
+    public File getFolder() {
+        return folder;
+    }
+
+    @Override
+    public void save(FileUpload fileItem, String uploadFieldId)
+    {
+        File uploadFieldFolder = new File(getFolder(), uploadFieldId);
+        uploadFieldFolder.mkdirs();
+        try
+        {
+            IOUtils.copy(fileItem.getInputStream(),  new FileOutputStream(new 
File(uploadFieldFolder, fileItem.getClientFileName())));
+        }
+        catch (IOException e)
+        {
+            throw new WicketRuntimeException(e);
+        }
+    }
+
+    @Override
+    public File getFile(String uploadFieldId, String clientFileName)
+    {
+        return new File(new File(getFolder(), uploadFieldId), clientFileName);
+    }
+}
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/IUploadsFileManager.java
 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/IUploadsFileManager.java
new file mode 100644
index 0000000000..78a1208503
--- /dev/null
+++ 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/IUploadsFileManager.java
@@ -0,0 +1,52 @@
+/*
+ * 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.markup.html.form.upload.resource;
+
+import org.apache.wicket.markup.html.form.upload.FileUpload;
+import org.apache.wicket.util.file.File;
+
+/**
+ * This interface defines the bridge between a file uploaded to a resource and 
the wicket component.
+ * Wicket component uses some identifier (passed as a request parameter) to 
instruct the resource
+ * how to store file. Same identifier is used to retrieve the file, once 
uploaded, from component in page.
+ * Mind that uploader resource is a singleton => identifier needs to be unique 
among different sessions
+ * (and pages in a session).
+ */
+public interface IUploadsFileManager
+{
+
+       /**
+        * Saves an uploaded files into some persistent storage (e,g, disk).
+        *
+        * @param fileItem
+        *            The {@link FileUpload}
+        * @param uploadFieldId
+        *            The unique ID of the upload field.
+        */
+       void save(FileUpload fileItem, String uploadFieldId);
+
+       /**
+        * Retrieves the file based on an uploadFieldId and clientFileName.
+        *
+        * @param uploadFieldId
+        *            The unique ID of the upload field.
+        * @param clientFileName
+        *            The client file name
+        * @return File the file
+        */
+       File getFile(String uploadFieldId, String clientFileName);
+}
diff --git 
a/wicket-examples/src/main/java/org/apache/wicket/examples/upload/UploadApplication.java
 
b/wicket-examples/src/main/java/org/apache/wicket/examples/upload/UploadApplication.java
index 0826393842..f6c5054c28 100644
--- 
a/wicket-examples/src/main/java/org/apache/wicket/examples/upload/UploadApplication.java
+++ 
b/wicket-examples/src/main/java/org/apache/wicket/examples/upload/UploadApplication.java
@@ -18,7 +18,11 @@ package org.apache.wicket.examples.upload;
 
 import org.apache.wicket.Page;
 import org.apache.wicket.examples.WicketExampleApplication;
+import 
org.apache.wicket.markup.html.form.upload.resource.FileUploadResourceReference;
+import 
org.apache.wicket.markup.html.form.upload.resource.FolderUploadsFileManager;
+import org.apache.wicket.markup.html.form.upload.resource.IUploadsFileManager;
 import org.apache.wicket.util.file.Folder;
+import org.apache.wicket.util.lang.Bytes;
 
 
 /**
@@ -30,6 +34,8 @@ public class UploadApplication extends 
WicketExampleApplication
 {
        private Folder uploadFolder = null;
 
+       private IUploadsFileManager uploadsFileManager;
+
        @Override
        public Class<? extends Page> getHomePage()
        {
@@ -55,9 +61,22 @@ public class UploadApplication extends 
WicketExampleApplication
                // Ensure folder exists
                uploadFolder.mkdirs();
 
+               uploadsFileManager = new FolderUploadsFileManager(uploadFolder);
+
+               mountResource("/uploads", 
FileUploadResourceReference.createNewInstance(uploadsFileManager, ()-> 
Bytes.kilobytes(100)));
+
                mountPage("/multi", MultiUploadPage.class);
                mountPage("/single", UploadPage.class);
+               mountPage("/uploadToResource", UploadToResourcePage.class);
 
                getApplicationSettings().setUploadProgressUpdatesEnabled(true);
        }
+
+       public IUploadsFileManager getUploadsFileManager() {
+               return uploadsFileManager;
+       }
+
+       public static UploadApplication getInstance() {
+               return (UploadApplication)get();
+       }
 }
diff --git 
a/wicket-examples/src/main/java/org/apache/wicket/examples/upload/UploadPage.java
 
b/wicket-examples/src/main/java/org/apache/wicket/examples/upload/UploadPage.java
index 3fe09b7eef..2ad1498e6b 100644
--- 
a/wicket-examples/src/main/java/org/apache/wicket/examples/upload/UploadPage.java
+++ 
b/wicket-examples/src/main/java/org/apache/wicket/examples/upload/UploadPage.java
@@ -20,7 +20,6 @@ import java.io.File;
 import java.util.Arrays;
 import java.util.List;
 
-import org.apache.wicket.Application;
 import org.apache.wicket.examples.WicketExamplePage;
 import 
org.apache.wicket.extensions.ajax.markup.html.form.upload.UploadProgressBar;
 import org.apache.wicket.markup.html.basic.Label;
@@ -173,7 +172,7 @@ public class UploadPage extends WicketExamplePage
 
                // Add folder view
                add(new Label("dir", uploadFolder.getAbsolutePath()));
-               fileListView = new FileListView("fileList", new 
LoadableDetachableModel<List<File>>()
+               fileListView = new FileListView("fileList", new 
LoadableDetachableModel<>()
                {
                        @Override
                        protected List<File> load()
@@ -216,6 +215,6 @@ public class UploadPage extends WicketExamplePage
 
        private Folder getUploadFolder()
        {
-               return ((UploadApplication)Application.get()).getUploadFolder();
+               return UploadApplication.getInstance().getUploadFolder();
        }
 }
diff --git 
a/wicket-examples/src/main/java/org/apache/wicket/examples/upload/UploadToResourcePage.html
 
b/wicket-examples/src/main/java/org/apache/wicket/examples/upload/UploadToResourcePage.html
new file mode 100644
index 0000000000..ba2097cddf
--- /dev/null
+++ 
b/wicket-examples/src/main/java/org/apache/wicket/examples/upload/UploadToResourcePage.html
@@ -0,0 +1,44 @@
+<html xmlns:wicket="http://wicket.apache.org";>
+<head>
+    <title>Wicket Examples - upload</title>
+       <style>
+               legend { border: 1px solid #e9601a; background-color: #bbb; 
color: #fff; padding: 4px;}
+               fieldset { border: 1px solid #e9601a; padding: 10px; 
margin-top: 10px;}
+       </style>
+</head>
+<body>
+    <wicket:extend>
+               <p>Wicket can upload to a <em>mounted resource.</em></p>
+               <p>This upload is done via jQuery AJAX and <em>does not</em> 
block Wicket AJAX</p>
+               <p><em>Upload progressbar</em> has been updated to work with 
this component</p>
+               <br/>
+
+               <div>
+                <fieldset>
+                        <legend>Upload a file(s) to a <em>mounted 
resource</em></legend>
+                       <p>
+                               <label wicket:for="fileInput">File</label>
+                               <input wicket:id="fileInput" type="file" 
multiple="multiple"/>
+                       </p>
+                       <form wicket:id="form">
+                               <p>
+                                       <input 
wicket:id="allowToLeavePageWhileUploading" type="checkbox"/>
+                                       <label 
wicket:for="allowToLeavePageWhileUploading">Block leaving page while upload is 
happening?</label>
+                               </p>
+                       </form>
+                        <div>
+                                <span wicket:id="progress">[[upload 
progressbar]]</span>
+                        </div>
+                        <input wicket:id="upload" type="button" value="Upload 
to a resource"/>
+                        <input wicket:id="cancelUpload" type="button" 
value="Cancel upload via AJAX request"/>
+                        <input wicket:id="cancelUploadClientSide" 
type="button" value="Cancel upload at client side"/>
+                        <button wicket:id="counter" type="button">Click 
me</button>
+                </fieldset>
+               </div>
+               <br/>
+               <div>
+                       <span wicket:id="uploadFeedback"/>
+               </div>
+       </wicket:extend>
+</body>
+</html>
diff --git 
a/wicket-examples/src/main/java/org/apache/wicket/examples/upload/UploadToResourcePage.java
 
b/wicket-examples/src/main/java/org/apache/wicket/examples/upload/UploadToResourcePage.java
new file mode 100644
index 0000000000..e9e43e9872
--- /dev/null
+++ 
b/wicket-examples/src/main/java/org/apache/wicket/examples/upload/UploadToResourcePage.java
@@ -0,0 +1,263 @@
+/*
+ * 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.examples.upload;
+
+import java.util.List;
+import org.apache.wicket.Component;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.markup.html.AjaxLink;
+import org.apache.wicket.ajax.markup.html.form.AjaxCheckBox;
+import org.apache.wicket.behavior.Behavior;
+import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
+import org.apache.wicket.event.IEvent;
+import org.apache.wicket.examples.WicketExamplePage;
+import 
org.apache.wicket.extensions.ajax.markup.html.form.upload.UploadProgressBar;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
+import org.apache.wicket.markup.head.OnEventHeaderItem;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.upload.resource.IUploadsFileManager;
+import 
org.apache.wicket.markup.html.form.upload.resource.FileUploadToResourceField;
+import org.apache.wicket.markup.html.panel.FeedbackPanel;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+
+/**
+ * Upload example.
+ *
+ * @author reiern70
+ */
+public class UploadToResourcePage extends WicketExamplePage
+{
+       private static class UpdateInEachAjaxRequestBehavior extends Behavior
+       {
+
+               @Override
+               public void bind(Component component)
+               {
+                       component.setOutputMarkupPlaceholderTag(true);
+               }
+
+               @Override
+               public void onEvent(Component component, IEvent<?> event)
+               {
+                       if (event.getPayload() instanceof AjaxRequestTarget)
+                       {
+                               
((AjaxRequestTarget)event.getPayload()).add(component);
+                       }
+               }
+       }
+
+       private final FeedbackPanel uploadFeedback;
+
+       private final FileUploadToResourceField fileUploadToResourceField;
+
+       private boolean uploading;
+
+       private int counter;
+
+       private final IModel<Boolean> allowToLeavePageWhileUploading = 
Model.of(true);
+
+       /**
+        * Constructor.
+        *
+        * @param parameters
+        *            Page parameters
+        */
+       public UploadToResourcePage(final PageParameters parameters)
+       {
+               fileUploadToResourceField = new 
FileUploadToResourceField("fileInput")
+               {
+
+                       private void updateFeedback(AjaxRequestTarget target)
+                       {
+                               target.add(uploadFeedback);
+                               uploading = false;
+                       }
+
+                       @Override
+                       protected void onUploadSuccess(AjaxRequestTarget 
target, List<UploadInfo> infos)
+                       {
+                               for (UploadInfo uploadInfo : infos) {
+                                       info("File " + 
uploadInfo.getClientFileName() + " successfully uploaded. Size: " + 
uploadInfo.getSize() + "bytes. ContentType: "
+                                                       + 
uploadInfo.getContentType() +". Stored at " +  
uploadInfo.getFile().getAbsolutePath());
+                               }
+                               updateFeedback(target);
+                       }
+
+                       @Override
+                       protected void onUploadFailure(AjaxRequestTarget 
target, String errorInfo)
+                       {
+                               error("Upload failed with error: " + errorInfo);
+                               updateFeedback(target);
+                       }
+
+                       @Override
+                       public void startUpload(IPartialPageRequestHandler 
target) {
+                               if (allowToLeavePageWhileUploading.getObject())
+                               {
+                                       
target.prependJavaScript("Wicket.CurrentUpload = {};");
+                               }
+                               super.startUpload(target);
+                       }
+
+                       @Override
+                       protected void onUploadCanceled(AjaxRequestTarget 
target)
+                       {
+                               info("You have canceled the upload!");
+                               updateFeedback(target);
+                       }
+
+                       @Override
+                       protected CharSequence getClientSideSuccessCallBack() {
+                               return "function() {Wicket.CurrentUpload = 
null}";
+                       }
+
+                       @Override
+                       protected CharSequence 
getClientSideUploadErrorCallBack()
+                       {
+                               return "function() {Wicket.CurrentUpload = 
null}";
+                       }
+
+                       @Override
+                       protected String getClientSideCancelCallBack()
+                       {
+                               return "function() {Wicket.CurrentUpload = 
null}";
+                       }
+
+                       @Override
+                       protected IUploadsFileManager fileManager()
+                       {
+                               return getUploadsFileManager();
+                       }
+               };
+               add(fileUploadToResourceField);
+
+               Form<Void> form = new Form<>("form");
+               add(form);
+               form.add(new AjaxCheckBox("allowToLeavePageWhileUploading", 
allowToLeavePageWhileUploading) {
+                       @Override
+                       protected void onUpdate(AjaxRequestTarget target) {
+
+                       }
+               });
+               final UploadProgressBar uploadProgressBar = new 
UploadProgressBar("progress", fileUploadToResourceField)
+               {
+
+                       @Override
+                       protected String getOnProgressUpdatedCallBack()
+                       {
+                               return "function(percent) { 
console.log(percent);}";
+                       }
+               };
+               add(uploadProgressBar);
+               add(new AjaxLink<Void>("upload")
+               {
+                       @Override
+                       public void onClick(AjaxRequestTarget target)
+                       {
+                               uploading = true;
+                               fileUploadToResourceField.startUpload(target);
+                               uploadProgressBar.start(target);
+                               target.add(uploadFeedback);
+                       }
+
+                       @Override
+                       protected void onConfigure()
+                       {
+                               super.onConfigure();
+                               setVisible(!uploading);
+                       }
+               }.add(new UpdateInEachAjaxRequestBehavior()));
+
+               add(new AjaxLink<Void>("cancelUpload")
+               {
+                       @Override
+                       public void onClick(AjaxRequestTarget target)
+                       {
+                               fileUploadToResourceField.cancelUpload(target);
+                               target.add(uploadFeedback);
+                       }
+
+                       @Override
+                       protected void onConfigure()
+                       {
+                               super.onConfigure();
+                               setVisible(uploading);
+                       }
+               }.add(new UpdateInEachAjaxRequestBehavior()));
+
+
+               add(new WebMarkupContainer("cancelUploadClientSide")
+               {
+
+                       @Override
+                       public void renderHead(IHeaderResponse response) {
+                               super.renderHead(response);
+                               
response.render(OnEventHeaderItem.forComponent(this, "click", 
fileUploadToResourceField.getTriggerCancelUploadScript()));
+                       }
+
+                       @Override
+                       protected void onConfigure()
+                       {
+                               super.onConfigure();
+                               setVisible(uploading);
+                       }
+
+               }.add(new UpdateInEachAjaxRequestBehavior()));
+
+               add(new AjaxLink<Void>("counter")
+               {
+
+                       @Override
+                       public void onClick(AjaxRequestTarget target)
+                       {
+                               counter ++;
+                               target.add(this);
+                       }
+
+               }.setBody(()-> "Click me: upload does not blocks normal wicket 
AJAX. I was clicked " + counter).setOutputMarkupId(true));
+               // Create feedback panels
+               uploadFeedback = new FeedbackPanel("uploadFeedback");
+               uploadFeedback.setOutputMarkupId(true);
+
+               // Add uploadFeedback to the page itself
+               add(uploadFeedback);
+
+       }
+
+       @Override
+       public void renderHead(IHeaderResponse response) {
+               super.renderHead(response);
+               response.render(
+                               
OnDomReadyHeaderItem.forScript("$(window).bind('beforeunload', function(){ if 
(Wicket.CurrentUpload != null) { return 'leave?'; }});"));
+       }
+
+       @Override
+       protected void onBeforeRender()
+       {
+               super.onBeforeRender();
+               uploading = false;
+       }
+
+       private IUploadsFileManager getUploadsFileManager()
+       {
+               return UploadApplication.getInstance().getUploadsFileManager();
+       }
+}
diff --git 
a/wicket-examples/src/main/resources/org/apache/wicket/examples/homepage/HomePage.html
 
b/wicket-examples/src/main/resources/org/apache/wicket/examples/homepage/HomePage.html
index 46631905f3..4c339f81c5 100644
--- 
a/wicket-examples/src/main/resources/org/apache/wicket/examples/homepage/HomePage.html
+++ 
b/wicket-examples/src/main/resources/org/apache/wicket/examples/homepage/HomePage.html
@@ -23,6 +23,7 @@
        <tr><td align="right"><a 
href="authorization">authorization</a></td><td> - Demonstrates authorization 
for pages and components.</td></tr>
        <tr><td align="right"><a href="upload/single">upload</a></td><td> - 
Single file upload.</td></tr>
        <tr><td align="right"><a href="upload/multi">upload</a></td><td> - 
Multiple file upload.</td></tr>
+       <tr><td align="right"><a 
href="upload/singleUploadToResource">upload</a></td><td> - Single AJAX file 
upload to a resource.</td></tr>
        <tr><td align="right"><a href="template">template</a></td><td> - 
Templating example.</td></tr>
        <tr><td align="right"><a href="mailtemplate">mail template</a></td><td> 
- Generate mail templates out of Page, Panel or TextTemplate.</td></tr>
        <tr><td align="right"><a href="stateless">stateless</a></td><td> - 
Demonstrates stateless pages/sessions.</td></tr>
diff --git 
a/wicket-examples/src/main/resources/org/apache/wicket/examples/style.css 
b/wicket-examples/src/main/resources/org/apache/wicket/examples/style.css
index f27f2c2dac..81fcf9216f 100644
--- a/wicket-examples/src/main/resources/org/apache/wicket/examples/style.css
+++ b/wicket-examples/src/main/resources/org/apache/wicket/examples/style.css
@@ -305,7 +305,7 @@ button.button--alert {
 input[type=submit],
 input[type=reset],
 input[type=button],
-button {
+button, a.button {
   background: #FF9925;
   color: #fff; }
 .button:hover,
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 d938e370e3..2b7e4aeca2 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.core.request.handler.IPartialPageRequestHandler;
 import org.apache.wicket.markup.head.CssHeaderItem;
 import org.apache.wicket.markup.head.IHeaderResponse;
 import org.apache.wicket.markup.head.JavaScriptHeaderItem;
@@ -29,7 +30,6 @@ import org.apache.wicket.markup.html.WebMarkupContainer;
 import org.apache.wicket.markup.html.form.Form;
 import org.apache.wicket.markup.html.form.upload.FileUploadField;
 import org.apache.wicket.markup.html.panel.Panel;
-import org.apache.wicket.model.IModel;
 import org.apache.wicket.model.StringResourceModel;
 import org.apache.wicket.request.resource.CssResourceReference;
 import org.apache.wicket.request.resource.JavaScriptResourceReference;
@@ -109,7 +109,7 @@ public class UploadProgressBar extends Panel
 
        private static final long serialVersionUID = 1L;
 
-       private final Form<?> form;
+       private Form<?> form;
 
        private MarkupContainer statusDiv;
 
@@ -117,6 +117,30 @@ public class UploadProgressBar extends Panel
 
        private final FileUploadField uploadField;
 
+       /**
+        * Constructor that will display the upload progress bar for every 
submit of the given form.
+        *
+        * @param id
+        *            component id (not null)
+        * @param uploadField
+        *            the file upload field to check for a file upload, or null 
to display the upload
+        *            field for every submit of the given form
+        */
+       public UploadProgressBar(final String id,  final FileUploadField 
uploadField)
+       {
+               super(id);
+
+               this.uploadField = uploadField;
+               if (uploadField != null)
+               {
+                       uploadField.setOutputMarkupId(true);
+               }
+
+               setRenderBodyOnly(true);
+       }
+
+
+
        /**
         * Constructor that will display the upload progress bar for every 
submit of the given form.
         * 
@@ -163,7 +187,11 @@ public class UploadProgressBar extends Panel
        protected void onInitialize()
        {
                super.onInitialize();
-               getCallbackForm().setOutputMarkupId(true);
+               Form<?> form = getCallbackForm();
+               if (form != null)
+               {
+                       form.setOutputMarkupId(true);
+               }
 
                barDiv = newBarComponent("bar");
                add(barDiv);
@@ -228,29 +256,53 @@ public class UploadProgressBar extends Panel
 
                ResourceReference ref = new 
SharedResourceReference(RESOURCE_NAME);
 
-               final String uploadFieldId = (uploadField == null) ? "" : 
uploadField.getMarkupId();
+               final String uploadFieldId = (uploadField == null) ? "null" : 
("'" + uploadField.getMarkupId() + "'");
 
-               final String status = new 
StringResourceModel(RESOURCE_STARTING, this, (IModel<?>)null).getString();
+               final String status = new 
StringResourceModel(RESOURCE_STARTING, this, null).getString();
 
-               CharSequence url = urlFor(ref, 
UploadStatusResource.newParameter(getPage().getId()));
+               CharSequence url = form != null ? urlFor(ref, 
UploadStatusResource.newParameter(getPage().getId())) :
+                               urlFor(ref, 
UploadStatusResource.newParameter(uploadField.getMarkupId()));
 
                StringBuilder builder = new StringBuilder(128);
                Formatter formatter = new Formatter(builder);
 
-               formatter.format(
-                       "new Wicket.WUPB('%s', '%s', '%s', '%s', '%s', '%s');",
-                               getCallbackForm().getMarkupId(), 
statusDiv.getMarkupId(), barDiv.getMarkupId(), url, uploadFieldId,
-                       status);
+               Form<?> form = getCallbackForm();
+
+               formatter.format(getVarName() + " = new Wicket.WUPB(%s, '%s', 
'%s', '%s', %s, '%s', %s);",
+                               form != null ? "'" + form.getMarkupId() + "'" : 
"null", statusDiv.getMarkupId(), barDiv.getMarkupId(), url, uploadFieldId,
+                       status, getOnProgressUpdatedCallBack());
                
response.render(OnDomReadyHeaderItem.forScript(builder.toString()));
        }
 
+       /**
+        * Allows to pass a JavaScript function that is called when progress in 
updated.
+        *
+        * @return A JavaScript function.
+        */
+       protected String getOnProgressUpdatedCallBack()
+       {
+               return "function(percent) {}";
+       }
+
+       private String getVarName() {
+               return "window.upb_" + barDiv.getMarkupId();
+       }
+
+
+       public void start(IPartialPageRequestHandler handler)
+       {
+               handler.appendJavaScript(getVarName() + ".start();");
+       }
+
        /**
         * Form on where will be installed the JavaScript callback to present 
the progress bar.
         * 
         * @return form
         */
-       private Form<?> getCallbackForm()
-       {
-               return form.getRootForm();
+       private Form<?> getCallbackForm() {
+               if (form != null) {
+                       return form.getRootForm();
+               }
+               return null;
        }
 }
diff --git 
a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/form/upload/progressbar.js
 
b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/form/upload/progressbar.js
index 6cd0ad930b..f540347786 100644
--- 
a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/form/upload/progressbar.js
+++ 
b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/form/upload/progressbar.js
@@ -25,16 +25,19 @@
        Wicket.WUPB = Wicket.Class.create();
        Wicket.WUPB.prototype = {
 
-               initialize : function(formid, statusid, barid, url, fileid, 
initialStatus) {
+               initialize : function(formid, statusid, barid, url, fileid, 
initialStatus, onProgressUpdated) {
                        this.statusid = statusid;
                        this.barid = barid;
                        this.url = url;
                        this.fileid = fileid;
                        this.initialStatus = initialStatus;
+                       this.onProgressUpdated = onProgressUpdated;
 
-                       var formElement = Wicket.$(formid);
-                       this.originalCallback = formElement.onsubmit;
-                       formElement.onsubmit = Wicket.bind(this.submitCallback, 
this);
+                       if (formid) {
+                               var formElement = Wicket.$(formid);
+                               this.originalCallback = formElement.onsubmit;
+                               formElement.onsubmit = 
Wicket.bind(this.submitCallback, this);
+                       }
                },
 
                submitCallback : function() {
@@ -55,8 +58,14 @@
                        if (displayprogress) {
                                this.setPercent(0);
                                this.setStatus(this.initialStatus);
-                               
Wicket.$(this.statusid).removeAttribute('hidden');
-                               Wicket.$(this.barid).removeAttribute('hidden');
+                               var $statusId = Wicket.$(this.statusid);
+                               if ($statusId != null) {
+                                       Wicket.DOM.show($statusId);
+                               }
+                               var $barid = Wicket.$(this.barid);
+                               if ($barid != null) {
+                                       Wicket.DOM.show($barid);
+                               }
                                this.scheduleUpdate();
                        }
                },
@@ -64,15 +73,24 @@
                setStatus : function(status) {
                        var label = document.createElement("label");
                        label.innerHTML = status;
-                       var oldLabel = Wicket.$(this.statusid).firstChild;
-                       if( oldLabel != null){
-                               Wicket.$(this.statusid).removeChild(oldLabel);
+                       var $statusId = Wicket.$(this.statusid);
+                       if ($statusId != null) {
+                               var oldLabel = $statusId.firstChild;
+                               if (oldLabel != null){
+                                       $statusId.removeChild(oldLabel);
+                               }
+                               $statusId.appendChild(label);
                        }
-                       Wicket.$(this.statusid).appendChild(label);
                },
 
                setPercent : function(progressPercent) {
-                       Wicket.$(this.barid).firstChild.firstChild.style.width 
= progressPercent + '%';
+                       var barId = Wicket.$(this.barid);
+                       if (barId != null && barId.firstChild != null && 
barId.firstChild.firstChild != null) {
+                               barId.firstChild.firstChild.style.width = 
progressPercent + '%';
+                       }
+                       if (this.onProgressUpdated) {
+                               this.onProgressUpdated(progressPercent);
+                       }
                },
 
                scheduleUpdate : function(){
@@ -117,8 +135,14 @@
                        this.iframe = null;
 
                        if (progressPercent === '100') {
-                               Wicket.$(this.statusid).setAttribute('hidden', 
'');
-                               Wicket.$(this.barid).setAttribute('hidden', '');
+                               var $statusId = Wicket.$(this.statusid);
+                               if ($statusId != null) {
+                                       Wicket.DOM.hide($statusId);
+                               }
+                               var $barid = Wicket.$(this.barid);
+                               if ($barid != null) {
+                                       Wicket.DOM.hide($barid);
+                               }
                        } else {
                                this.scheduleUpdate();
                        }

Reply via email to