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
019
020import java.util.ArrayList;
021import java.util.List;
022
023import org.apache.commons.fileupload.FileItem;
024import org.apache.wicket.markup.ComponentTag;
025import org.apache.wicket.markup.html.form.FormComponent;
026import org.apache.wicket.model.IModel;
027import org.apache.wicket.protocol.http.IMultipartWebRequest;
028import org.apache.wicket.request.Request;
029import org.apache.wicket.util.convert.ConversionException;
030import org.apache.wicket.util.string.Strings;
031
032/**
033 * Form component that corresponds to a <input type="file">. When a FileInput
034 * component is nested in a {@link org.apache.wicket.markup.html.form.Form}, that has multipart ==
035 * true, its model is updated with the {@link org.apache.wicket.markup.html.form.upload.FileUpload}
036 * for this component.
037 * <p>
038 * <strong>NOTE</strong>The model of this component is reset with {@code null} at the end of the
039 * request because {@link FileUpload} instances do not survive across requests since the input
040 * streams they point to will be closed. Because of this, the {@link FileUpload} instance should be
041 * processed within the same request as the form containing it was submitted.
042 * </p>
043 * 
044 * @author Eelco Hillenius
045 */
046public class FileUploadField extends FormComponent<List<FileUpload>>
047{
048        private static final long serialVersionUID = 1L;
049
050        private transient List<FileUpload> fileUploads;
051
052        /**
053         * @see org.apache.wicket.Component#Component(String)
054         */
055        public FileUploadField(final String id)
056        {
057                super(id);
058        }
059
060        /**
061         * @param id
062         *            See Component
063         * @param model
064         *            the model holding the uploaded {@link FileUpload}s
065         */
066        @SuppressWarnings("unchecked")
067        public FileUploadField(final String id, IModel<? extends List<FileUpload>> model)
068        {
069                super(id, (IModel<List<FileUpload>>)model);
070        }
071
072        /**
073         * @return the first uploaded file if HTML5 &lt;input type="file" <strong>multiple</strong>
074         *         /&gt; is used and the browser supports <em>multiple</em>, otherwise returns the
075         *         single uploaded file.
076         * @see #getFileUploads()
077         */
078        public FileUpload getFileUpload()
079        {
080                List<FileUpload> fileUploads = getFileUploads();
081
082                return fileUploads.isEmpty() ? null : fileUploads.get(0) ;
083        }
084
085        /**
086         * @return a list of all uploaded files. The list is empty if no files were selected. It will return more than one files if:
087         *         <ul>
088         *         <li>HTML5 &lt;input type="file" <strong>multiple</strong> /&gt; is used</li>
089         *         <li>the browser supports <em>multiple</em> attribute</li>
090         *         <li>the user has selected more than one files from the <em>Select file</em> dialog</li>
091         *         </ul>
092         */
093        public List<FileUpload> getFileUploads()
094        {
095                if (fileUploads != null)
096                {
097                        return fileUploads;
098                }
099
100                fileUploads = new ArrayList<>();
101
102                // Get request
103                final Request request = getRequest();
104
105                // If we successfully installed a multipart request
106                if (request instanceof IMultipartWebRequest)
107                {
108                        // Get the item for the path
109                        final List<FileItem> fileItems = ((IMultipartWebRequest)request).getFile(getInputName());
110
111                        if (fileItems != null)
112                        {
113                                for (FileItem item : fileItems)
114                                {
115                                        // WICKET-6270 detect empty field by missing file name
116                                        if (Strings.isEmpty(item.getName()) == false) {
117                                                fileUploads.add(new FileUpload(item));
118                                        }
119                                }
120                        }
121                }
122                
123                return fileUploads;
124        }
125
126        @Override
127        public void updateModel()
128        {
129                if (getModel() != null)
130                {
131                        super.updateModel();
132                }
133        }
134
135        @Override
136        public String[] getInputAsArray()
137        {
138                List<FileUpload> fileUploads = getFileUploads();
139                if (fileUploads.isEmpty() == false)
140                {
141                        List<String> clientFileNames = new ArrayList<>();
142                        for (FileUpload fu : fileUploads)
143                        {
144                                clientFileNames.add(fu.getClientFileName());
145                        }
146                        return clientFileNames.toArray(new String[clientFileNames.size()]);
147                }
148                return null;
149        }
150
151        @Override
152        protected List<FileUpload> convertValue(String[] value) throws ConversionException
153        {
154                final String[] filenames = getInputAsArray();
155                if (filenames == null)
156                {
157                        return null;
158                }
159                return getFileUploads();
160        }
161
162        @Override
163        public boolean isMultiPart()
164        {
165                return true;
166        }
167
168        @Override
169        protected void onComponentTag(ComponentTag tag)
170        {
171                // Must be attached to an input tag
172                checkComponentTag(tag, "input");
173
174                // Check for file type
175                checkComponentTagAttribute(tag, "type", "file");
176
177                // Default handling for component tag
178                super.onComponentTag(tag);
179        }
180
181        /**
182         * Clean up at the end of the request. This means closing all inputstreams which might have been
183         * opened from the fileUpload.
184         * 
185         * @see org.apache.wicket.Component#onDetach()
186         */
187        @Override
188        protected void onDetach()
189        {
190                if (fileUploads != null)
191                {
192                        if (forceCloseStreamsOnDetach()) {
193                                for (FileUpload fu : fileUploads)
194                                {
195                                        fu.closeStreams();
196                                }
197
198                                if (getModel() != null)
199                                {
200                                        getModel().setObject(null);
201                                }
202                        }
203
204                        fileUploads = null;
205                }
206                super.onDetach();
207        }
208
209        /**
210         * The FileUploadField will close any input streams you have opened in its FileUpload by
211         * default. If you wish to manage the stream yourself (e.g. you want to use it in another
212         * thread) then you can override this method to prevent this behavior.
213         * 
214         * @return <code>true</code> if stream should be closed at the end of request
215         */
216        protected boolean forceCloseStreamsOnDetach()
217        {
218                return true;
219        }
220}