You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by re...@apache.org on 2023/04/14 08:50:54 UTC

[wicket] 01/01: [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

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 <re...@gmail.com>
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();
 			}