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(); }
