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.lang.Args;
042
043import com.github.openjson.JSONObject;
044
045/**
046 * Download resources via Ajax.
047 * <p>
048 * Usage:
049 *
050 * <pre>
051 * final AjaxDownloadBehavior download = new AjaxDownloadBehavior(resource);
052 * add(download);
053 *
054 * add(new AjaxButton("download")
055 * {
056 *      &#64;Override
057 *      protected void onSubmit(IPartialPageRequestHandler handler, Form&lt;?&gt; form)
058 *      {
059 *              download.initiate(handler);
060 *      }
061 * });
062 * </pre>
063 *
064 * <p>To set the name of the downloaded resource make use of
065 * {@link org.apache.wicket.request.resource.ResourceStreamResource#setFileName(String)} or
066 * {@link org.apache.wicket.request.resource.AbstractResource.ResourceResponse#setFileName(String)}</p>
067 *
068 * @author svenmeier
069 * @author Martin Grigorov
070 * @author Maxim Solodovnik
071 */
072public class AjaxDownloadBehavior extends AbstractDefaultAjaxBehavior
073{
074        private static final long serialVersionUID = 1L;
075
076        public enum Location {
077                /**
078                 * The resource will be downloaded into a {@code blob}.
079                 * <p>
080                 * This is recommended for modern browsers.
081                 */
082                Blob,
083
084                /**
085                 * The resource will be downloaded via a temporary created iframe, the resource has to be a
086                 * {@link ContentDisposition#ATTACHMENT}.
087                 * <p>
088                 * This is recommended when there are resources in the DOM which will be
089                 * closed automatically on JavaScript <em>unload</em> event, like WebSockets.
090                 * Supports both <em>success</em> and <em>failure</em> callbacks!
091                 */
092                IFrame,
093
094                /**
095                 * The resource will be downloaded by changing the location of the current DOM document,
096                 * the resource has to be a {@link ContentDisposition#ATTACHMENT}.
097                 * <p>
098                 * Note: This will trigger JavaScript <em>unload</em> event on the page!
099                 * Does not support {@link AjaxDownloadBehavior#onDownloadFailed(AjaxRequestTarget)} callback,
100                 * i.e. it is not possible to detect when the download has failed!
101                 */
102                SameWindow,
103
104                /**
105                 * The resource will be downloaded in a new browser window by using JavaScript <code>window.open()</code> API,
106                 * the resource has to be a {@link ContentDisposition#INLINE}.
107                 */
108                NewWindow
109        }
110
111        /**
112         * Name of parameter used to transfer the download identifier to the resource.
113         *
114         * @see #markCompleted(Attributes)
115         */
116        private static final String RESOURCE_PARAMETER_NAME = "wicket-ajaxdownload";
117
118        private static final ResourceReference JS = new JQueryPluginResourceReference(
119                AjaxDownloadBehavior.class, "wicket-ajaxdownload.js");
120
121        private final ResourceReference resourceReference;
122
123        private final ResourceBehavior resourceBehavior;
124
125        private PageParameters resourceParameters;
126
127        private Location location = Location.Blob;
128
129        /**
130         * Download of a {@link IResource}.
131         *
132         * @param resource
133         *            resource to download
134         */
135        public AjaxDownloadBehavior(IResource resource)
136        {
137                Args.notNull(resource, "resource");
138                this.resourceBehavior = new ResourceBehavior(resource);
139                this.resourceReference = null;
140        }
141
142        /**
143         * Download of a {@link ResourceReference}.
144         * <p>
145         * The {@link IResource} returned by {@link ResourceReference#getResource()} must call
146         * {@link #markCompleted(Attributes)} when responding, otherwise the callback
147         * {@link #onDownloadSuccess(AjaxRequestTarget)} will not work.
148         *
149         * @param reference
150         *            reference to resource to download
151         */
152        public AjaxDownloadBehavior(ResourceReference reference)
153        {
154                this(reference, null);
155        }
156
157        /**
158         * Download of a {@link ResourceReference}.
159         * <p>
160         * The {@link IResource} returned by {@link ResourceReference#getResource()} must call
161         * {@link #markCompleted(Attributes)} when responding, otherwise the callback
162         * {@link #onDownloadSuccess(AjaxRequestTarget)} will not work.
163         *
164         * @param reference
165         *            reference to resource to download
166         * @param resourceParameters
167         *            parameters for the resource
168         */
169        public AjaxDownloadBehavior(ResourceReference reference, PageParameters resourceParameters)
170        {
171                this.resourceBehavior = null;
172
173                this.resourceReference = Args.notNull(reference, "reference");
174                this.resourceParameters = resourceParameters;
175        }
176
177        @Override
178        protected void onBind()
179        {
180                super.onBind();
181
182                if (resourceBehavior != null)
183                {
184                        getComponent().add(resourceBehavior);
185                }
186        }
187
188        @Override
189        protected void onUnbind()
190        {
191                super.onUnbind();
192
193                if (resourceBehavior != null)
194                {
195                        getComponent().remove(resourceBehavior);
196                }
197        }
198
199        /**
200         * Call this method to initiate the download. You can use the {@link #resourceParameters} to dynamically pass
201         * information to the {@link org.apache.wicket.request.resource.IResource} in order to generate contents.
202         *
203         * @param handler
204         *          the initiating RequestHandler
205         * @param resourceParameters
206         *          Some PageParameters that might be used by the resource in order to generate content
207         */
208        public void initiate(IPartialPageRequestHandler handler, PageParameters resourceParameters)
209        {
210                this.resourceParameters = resourceParameters;
211                initiate(handler);
212        }
213
214        /**
215         * Call this method to initiate the download.
216         *
217         * @param handler
218         *            the initiating RequestHandler
219         */
220        public void initiate(IPartialPageRequestHandler handler)
221        {
222                if (getComponent() == null)
223                {
224                        throw new WicketRuntimeException("not bound to a component");
225                }
226
227                CharSequence url;
228                if (resourceBehavior == null)
229                {
230                        if (resourceReference.canBeRegistered())
231                        {
232                                getComponent().getApplication().getResourceReferenceRegistry()
233                                        .registerResourceReference(resourceReference);
234                        }
235
236                        PageParameters parameters = new PageParameters();
237                        if (resourceParameters != null)
238                        {
239                                parameters.mergeWith(resourceParameters);
240                        }
241                        parameters.set(RESOURCE_PARAMETER_NAME, getName());
242
243                        url = getComponent().getRequestCycle()
244                                .urlFor(new ResourceReferenceRequestHandler(resourceReference, parameters));
245                }
246                else
247                {
248                        url = resourceBehavior.getUrl();
249                }
250
251                JSONObject settings = new JSONObject();
252                settings.put("attributes", new JSONFunction(renderAjaxAttributes(getComponent())));
253                settings.put("name", getName());
254                settings.put("downloadUrl", url);
255                settings.put("method", getLocation().name().toLowerCase(Locale.ROOT));
256
257                handler.appendJavaScript(String.format("Wicket.AjaxDownload.initiate(%s);", settings));
258
259                onBeforeDownload(handler);
260        }
261
262        protected void onBeforeDownload(IPartialPageRequestHandler handler)
263        {
264        }
265
266        /**
267         * A callback executed when the download of the resource finished successfully.
268         *
269         * @param target The Ajax request handler
270         */
271        protected void onDownloadSuccess(AjaxRequestTarget target)
272        {
273        }
274
275        /**
276         * A callback executed when the download of the resource failed for some reason,
277         * e.g. an error at the server side.
278         * <p>
279         * Since the HTTP status code of the download is not available to Wicket, any HTML in the resource response
280         * will be interpreted as a failure HTTP status message. Thus is it not possible to download HTML resources
281         * via {@link AjaxDownloadBehavior}.
282         *
283         * @param target The Ajax request handler
284         */
285        protected void onDownloadFailed(AjaxRequestTarget target)
286        {
287        }
288
289        /**
290         * A callback executed when the download of the resource finished successfully or with a failure.
291         *
292         * @param target The Ajax request handler
293         */
294        protected void onDownloadCompleted(AjaxRequestTarget target)
295        {
296        }
297
298        @Override
299        public void renderHead(Component component, IHeaderResponse response)
300        {
301                super.renderHead(component, response);
302
303                response.render(JavaScriptHeaderItem.forReference(JS));
304        }
305
306        @Override
307        protected void respond(AjaxRequestTarget target)
308        {
309                String result = getComponent().getRequest().getRequestParameters().getParameterValue("result").toOptionalString();
310                if ("success".equals(result)) {
311                        onDownloadSuccess(target);
312                } else if ("failed".equals(result)) {
313                        onDownloadFailed(target);
314                }
315                onDownloadCompleted(target);
316        }
317
318        public Location getLocation() {
319                return location;
320        }
321
322        public AjaxDownloadBehavior setLocation(final Location location) {
323                this.location = Args.notNull(location, "location");
324                return this;
325        }
326
327        /**
328         * Identifying name of this behavior.
329         */
330        private String getName()
331        {
332                return String.format("wicket-ajaxdownload-%s-%s", getComponent().getMarkupId(),
333                        getComponent().getBehaviorId(this));
334        }
335
336        /**
337         * The behavior responding with the actual resource.
338         */
339        private class ResourceBehavior extends Behavior implements IRequestListener
340        {
341                private static final long serialVersionUID = 1L;
342                private final IResource resource;
343
344                private ResourceBehavior(IResource resource)
345                {
346                        this.resource = Args.notNull(resource, "resource");
347                }
348
349                @Override
350                public boolean rendersPage()
351                {
352                        return false;
353                }
354
355                @Override
356                public void onRequest()
357                {
358                        final RequestCycle requestCycle = RequestCycle.get();
359                        final Response response = requestCycle.getResponse();
360                        ((WebResponse) response).addCookie(cookie(getName()));
361
362                        Attributes a = new Attributes(requestCycle.getRequest(), response, null);
363
364                        resource.respond(a);
365                }
366
367                public CharSequence getUrl()
368                {
369                        return getComponent().urlForListener(this, null);
370                }
371        }
372
373        /**
374         * Mark a resource as complete.
375         * <p>
376         * Has to be called from {@link IResource#respond(Attributes)} when downloaded via
377         * {@link #AjaxDownloadBehavior(IResource)}.
378         *
379         * @param attributes
380         *            resource attributes
381         */
382        public static void markCompleted(IResource.Attributes attributes)
383        {
384                String cookieName = attributes.getParameters().get(RESOURCE_PARAMETER_NAME).toString();
385
386                ((WebResponse)attributes.getResponse()).addCookie(cookie(cookieName));
387        }
388
389        private static Cookie cookie(String name)
390        {
391                Cookie cookie = new Cookie(name, "complete");
392
393                // has to be on root, otherwise JavaScript will not be able to access the
394                // cookie when it is set from a different path - which is the case when a
395                // ResourceReference is used
396                cookie.setPath("/");
397
398                return cookie;
399        }
400}