001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.extensions.ajax; 018 019import java.util.Locale; 020import javax.servlet.http.Cookie; 021import org.apache.wicket.Component; 022import org.apache.wicket.IRequestListener; 023import org.apache.wicket.WicketRuntimeException; 024import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior; 025import org.apache.wicket.ajax.AjaxRequestTarget; 026import org.apache.wicket.ajax.json.JSONFunction; 027import org.apache.wicket.behavior.Behavior; 028import org.apache.wicket.core.request.handler.IPartialPageRequestHandler; 029import org.apache.wicket.markup.head.IHeaderResponse; 030import org.apache.wicket.markup.head.JavaScriptHeaderItem; 031import org.apache.wicket.request.Response; 032import org.apache.wicket.request.cycle.RequestCycle; 033import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler; 034import org.apache.wicket.request.http.WebResponse; 035import org.apache.wicket.request.mapper.parameter.PageParameters; 036import org.apache.wicket.request.resource.ContentDisposition; 037import org.apache.wicket.request.resource.IResource; 038import org.apache.wicket.request.resource.IResource.Attributes; 039import org.apache.wicket.request.resource.ResourceReference; 040import org.apache.wicket.resource.JQueryPluginResourceReference; 041import org.apache.wicket.util.cookies.CookieDefaults; 042import org.apache.wicket.util.lang.Args; 043 044import com.github.openjson.JSONObject; 045 046/** 047 * Download resources via Ajax. 048 * <p> 049 * Usage: 050 * 051 * <pre> 052 * final AjaxDownloadBehavior download = new AjaxDownloadBehavior(resource); 053 * add(download); 054 * 055 * add(new AjaxButton("download") 056 * { 057 * @Override 058 * protected void onSubmit(IPartialPageRequestHandler handler, Form<?> form) 059 * { 060 * download.initiate(handler); 061 * } 062 * }); 063 * </pre> 064 * 065 * <p>To set the name of the downloaded resource make use of 066 * {@link org.apache.wicket.request.resource.ResourceStreamResource#setFileName(String)} or 067 * {@link org.apache.wicket.request.resource.AbstractResource.ResourceResponse#setFileName(String)}</p> 068 * 069 * @author svenmeier 070 * @author Martin Grigorov 071 * @author Maxim Solodovnik 072 */ 073public class AjaxDownloadBehavior extends AbstractDefaultAjaxBehavior 074{ 075 private static final long serialVersionUID = 1L; 076 077 public enum Location { 078 /** 079 * The resource will be downloaded into a {@code blob}. 080 * <p> 081 * This is recommended for modern browsers. 082 */ 083 Blob, 084 085 /** 086 * The resource will be downloaded via a temporary created iframe, the resource has to be a 087 * {@link ContentDisposition#ATTACHMENT}. 088 * <p> 089 * This is recommended when there are resources in the DOM which will be 090 * closed automatically on JavaScript <em>unload</em> event, like WebSockets. 091 * Supports both <em>success</em> and <em>failure</em> callbacks! 092 */ 093 IFrame, 094 095 /** 096 * The resource will be downloaded by changing the location of the current DOM document, 097 * the resource has to be a {@link ContentDisposition#ATTACHMENT}. 098 * <p> 099 * Note: This will trigger JavaScript <em>unload</em> event on the page! 100 * Does not support {@link AjaxDownloadBehavior#onDownloadFailed(AjaxRequestTarget)} callback, 101 * i.e. it is not possible to detect when the download has failed! 102 */ 103 SameWindow, 104 105 /** 106 * The resource will be downloaded in a new browser window by using JavaScript <code>window.open()</code> API, 107 * the resource has to be a {@link ContentDisposition#INLINE}. 108 */ 109 NewWindow 110 } 111 112 /** 113 * Name of parameter used to transfer the download identifier to the resource. 114 * 115 * @see #markCompleted(Attributes) 116 */ 117 private static final String RESOURCE_PARAMETER_NAME = "wicket-ajaxdownload"; 118 119 private static final ResourceReference JS = new JQueryPluginResourceReference( 120 AjaxDownloadBehavior.class, "wicket-ajaxdownload.js"); 121 122 private final ResourceReference resourceReference; 123 124 private final ResourceBehavior resourceBehavior; 125 126 private PageParameters resourceParameters; 127 128 private Location location = Location.Blob; 129 130 private CookieDefaults.SameSite sameSite = null; 131 132 /** 133 * Download of a {@link IResource}. 134 * 135 * @param resource 136 * resource to download 137 */ 138 public AjaxDownloadBehavior(IResource resource) 139 { 140 Args.notNull(resource, "resource"); 141 this.resourceBehavior = new ResourceBehavior(resource); 142 this.resourceReference = null; 143 } 144 145 /** 146 * Download of a {@link ResourceReference}. 147 * <p> 148 * The {@link IResource} returned by {@link ResourceReference#getResource()} must call 149 * {@link #markCompleted(Attributes)} when responding, otherwise the callback 150 * {@link #onDownloadSuccess(AjaxRequestTarget)} will not work. 151 * 152 * @param reference 153 * reference to resource to download 154 */ 155 public AjaxDownloadBehavior(ResourceReference reference) 156 { 157 this(reference, null); 158 } 159 160 /** 161 * Download of a {@link ResourceReference}. 162 * <p> 163 * The {@link IResource} returned by {@link ResourceReference#getResource()} must call 164 * {@link #markCompleted(Attributes)} when responding, otherwise the callback 165 * {@link #onDownloadSuccess(AjaxRequestTarget)} will not work. 166 * 167 * @param reference 168 * reference to resource to download 169 * @param resourceParameters 170 * parameters for the resource 171 */ 172 public AjaxDownloadBehavior(ResourceReference reference, PageParameters resourceParameters) 173 { 174 this.resourceBehavior = null; 175 176 this.resourceReference = Args.notNull(reference, "reference"); 177 this.resourceParameters = resourceParameters; 178 } 179 180 @Override 181 protected void onBind() 182 { 183 super.onBind(); 184 185 if (resourceBehavior != null) 186 { 187 getComponent().add(resourceBehavior); 188 } 189 } 190 191 @Override 192 protected void onUnbind() 193 { 194 super.onUnbind(); 195 196 if (resourceBehavior != null) 197 { 198 getComponent().remove(resourceBehavior); 199 } 200 } 201 202 /** 203 * Call this method to initiate the download. You can use the {@link #resourceParameters} to dynamically pass 204 * information to the {@link org.apache.wicket.request.resource.IResource} in order to generate contents. 205 * 206 * @param handler 207 * the initiating RequestHandler 208 * @param resourceParameters 209 * Some PageParameters that might be used by the resource in order to generate content 210 */ 211 public void initiate(IPartialPageRequestHandler handler, PageParameters resourceParameters) 212 { 213 this.resourceParameters = resourceParameters; 214 initiate(handler); 215 } 216 217 /** 218 * Call this method to initiate the download. 219 * 220 * @param handler 221 * the initiating RequestHandler 222 */ 223 public void initiate(IPartialPageRequestHandler handler) 224 { 225 if (getComponent() == null) 226 { 227 throw new WicketRuntimeException("not bound to a component"); 228 } 229 230 CharSequence url; 231 if (resourceBehavior == null) 232 { 233 if (resourceReference.canBeRegistered()) 234 { 235 getComponent().getApplication().getResourceReferenceRegistry() 236 .registerResourceReference(resourceReference); 237 } 238 239 PageParameters parameters = new PageParameters(); 240 if (resourceParameters != null) 241 { 242 parameters.mergeWith(resourceParameters); 243 } 244 parameters.set(RESOURCE_PARAMETER_NAME, getName()); 245 246 url = getComponent().getRequestCycle() 247 .urlFor(new ResourceReferenceRequestHandler(resourceReference, parameters)); 248 } 249 else 250 { 251 url = resourceBehavior.getUrl(); 252 } 253 254 JSONObject settings = new JSONObject(); 255 settings.put("attributes", new JSONFunction(renderAjaxAttributes(getComponent()))); 256 settings.put("name", getName()); 257 settings.put("downloadUrl", url); 258 settings.put("sameSite", generateSameSiteAttribute()); 259 settings.put("method", getLocation().name().toLowerCase(Locale.ROOT)); 260 261 handler.appendJavaScript(String.format("Wicket.AjaxDownload.initiate(%s);", settings)); 262 263 onBeforeDownload(handler); 264 } 265 266 private String generateSameSiteAttribute() { 267 if (sameSite == null) 268 { 269 return ""; 270 } 271 272 StringBuilder stringBuffer = new StringBuilder(30); 273 if (sameSite.equals(CookieDefaults.SameSite.None)) 274 { 275 stringBuffer.append("; Secure"); 276 } 277 278 stringBuffer.append("; SameSite="); 279 stringBuffer.append(sameSite.name()); 280 281 return stringBuffer.toString(); 282 283 } 284 285 protected void onBeforeDownload(IPartialPageRequestHandler handler) 286 { 287 } 288 289 /** 290 * A callback executed when the download of the resource finished successfully. 291 * 292 * @param target The Ajax request handler 293 */ 294 protected void onDownloadSuccess(AjaxRequestTarget target) 295 { 296 } 297 298 /** 299 * A callback executed when the download of the resource failed for some reason, 300 * e.g. an error at the server side. 301 * <p> 302 * Since the HTTP status code of the download is not available to Wicket, any HTML in the resource response 303 * will be interpreted as a failure HTTP status message. Thus is it not possible to download HTML resources 304 * via {@link AjaxDownloadBehavior}. 305 * 306 * @param target The Ajax request handler 307 */ 308 protected void onDownloadFailed(AjaxRequestTarget target) 309 { 310 } 311 312 /** 313 * A callback executed when the download of the resource finished successfully or with a failure. 314 * 315 * @param target The Ajax request handler 316 */ 317 protected void onDownloadCompleted(AjaxRequestTarget target) 318 { 319 } 320 321 @Override 322 public void renderHead(Component component, IHeaderResponse response) 323 { 324 super.renderHead(component, response); 325 326 response.render(JavaScriptHeaderItem.forReference(JS)); 327 } 328 329 @Override 330 protected void respond(AjaxRequestTarget target) 331 { 332 String result = getComponent().getRequest().getRequestParameters().getParameterValue("result").toOptionalString(); 333 if ("success".equals(result)) { 334 onDownloadSuccess(target); 335 } else if ("failed".equals(result)) { 336 onDownloadFailed(target); 337 } 338 onDownloadCompleted(target); 339 } 340 341 public Location getLocation() { 342 return location; 343 } 344 345 public AjaxDownloadBehavior setLocation(final Location location) { 346 this.location = Args.notNull(location, "location"); 347 return this; 348 } 349 350 /** 351 * Identifying name of this behavior. 352 */ 353 private String getName() 354 { 355 return String.format("wicket-ajaxdownload-%s-%s", getComponent().getMarkupId(), 356 getComponent().getBehaviorId(this)); 357 } 358 359 /** 360 * The behavior responding with the actual resource. 361 */ 362 private class ResourceBehavior extends Behavior implements IRequestListener 363 { 364 private static final long serialVersionUID = 1L; 365 private final IResource resource; 366 367 private ResourceBehavior(IResource resource) 368 { 369 this.resource = Args.notNull(resource, "resource"); 370 } 371 372 @Override 373 public boolean rendersPage() 374 { 375 return false; 376 } 377 378 @Override 379 public void onRequest() 380 { 381 final RequestCycle requestCycle = RequestCycle.get(); 382 final Response response = requestCycle.getResponse(); 383 ((WebResponse) response).addCookie(cookie(getName())); 384 385 Attributes a = new Attributes(requestCycle.getRequest(), response, null); 386 387 resource.respond(a); 388 } 389 390 public CharSequence getUrl() 391 { 392 return getComponent().urlForListener(this, null); 393 } 394 } 395 396 /** 397 * Mark a resource as complete. 398 * <p> 399 * Has to be called from {@link IResource#respond(Attributes)} when downloaded via 400 * {@link #AjaxDownloadBehavior(IResource)}. 401 * 402 * @param attributes 403 * resource attributes 404 */ 405 public static void markCompleted(IResource.Attributes attributes) 406 { 407 String cookieName = attributes.getParameters().get(RESOURCE_PARAMETER_NAME).toString(); 408 409 ((WebResponse)attributes.getResponse()).addCookie(cookie(cookieName)); 410 } 411 412 413 private static Cookie cookie(String name) 414 { 415 Cookie cookie = new Cookie(name, "complete"); 416 417 // has to be on root, otherwise JavaScript will not be able to access the 418 // cookie when it is set from a different path - which is the case when a 419 // ResourceReference is used 420 cookie.setPath("/"); 421 422 return cookie; 423 } 424 425 /** 426 * @return The {@link org.apache.wicket.util.cookies.CookieDefaults.SameSite} attribute to be used for the complete download. 427 */ 428 public CookieDefaults.SameSite getSameSite() 429 { 430 return sameSite; 431 } 432 433 /** 434 * Setter for the same {@link org.apache.wicket.util.cookies.CookieDefaults.SameSite} 435 * 436 * @param sameSite The non-null sameSite attribute 437 */ 438 public void setSameSite(CookieDefaults.SameSite sameSite) 439 { 440 Args.notNull(sameSite, "sameSite"); 441 this.sameSite = sameSite; 442 } 443}