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.io.File;
020import java.io.IOException;
021import java.io.InputStream;
022import java.security.MessageDigest;
023import java.security.NoSuchAlgorithmException;
024import java.util.ArrayList;
025import java.util.List;
026
027import org.apache.commons.fileupload.FileItem;
028import org.apache.wicket.util.io.IClusterable;
029import org.apache.wicket.Session;
030import org.apache.wicket.WicketRuntimeException;
031import org.apache.wicket.request.cycle.RequestCycle;
032import org.apache.wicket.util.file.Files;
033import org.apache.wicket.util.io.IOUtils;
034import org.apache.wicket.util.lang.Args;
035import org.apache.wicket.util.string.Strings;
036
037
038/**
039 * Model for file uploads. Objects of this class should not be kept between requests, and should
040 * therefore be marked as <code>transient</code> if they become a property of an IModel.
041 * 
042 * @author Jonathan Locke
043 */
044public class FileUpload
045{
046        private static final long serialVersionUID = 1L;
047
048        private final FileItem item;
049
050        private transient List<InputStream> inputStreamsToClose;
051
052        /**
053         * Constructor
054         * 
055         * @param item
056         *            The uploaded file item
057         */
058        public FileUpload(final FileItem item)
059        {
060                Args.notNull(item, "item");
061                this.item = item;
062        }
063
064        /**
065         * Close the streams which has been opened when getting the InputStream using
066         * {@link #getInputStream()}. All the input streams are closed at the end of the request. This
067         * is done when the FileUploadField, which is associated with this FileUpload is detached.
068         * <p>
069         * If an exception is thrown when closing the input streams, we ignore it, because the stream
070         * might have been closed already.
071         */
072        public final void closeStreams()
073        {
074                if (inputStreamsToClose != null)
075                {
076                        for (InputStream inputStream : inputStreamsToClose)
077                        {
078                                IOUtils.closeQuietly(inputStream);
079                        }
080
081                        // Reset the list
082                        inputStreamsToClose = null;
083                }
084        }
085
086        /**
087         * Deletes temp file from disk
088         */
089        public void delete()
090        {
091                item.delete();
092        }
093
094        /**
095         * @return Uploaded file as an array of bytes
096         */
097        public byte[] getBytes()
098        {
099                return item.get();
100        }
101
102        /**
103         * Get the MD5 checksum.
104         * 
105         * @param algorithm
106         *            the digest algorithm, e.g. MD5, SHA-1, SHA-256, SHA-512
107         * 
108         * @return The cryptographic digest of the file
109         */
110        public byte[] getDigest(String algorithm)
111        {
112                try
113                {
114                        Args.notEmpty(algorithm, "algorithm");
115                        MessageDigest digest = java.security.MessageDigest.getInstance(algorithm);
116
117                        if (item.isInMemory())
118                        {
119                                digest.update(getBytes());
120                                return digest.digest();
121                        }
122
123                        InputStream in = null;
124
125                        try
126                        {
127                                in = item.getInputStream();
128                                byte[] buf = new byte[Math.min((int)item.getSize(), 4096 * 10)];
129                                int len;
130                                while (-1 != (len = in.read(buf)))
131                                {
132                                        digest.update(buf, 0, len);
133                                }
134                                return digest.digest();
135                        }
136                        catch (IOException ex)
137                        {
138                                throw new WicketRuntimeException("Error while reading input data for " + algorithm +
139                                        " checksum", ex);
140                        }
141                        finally
142                        {
143                                IOUtils.closeQuietly(in);
144                        }
145                }
146                catch (NoSuchAlgorithmException ex)
147                {
148                        String error = String.format(
149                                "Your java runtime does not support digest algorithm [%s]. "
150                                        + "Please see java.security.MessageDigest.getInstance(\"%s\")", algorithm,
151                                algorithm);
152
153                        throw new WicketRuntimeException(error, ex);
154                }
155        }
156
157        /**
158         * Get the MD5 checksum.
159         * 
160         * @return The MD5 checksum of the file
161         */
162        public byte[] getMD5()
163        {
164                return getDigest("MD5");
165        }
166
167        /**
168         * @since 1.2
169         * @return name of uploaded client side file
170         */
171        public String getClientFileName()
172        {
173                String name = item.getName();
174
175                // when uploading from localhost some browsers will specify the entire path, we strip it
176                // down to just the file name
177                name = Strings.lastPathComponent(name, '/');
178                name = Strings.lastPathComponent(name, '\\');
179
180                return name;
181        }
182
183        /**
184         * @return Content type for upload
185         */
186        public String getContentType()
187        {
188                return item.getContentType();
189        }
190
191        /**
192         * Get an input stream for the file uploaded. Use this input stream if you can't use
193         * {@link #writeTo(File)} for persisting the uploaded file. This can be if you need to react
194         * upon the content of the file or need to persist it elsewhere, i.e. a database or external
195         * filesystem.
196         * <p>
197         * <b>PLEASE NOTE!</b><br>
198         * The InputStream return will be closed be Wicket at the end of the request. If you need it
199         * across a request you need to hold on to this FileUpload instead.
200         * 
201         * @return Input stream with file contents.
202         * @throws IOException
203         */
204        public InputStream getInputStream() throws IOException
205        {
206                if (inputStreamsToClose == null)
207                {
208                        inputStreamsToClose = new ArrayList<InputStream>();
209                }
210
211                InputStream is = item.getInputStream();
212                inputStreamsToClose.add(is);
213
214                return is;
215        }
216
217        /**
218         * @return The upload's size
219         */
220        public long getSize()
221        {
222                return item.getSize();
223        }
224
225        /**
226         * Saves this file upload to a given file on the server side.
227         * 
228         * @param file
229         *            The file
230         * @throws Exception
231         */
232        public void writeTo(final File file) throws Exception
233        {
234                Files.remove(file);
235                item.write(file);
236        }
237
238        /**
239         * Convenience method that copies the input stream returned by {@link #getInputStream()} into a
240         * temporary file.
241         * <p>
242         * Only use this if you actually need a {@link File} to work with, in all other cases use
243         * {@link #getInputStream()} or {@link #getBytes()}
244         * 
245         * @since 1.2
246         * 
247         * @return temporary file containing the contents of the uploaded file
248         * @throws Exception
249         */
250        public final File writeToTempFile() throws Exception
251        {
252                String sessionId = Session.exists() ? Session.get().getId() : "";
253                String tempFileName = sessionId + "_" + RequestCycle.get().getStartTime();
254                File temp = File.createTempFile(tempFileName, Files.cleanupFilename(item.getFieldName()));
255                writeTo(temp);
256                return temp;
257        }
258}