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.form.upload; 018 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Map.Entry; 025 026import org.apache.commons.fileupload.FileItem; 027import org.apache.wicket.markup.ComponentTag; 028import org.apache.wicket.markup.head.IHeaderResponse; 029import org.apache.wicket.markup.head.JavaScriptHeaderItem; 030import org.apache.wicket.markup.head.OnDomReadyHeaderItem; 031import org.apache.wicket.markup.html.WebComponent; 032import org.apache.wicket.markup.html.WebMarkupContainer; 033import org.apache.wicket.markup.html.basic.Label; 034import org.apache.wicket.markup.html.form.Form; 035import org.apache.wicket.markup.html.form.FormComponent; 036import org.apache.wicket.markup.html.form.FormComponentPanel; 037import org.apache.wicket.model.IModel; 038import org.apache.wicket.model.Model; 039import org.apache.wicket.protocol.http.IMultipartWebRequest; 040import org.apache.wicket.request.Request; 041import org.apache.wicket.request.resource.JavaScriptResourceReference; 042import org.apache.wicket.request.resource.ResourceReference; 043import org.apache.wicket.util.convert.ConversionException; 044import org.apache.wicket.util.string.Strings; 045 046 047/** 048 * Form component that allows the user to select multiple files to upload via a single <input 049 * type="file"/> field. 050 * 051 * Notice that this component clears its model at the end of the request, so the uploaded files MUST 052 * be processed within the request they were uploaded. 053 * 054 * Uses javascript implementation from 055 * http://the-stickman.com/web-development/javascript/upload-multiple-files-with-a-single-file-element/ 056 * 057 * For customizing caption text see {@link #RESOURCE_LIMITED} and {@link #RESOURCE_UNLIMITED} 058 * 059 * For an example of styling using CSS see the upload example in wicket-examples 060 * 061 * @author Igor Vaynberg (ivaynberg) 062 */ 063public class MultiFileUploadField extends FormComponentPanel<Collection<FileUpload>> 064{ 065 private static final long serialVersionUID = 1L; 066 067 /** 068 * Represents an unlimited max count of uploads 069 */ 070 public static final int UNLIMITED = -1; 071 072 /** 073 * Resource key used to retrieve caption message for when a limit on the number of uploads is 074 * specified. The limit is represented via ${max} variable. 075 * 076 * Example: org.apache.wicket.mfu.caption.limited=Files (maximum ${max}): 077 */ 078 public static final String RESOURCE_LIMITED = "org.apache.wicket.mfu.caption.limited"; 079 080 /** 081 * Resource key used to retrieve caption message for when no limit on the number of uploads is 082 * specified. 083 * 084 * Example: org.apache.wicket.mfu.caption.unlimited=Files: 085 */ 086 public static final String RESOURCE_UNLIMITED = "org.apache.wicket.mfu.caption.unlimited"; 087 088 089 private static final String NAME_ATTR = "name"; 090 091 public static final String MAGIC_SEPARATOR = "_mf_"; 092 093 public static final ResourceReference JS = new JavaScriptResourceReference( 094 MultiFileUploadField.class, "MultiFileUploadField.js"); 095 096 private final WebComponent upload; 097 private final WebMarkupContainer container; 098 099 private final int max; 100 101 private final boolean useMultipleAttr; 102 103 private transient String[] inputArrayCache = null; 104 105 106 /** 107 * Constructor 108 * 109 * @param id 110 */ 111 public MultiFileUploadField(String id) 112 { 113 this(id, null, UNLIMITED); 114 } 115 116 /** 117 * Constructor 118 * 119 * @param id 120 * @param max 121 * max number of files a user can upload 122 */ 123 public MultiFileUploadField(String id, int max) 124 { 125 this(id, null, max); 126 } 127 128 /** 129 * Constructor 130 * 131 * @param id 132 * @param model 133 */ 134 public MultiFileUploadField(String id, IModel<? extends Collection<FileUpload>> model) 135 { 136 this(id, model, UNLIMITED); 137 } 138 139 /** 140 * Constructor 141 * 142 * @param id 143 * @param model 144 * @param max 145 * max number of files a user can upload 146 */ 147 public MultiFileUploadField(String id, IModel<? extends Collection<FileUpload>> model, int max) 148 { 149 this(id, model, max, false); 150 } 151 152 /** 153 * Constructor 154 * 155 * @param id 156 * @param model 157 * @param max 158 * max number of files a user can upload 159 * @param useMultipleAttr 160 * true in order to use the new HTML5 "multiple" <input> attribute. It will allow 161 * the users to select multiple files at once for multiple times if the browser 162 * supports it, otherwise it will work just as before - one file multiple times. 163 * 164 */ 165 @SuppressWarnings("unchecked") 166 public MultiFileUploadField(String id, IModel<? extends Collection<FileUpload>> model, int max, 167 boolean useMultipleAttr) 168 { 169 super(id, (IModel<Collection<FileUpload>>)model); 170 171 this.max = max; 172 this.useMultipleAttr = useMultipleAttr; 173 174 upload = new WebComponent("upload") { 175 @Override 176 protected void onComponentTag(ComponentTag tag) 177 { 178 super.onComponentTag(tag); 179 180 if (!isEnabledInHierarchy()) 181 { 182 onDisabled(tag); 183 } 184 } 185 }; 186 upload.setOutputMarkupId(true); 187 add(upload); 188 189 container = new WebMarkupContainer("container"); 190 container.setOutputMarkupId(true); 191 add(container); 192 193 container.add(new Label("caption", new CaptionModel())); 194 } 195 196 /** 197 * @see org.apache.wicket.markup.html.form.FormComponentPanel#onComponentTag(org.apache.wicket.markup.ComponentTag) 198 */ 199 @Override 200 protected void onComponentTag(ComponentTag tag) 201 { 202 super.onComponentTag(tag); 203 // remove the name attribute added by the FormComponent 204 if (tag.getAttributes().containsKey(NAME_ATTR)) 205 { 206 tag.getAttributes().remove(NAME_ATTR); 207 } 208 } 209 210 /** 211 * @see org.apache.wicket.Component#onBeforeRender() 212 */ 213 @Override 214 protected void onBeforeRender() 215 { 216 super.onBeforeRender(); 217 218 Form<?> form = findParent(Form.class); 219 if (form == null) 220 { 221 // woops 222 throw new IllegalStateException("Component " + getClass().getName() + " must have a " + 223 Form.class.getName() + " component above in the hierarchy"); 224 } 225 } 226 227 @Override 228 public boolean isMultiPart() 229 { 230 return true; 231 } 232 233 @Override 234 public void renderHead(IHeaderResponse response) 235 { 236 // initialize the javascript library 237 response.render(JavaScriptHeaderItem.forReference(JS)); 238 response.render(OnDomReadyHeaderItem.forScript("new MultiSelector('" + getInputName() + 239 "', document.getElementById('" + container.getMarkupId() + "'), " + max + ", " + 240 useMultipleAttr + ", '" + getString("org.apache.wicket.mfu.delete") + 241 "').addElement(document.getElementById('" + upload.getMarkupId() + "'));")); 242 } 243 244 /** 245 * @see org.apache.wicket.markup.html.form.FormComponent#getInputAsArray() 246 */ 247 @Override 248 public String[] getInputAsArray() 249 { 250 // fake the input array as if it contained an array of all uploaded file 251 // names 252 253 if (inputArrayCache == null) 254 { 255 // this array will aggregate all input names 256 ArrayList<String> names = null; 257 258 final Request request = getRequest(); 259 if (request instanceof IMultipartWebRequest) 260 { 261 // retrieve the filename->FileItem map from request 262 final Map<String, List<FileItem>> itemNameToItem = ((IMultipartWebRequest)request).getFiles(); 263 for (Entry<String, List<FileItem>> entry : itemNameToItem.entrySet()) 264 { 265 // iterate over the map and build the list of filenames 266 267 final String name = entry.getKey(); 268 final List<FileItem> fileItems = entry.getValue(); 269 270 if (!Strings.isEmpty(name) && 271 name.startsWith(getInputName() + MAGIC_SEPARATOR) && !fileItems.isEmpty() && 272 !Strings.isEmpty(fileItems.get(0).getName())) 273 { 274 275 // make sure the fileitem belongs to this component and 276 // is not empty 277 278 names = (names != null) ? names : new ArrayList<String>(); 279 names.add(name); 280 } 281 } 282 } 283 284 if (names != null) 285 { 286 inputArrayCache = names.toArray(new String[names.size()]); 287 } 288 } 289 return inputArrayCache; 290 291 } 292 293 @Override 294 protected Collection<FileUpload> convertValue(String[] value) throws ConversionException 295 { 296 // convert the array of filenames into a collection of FileItems 297 298 Collection<FileUpload> uploads = null; 299 300 final String[] filenames = getInputAsArray(); 301 302 if (filenames != null) 303 { 304 final IMultipartWebRequest request = (IMultipartWebRequest)getRequest(); 305 306 uploads = new ArrayList<>(filenames.length); 307 308 for (String filename : filenames) 309 { 310 List<FileItem> fileItems = request.getFile(filename); 311 for (FileItem fileItem : fileItems) 312 { 313 uploads.add(new FileUpload(fileItem)); 314 } 315 } 316 } 317 318 return uploads; 319 320 } 321 322 /** 323 * See {@link FormComponent#updateCollectionModel(FormComponent)} for details on how the model 324 * is updated. 325 */ 326 @Override 327 public void updateModel() 328 { 329 FormComponent.updateCollectionModel(this); 330 } 331 332 @Override 333 protected void onDetach() 334 { 335 if (forceCloseStreamsOnDetach()) 336 { 337 // cleanup any opened filestreams 338 Collection<FileUpload> uploads = getConvertedInput(); 339 if (uploads != null) 340 { 341 for (FileUpload upload : uploads) 342 { 343 upload.closeStreams(); 344 } 345 } 346 347 // cleanup any caches 348 inputArrayCache = null; 349 350 // clean up the model because we don't want FileUpload objects in session 351 Collection<FileUpload> modelObject = getModelObject(); 352 if (modelObject != null) 353 { 354 modelObject.clear(); 355 } 356 } 357 358 super.onDetach(); 359 } 360 361 /** 362 * The FileUploadField will close any input streams you have opened in its FileUpload by 363 * default. If you wish to manage the stream yourself (e.g. you want to use it in another 364 * thread) then you can override this method to prevent this behavior. 365 * 366 * @return <code>true</code> if stream should be closed at the end of request 367 */ 368 protected boolean forceCloseStreamsOnDetach() 369 { 370 return true; 371 } 372 373 /** 374 * Model that will construct the caption string 375 * 376 * @author ivaynberg 377 */ 378 private class CaptionModel implements IModel<String> 379 { 380 private static final long serialVersionUID = 1L; 381 382 @Override 383 public String getObject() 384 { 385 if (max == UNLIMITED) 386 { 387 return getString(RESOURCE_UNLIMITED); 388 } 389 else 390 { 391 HashMap<String, Object> vars = new HashMap<>(1); 392 vars.put("max", max); 393 return getString(RESOURCE_LIMITED, new Model<>(vars)); 394 } 395 } 396 397 } 398}