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" &lt;input&gt; 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}