| /* |
| * 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.extensions.ajax; |
| |
| import java.util.Locale; |
| import javax.servlet.http.Cookie; |
| import org.apache.wicket.Component; |
| import org.apache.wicket.IRequestListener; |
| import org.apache.wicket.WicketRuntimeException; |
| import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior; |
| import org.apache.wicket.ajax.AjaxRequestTarget; |
| import org.apache.wicket.ajax.json.JSONFunction; |
| import org.apache.wicket.behavior.Behavior; |
| 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.request.Response; |
| import org.apache.wicket.request.cycle.RequestCycle; |
| import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler; |
| import org.apache.wicket.request.http.WebResponse; |
| import org.apache.wicket.request.mapper.parameter.PageParameters; |
| import org.apache.wicket.request.resource.ContentDisposition; |
| import org.apache.wicket.request.resource.IResource; |
| import org.apache.wicket.request.resource.IResource.Attributes; |
| import org.apache.wicket.request.resource.ResourceReference; |
| import org.apache.wicket.resource.JQueryPluginResourceReference; |
| import org.apache.wicket.util.cookies.CookieDefaults; |
| import org.apache.wicket.util.lang.Args; |
| |
| import com.github.openjson.JSONObject; |
| |
| /** |
| * Download resources via Ajax. |
| * <p> |
| * Usage: |
| * |
| * <pre> |
| * final AjaxDownloadBehavior download = new AjaxDownloadBehavior(resource); |
| * add(download); |
| * |
| * add(new AjaxButton("download") |
| * { |
| * @Override |
| * protected void onSubmit(IPartialPageRequestHandler handler, Form<?> form) |
| * { |
| * download.initiate(handler); |
| * } |
| * }); |
| * </pre> |
| * |
| * <p>To set the name of the downloaded resource make use of |
| * {@link org.apache.wicket.request.resource.ResourceStreamResource#setFileName(String)} or |
| * {@link org.apache.wicket.request.resource.AbstractResource.ResourceResponse#setFileName(String)}</p> |
| * |
| * @author svenmeier |
| * @author Martin Grigorov |
| * @author Maxim Solodovnik |
| */ |
| public class AjaxDownloadBehavior extends AbstractDefaultAjaxBehavior |
| { |
| private static final long serialVersionUID = 1L; |
| |
| public enum Location { |
| /** |
| * The resource will be downloaded into a {@code blob}. |
| * <p> |
| * This is recommended for modern browsers. |
| */ |
| Blob, |
| |
| /** |
| * The resource will be downloaded via a temporary created iframe, the resource has to be a |
| * {@link ContentDisposition#ATTACHMENT}. |
| * <p> |
| * This is recommended when there are resources in the DOM which will be |
| * closed automatically on JavaScript <em>unload</em> event, like WebSockets. |
| * Supports both <em>success</em> and <em>failure</em> callbacks! |
| */ |
| IFrame, |
| |
| /** |
| * The resource will be downloaded by changing the location of the current DOM document, |
| * the resource has to be a {@link ContentDisposition#ATTACHMENT}. |
| * <p> |
| * Note: This will trigger JavaScript <em>unload</em> event on the page! |
| * Does not support {@link AjaxDownloadBehavior#onDownloadFailed(AjaxRequestTarget)} callback, |
| * i.e. it is not possible to detect when the download has failed! |
| */ |
| SameWindow, |
| |
| /** |
| * The resource will be downloaded in a new browser window by using JavaScript <code>window.open()</code> API, |
| * the resource has to be a {@link ContentDisposition#INLINE}. |
| */ |
| NewWindow |
| } |
| |
| /** |
| * Name of parameter used to transfer the download identifier to the resource. |
| * |
| * @see #markCompleted(Attributes) |
| */ |
| private static final String RESOURCE_PARAMETER_NAME = "wicket-ajaxdownload"; |
| |
| private static final ResourceReference JS = new JQueryPluginResourceReference( |
| AjaxDownloadBehavior.class, "wicket-ajaxdownload.js"); |
| |
| private final ResourceReference resourceReference; |
| |
| private final ResourceBehavior resourceBehavior; |
| |
| private PageParameters resourceParameters; |
| |
| private Location location = Location.Blob; |
| |
| private CookieDefaults.SameSite sameSite = null; |
| |
| /** |
| * Download of a {@link IResource}. |
| * |
| * @param resource |
| * resource to download |
| */ |
| public AjaxDownloadBehavior(IResource resource) |
| { |
| Args.notNull(resource, "resource"); |
| this.resourceBehavior = new ResourceBehavior(resource); |
| this.resourceReference = null; |
| } |
| |
| /** |
| * Download of a {@link ResourceReference}. |
| * <p> |
| * The {@link IResource} returned by {@link ResourceReference#getResource()} must call |
| * {@link #markCompleted(Attributes)} when responding, otherwise the callback |
| * {@link #onDownloadSuccess(AjaxRequestTarget)} will not work. |
| * |
| * @param reference |
| * reference to resource to download |
| */ |
| public AjaxDownloadBehavior(ResourceReference reference) |
| { |
| this(reference, null); |
| } |
| |
| /** |
| * Download of a {@link ResourceReference}. |
| * <p> |
| * The {@link IResource} returned by {@link ResourceReference#getResource()} must call |
| * {@link #markCompleted(Attributes)} when responding, otherwise the callback |
| * {@link #onDownloadSuccess(AjaxRequestTarget)} will not work. |
| * |
| * @param reference |
| * reference to resource to download |
| * @param resourceParameters |
| * parameters for the resource |
| */ |
| public AjaxDownloadBehavior(ResourceReference reference, PageParameters resourceParameters) |
| { |
| this.resourceBehavior = null; |
| |
| this.resourceReference = Args.notNull(reference, "reference"); |
| this.resourceParameters = resourceParameters; |
| } |
| |
| @Override |
| protected void onBind() |
| { |
| super.onBind(); |
| |
| if (resourceBehavior != null) |
| { |
| getComponent().add(resourceBehavior); |
| } |
| } |
| |
| @Override |
| protected void onUnbind() |
| { |
| super.onUnbind(); |
| |
| if (resourceBehavior != null) |
| { |
| getComponent().remove(resourceBehavior); |
| } |
| } |
| |
| /** |
| * Call this method to initiate the download. You can use the {@link #resourceParameters} to dynamically pass |
| * information to the {@link org.apache.wicket.request.resource.IResource} in order to generate contents. |
| * |
| * @param handler |
| * the initiating RequestHandler |
| * @param resourceParameters |
| * Some PageParameters that might be used by the resource in order to generate content |
| */ |
| public void initiate(IPartialPageRequestHandler handler, PageParameters resourceParameters) |
| { |
| this.resourceParameters = resourceParameters; |
| initiate(handler); |
| } |
| |
| /** |
| * Call this method to initiate the download. |
| * |
| * @param handler |
| * the initiating RequestHandler |
| */ |
| public void initiate(IPartialPageRequestHandler handler) |
| { |
| if (getComponent() == null) |
| { |
| throw new WicketRuntimeException("not bound to a component"); |
| } |
| |
| CharSequence url; |
| if (resourceBehavior == null) |
| { |
| if (resourceReference.canBeRegistered()) |
| { |
| getComponent().getApplication().getResourceReferenceRegistry() |
| .registerResourceReference(resourceReference); |
| } |
| |
| PageParameters parameters = new PageParameters(); |
| if (resourceParameters != null) |
| { |
| parameters.mergeWith(resourceParameters); |
| } |
| parameters.set(RESOURCE_PARAMETER_NAME, getName()); |
| |
| url = getComponent().getRequestCycle() |
| .urlFor(new ResourceReferenceRequestHandler(resourceReference, parameters)); |
| } |
| else |
| { |
| url = resourceBehavior.getUrl(); |
| } |
| |
| JSONObject settings = new JSONObject(); |
| settings.put("attributes", new JSONFunction(renderAjaxAttributes(getComponent()))); |
| settings.put("name", getName()); |
| settings.put("downloadUrl", url); |
| settings.put("sameSite", generateSameSiteAttribute()); |
| settings.put("method", getLocation().name().toLowerCase(Locale.ROOT)); |
| |
| handler.appendJavaScript(String.format("Wicket.AjaxDownload.initiate(%s);", settings)); |
| |
| onBeforeDownload(handler); |
| } |
| |
| private String generateSameSiteAttribute() { |
| if (sameSite == null) |
| { |
| return ""; |
| } |
| |
| StringBuilder stringBuffer = new StringBuilder(30); |
| if (sameSite.equals(CookieDefaults.SameSite.None)) |
| { |
| stringBuffer.append("; Secure"); |
| } |
| |
| stringBuffer.append("; SameSite="); |
| stringBuffer.append(sameSite.name()); |
| |
| return stringBuffer.toString(); |
| |
| } |
| |
| protected void onBeforeDownload(IPartialPageRequestHandler handler) |
| { |
| } |
| |
| /** |
| * A callback executed when the download of the resource finished successfully. |
| * |
| * @param target The Ajax request handler |
| */ |
| protected void onDownloadSuccess(AjaxRequestTarget target) |
| { |
| } |
| |
| /** |
| * A callback executed when the download of the resource failed for some reason, |
| * e.g. an error at the server side. |
| * <p> |
| * Since the HTTP status code of the download is not available to Wicket, any HTML in the resource response |
| * will be interpreted as a failure HTTP status message. Thus is it not possible to download HTML resources |
| * via {@link AjaxDownloadBehavior}. |
| * |
| * @param target The Ajax request handler |
| */ |
| protected void onDownloadFailed(AjaxRequestTarget target) |
| { |
| } |
| |
| /** |
| * A callback executed when the download of the resource finished successfully or with a failure. |
| * |
| * @param target The Ajax request handler |
| */ |
| protected void onDownloadCompleted(AjaxRequestTarget target) |
| { |
| } |
| |
| @Override |
| public void renderHead(Component component, IHeaderResponse response) |
| { |
| super.renderHead(component, response); |
| |
| response.render(JavaScriptHeaderItem.forReference(JS)); |
| } |
| |
| @Override |
| protected void respond(AjaxRequestTarget target) |
| { |
| String result = getComponent().getRequest().getRequestParameters().getParameterValue("result").toOptionalString(); |
| if ("success".equals(result)) { |
| onDownloadSuccess(target); |
| } else if ("failed".equals(result)) { |
| onDownloadFailed(target); |
| } |
| onDownloadCompleted(target); |
| } |
| |
| public Location getLocation() { |
| return location; |
| } |
| |
| public AjaxDownloadBehavior setLocation(final Location location) { |
| this.location = Args.notNull(location, "location"); |
| return this; |
| } |
| |
| /** |
| * Identifying name of this behavior. |
| */ |
| private String getName() |
| { |
| return String.format("wicket-ajaxdownload-%s-%s", getComponent().getMarkupId(), |
| getComponent().getBehaviorId(this)); |
| } |
| |
| /** |
| * The behavior responding with the actual resource. |
| */ |
| private class ResourceBehavior extends Behavior implements IRequestListener |
| { |
| private static final long serialVersionUID = 1L; |
| private final IResource resource; |
| |
| private ResourceBehavior(IResource resource) |
| { |
| this.resource = Args.notNull(resource, "resource"); |
| } |
| |
| @Override |
| public boolean rendersPage() |
| { |
| return false; |
| } |
| |
| @Override |
| public void onRequest() |
| { |
| final RequestCycle requestCycle = RequestCycle.get(); |
| final Response response = requestCycle.getResponse(); |
| ((WebResponse) response).addCookie(cookie(getName())); |
| |
| Attributes a = new Attributes(requestCycle.getRequest(), response, null); |
| |
| resource.respond(a); |
| } |
| |
| public CharSequence getUrl() |
| { |
| return getComponent().urlForListener(this, null); |
| } |
| } |
| |
| /** |
| * Mark a resource as complete. |
| * <p> |
| * Has to be called from {@link IResource#respond(Attributes)} when downloaded via |
| * {@link #AjaxDownloadBehavior(IResource)}. |
| * |
| * @param attributes |
| * resource attributes |
| */ |
| public static void markCompleted(IResource.Attributes attributes) |
| { |
| String cookieName = attributes.getParameters().get(RESOURCE_PARAMETER_NAME).toString(); |
| |
| ((WebResponse)attributes.getResponse()).addCookie(cookie(cookieName)); |
| } |
| |
| |
| private static Cookie cookie(String name) |
| { |
| Cookie cookie = new Cookie(name, "complete"); |
| |
| // has to be on root, otherwise JavaScript will not be able to access the |
| // cookie when it is set from a different path - which is the case when a |
| // ResourceReference is used |
| cookie.setPath("/"); |
| |
| return cookie; |
| } |
| |
| /** |
| * @return The {@link org.apache.wicket.util.cookies.CookieDefaults.SameSite} attribute to be used for the complete download. |
| */ |
| public CookieDefaults.SameSite getSameSite() |
| { |
| return sameSite; |
| } |
| |
| /** |
| * Setter for the same {@link org.apache.wicket.util.cookies.CookieDefaults.SameSite} |
| * |
| * @param sameSite The non-null sameSite attribute |
| */ |
| public void setSameSite(CookieDefaults.SameSite sameSite) |
| { |
| Args.notNull(sameSite, "sameSite"); |
| this.sameSite = sameSite; |
| } |
| } |