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.markup.html.link;
018
019import java.io.File;
020
021import org.apache.wicket.model.IModel;
022import org.apache.wicket.model.Model;
023import org.apache.wicket.request.IRequestCycle;
024import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler;
025import org.apache.wicket.request.resource.ContentDisposition;
026import org.apache.wicket.util.file.Files;
027import org.apache.wicket.util.lang.Args;
028import org.apache.wicket.util.resource.FileResourceStream;
029import org.apache.wicket.util.resource.IResourceStream;
030import org.apache.wicket.util.string.Strings;
031import java.time.Duration;
032
033/**
034 * A link that streams a file to the client. When clicked this link will prompt the save as dialog
035 * in the browser.
036 * 
037 * NOTICE that this link will lock the page. That means only one link from the page can be
038 * downloaded at a time, and also while the download happens the page cannot be accessed by other
039 * threads. If you need to stream multiple files concurrently without blocking then you should use
040 * shared resources or a non-wicket servlet.
041 * 
042 * @author Igor Vaynberg (ivaynberg)
043 */
044public class DownloadLink extends Link<File>
045{
046        private static final long serialVersionUID = 1L;
047
048        /**
049         * The file name that will be used in the response headers.<br/>
050         * Optional. If omitted the name of the provided file will be used.
051         */
052        private IModel<String> fileNameModel;
053
054        /**
055         * A flag indicating whether the file should be deleted after download.
056         */
057        private boolean deleteAfter;
058
059        /**
060         * The duration for which the file resource should be cached by the browser.
061         * <p>
062         * By default is {@code null} and
063         * {@link org.apache.wicket.settings.ResourceSettings#getDefaultCacheDuration()} is used.
064         */
065        private Duration cacheDuration;
066
067        /**
068         * Controls whether the browser will save the file or display it inline.
069         * <p>
070         * The default is ATTACHMENT to initiate the browser file save dialog.
071         */
072        private ContentDisposition contentDisposition = ContentDisposition.ATTACHMENT;
073        
074        /**
075         * Constructor. File name used will be the result of <code>file.getName()</code>
076         * 
077         * @param id
078         *            component id
079         * @param file
080         *            file to stream to client
081         */
082        public DownloadLink(String id, File file)
083        {
084                this(id, new Model<File>(Args.notNull(file, "file")));
085        }
086
087        /**
088         * Constructor. File name used will be the result of <code>file.getName()</code>
089         * 
090         * @param id
091         *            component id
092         * @param model
093         *            model that contains the file object
094         */
095        public DownloadLink(String id, IModel<File> model)
096        {
097                this(id, model, (IModel<String>)null);
098        }
099
100        /**
101         * Constructor. File name used will be the result of <code>file.getName()</code>
102         * 
103         * @param id
104         *            component id
105         * @param model
106         *            model that contains the file object
107         * @param fileName
108         *            name of the file
109         */
110        public DownloadLink(String id, IModel<File> model, String fileName)
111        {
112                this(id, model, Model.of(fileName));
113        }
114
115        /**
116         * Constructor
117         * 
118         * @param id
119         *            component id
120         * @param file
121         *            file to stream to client
122         * @param fileName
123         *            name of the file
124         */
125        public DownloadLink(String id, File file, String fileName)
126        {
127                this(id, Model.of(Args.notNull(file, "file")), Model.of(fileName));
128        }
129
130        /**
131         * Constructor. File name used will be the result of <code>file.getName()</code>
132         * 
133         * @param id
134         *            component id
135         * @param fileModel
136         *            model that contains the file object
137         * @param fileNameModel
138         *            model that provides the file name to use in the response headers
139         */
140        public DownloadLink(String id, IModel<File> fileModel, IModel<String> fileNameModel)
141        {
142                super(id, fileModel);
143                this.fileNameModel = wrap(fileNameModel);
144        }
145
146        @Override
147        public void detachModels()
148        {
149                super.detachModels();
150
151                if (fileNameModel != null)
152                {
153                        fileNameModel.detach();
154                }
155        }
156
157        @Override
158        public void onClick()
159        {
160                final File file = getModelObject();
161                if (file == null)
162                {
163                        throw new IllegalStateException(getClass().getName() +
164                                " failed to retrieve a File object from model");
165                }
166
167                String fileName = fileNameModel != null ? fileNameModel.getObject() : null;
168                if (Strings.isEmpty(fileName))
169                {
170                        fileName = file.getName();
171                }
172
173                IResourceStream resourceStream = new FileResourceStream(
174                        new org.apache.wicket.util.file.File(file));
175                getRequestCycle().scheduleRequestHandlerAfterCurrent(
176                        new ResourceStreamRequestHandler(resourceStream)
177                        {
178                                @Override
179                                public void respond(IRequestCycle requestCycle)
180                                {
181                                        super.respond(requestCycle);
182
183                                        if (deleteAfter)
184                                        {
185                                                Files.remove(file);
186                                        }
187                                }
188                        }.setFileName(fileName)
189                                .setContentDisposition(contentDisposition)
190                                .setCacheDuration(cacheDuration));
191        }
192
193        /**
194         * USE THIS METHOD WITH CAUTION!
195         * 
196         * If true, the file will be deleted! The recommended way to use this setting, is to set this
197         * DownloadLink object's model with a LoadableDetachableModel instance and the resulting file
198         * being generated in a temporary folder.
199         * 
200         * @param deleteAfter
201         *            true to delete file after download succeeds
202         * @return this component
203         */
204        public final DownloadLink setDeleteAfterDownload(boolean deleteAfter)
205        {
206                this.deleteAfter = deleteAfter;
207
208                return this;
209        }
210
211        /**
212         * Sets the duration for which the file resource should be cached by the client.
213         * 
214         * @param duration
215         *            the duration to cache
216         * @return this component.
217         */
218        public DownloadLink setCacheDuration(final Duration duration)
219        {
220                cacheDuration = duration;
221                return this;
222        }
223        
224        /**
225         * Sets the content disposition of the request.
226         * 
227         * @param contentDisposition
228         *            the content disposition of the file
229         * @return this component
230         */
231        public DownloadLink setContentDisposition(ContentDisposition contentDisposition) {
232                this.contentDisposition = contentDisposition;
233                return this;
234        }
235
236}