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}