001/*
002 * Copyright 2002-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.multipart.commons;
018
019import java.io.File;
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.Serializable;
023
024import org.apache.commons.fileupload.FileItem;
025import org.apache.commons.fileupload.FileUploadException;
026import org.apache.commons.fileupload.disk.DiskFileItem;
027import org.apache.commons.logging.Log;
028import org.apache.commons.logging.LogFactory;
029
030import org.springframework.util.StreamUtils;
031import org.springframework.web.multipart.MultipartFile;
032
033/**
034 * {@link MultipartFile} implementation for Apache Commons FileUpload.
035 *
036 * @author Trevor D. Cook
037 * @author Juergen Hoeller
038 * @since 29.09.2003
039 * @see CommonsMultipartResolver
040 */
041@SuppressWarnings("serial")
042public class CommonsMultipartFile implements MultipartFile, Serializable {
043
044        protected static final Log logger = LogFactory.getLog(CommonsMultipartFile.class);
045
046        private final FileItem fileItem;
047
048        private final long size;
049
050        private boolean preserveFilename = false;
051
052
053        /**
054         * Create an instance wrapping the given FileItem.
055         * @param fileItem the FileItem to wrap
056         */
057        public CommonsMultipartFile(FileItem fileItem) {
058                this.fileItem = fileItem;
059                this.size = this.fileItem.getSize();
060        }
061
062
063        /**
064         * Return the underlying {@code org.apache.commons.fileupload.FileItem}
065         * instance. There is hardly any need to access this.
066         */
067        public final FileItem getFileItem() {
068                return this.fileItem;
069        }
070
071        /**
072         * Set whether to preserve the filename as sent by the client, not stripping off
073         * path information in {@link CommonsMultipartFile#getOriginalFilename()}.
074         * <p>Default is "false", stripping off path information that may prefix the
075         * actual filename e.g. from Opera. Switch this to "true" for preserving the
076         * client-specified filename as-is, including potential path separators.
077         * @since 4.3.5
078         * @see #getOriginalFilename()
079         * @see CommonsMultipartResolver#setPreserveFilename(boolean)
080         */
081        public void setPreserveFilename(boolean preserveFilename) {
082                this.preserveFilename = preserveFilename;
083        }
084
085
086        @Override
087        public String getName() {
088                return this.fileItem.getFieldName();
089        }
090
091        @Override
092        public String getOriginalFilename() {
093                String filename = this.fileItem.getName();
094                if (filename == null) {
095                        // Should never happen.
096                        return "";
097                }
098                if (this.preserveFilename) {
099                        // Do not try to strip off a path...
100                        return filename;
101                }
102
103                // Check for Unix-style path
104                int unixSep = filename.lastIndexOf('/');
105                // Check for Windows-style path
106                int winSep = filename.lastIndexOf('\\');
107                // Cut off at latest possible point
108                int pos = (winSep > unixSep ? winSep : unixSep);
109                if (pos != -1)  {
110                        // Any sort of path separator found...
111                        return filename.substring(pos + 1);
112                }
113                else {
114                        // A plain name
115                        return filename;
116                }
117        }
118
119        @Override
120        public String getContentType() {
121                return this.fileItem.getContentType();
122        }
123
124        @Override
125        public boolean isEmpty() {
126                return (this.size == 0);
127        }
128
129        @Override
130        public long getSize() {
131                return this.size;
132        }
133
134        @Override
135        public byte[] getBytes() {
136                if (!isAvailable()) {
137                        throw new IllegalStateException("File has been moved - cannot be read again");
138                }
139                byte[] bytes = this.fileItem.get();
140                return (bytes != null ? bytes : new byte[0]);
141        }
142
143        @Override
144        public InputStream getInputStream() throws IOException {
145                if (!isAvailable()) {
146                        throw new IllegalStateException("File has been moved - cannot be read again");
147                }
148                InputStream inputStream = this.fileItem.getInputStream();
149                return (inputStream != null ? inputStream : StreamUtils.emptyInput());
150        }
151
152        @Override
153        public void transferTo(File dest) throws IOException, IllegalStateException {
154                if (!isAvailable()) {
155                        throw new IllegalStateException("File has already been moved - cannot be transferred again");
156                }
157
158                if (dest.exists() && !dest.delete()) {
159                        throw new IOException(
160                                        "Destination file [" + dest.getAbsolutePath() + "] already exists and could not be deleted");
161                }
162
163                try {
164                        this.fileItem.write(dest);
165                        if (logger.isDebugEnabled()) {
166                                String action = "transferred";
167                                if (!this.fileItem.isInMemory()) {
168                                        action = (isAvailable() ? "copied" : "moved");
169                                }
170                                logger.debug("Multipart file '" + getName() + "' with original filename [" +
171                                                getOriginalFilename() + "], stored " + getStorageDescription() + ": " +
172                                                action + " to [" + dest.getAbsolutePath() + "]");
173                        }
174                }
175                catch (FileUploadException ex) {
176                        throw new IllegalStateException(ex.getMessage(), ex);
177                }
178                catch (IllegalStateException ex) {
179                        // Pass through when coming from FileItem directly
180                        throw ex;
181                }
182                catch (IOException ex) {
183                        // From I/O operations within FileItem.write
184                        throw ex;
185                }
186                catch (Exception ex) {
187                        throw new IOException("File transfer failed", ex);
188                }
189        }
190
191        /**
192         * Determine whether the multipart content is still available.
193         * If a temporary file has been moved, the content is no longer available.
194         */
195        protected boolean isAvailable() {
196                // If in memory, it's available.
197                if (this.fileItem.isInMemory()) {
198                        return true;
199                }
200                // Check actual existence of temporary file.
201                if (this.fileItem instanceof DiskFileItem) {
202                        return ((DiskFileItem) this.fileItem).getStoreLocation().exists();
203                }
204                // Check whether current file size is different than original one.
205                return (this.fileItem.getSize() == this.size);
206        }
207
208        /**
209         * Return a description for the storage location of the multipart content.
210         * Tries to be as specific as possible: mentions the file location in case
211         * of a temporary file.
212         */
213        public String getStorageDescription() {
214                if (this.fileItem.isInMemory()) {
215                        return "in memory";
216                }
217                else if (this.fileItem instanceof DiskFileItem) {
218                        return "at [" + ((DiskFileItem) this.fileItem).getStoreLocation().getAbsolutePath() + "]";
219                }
220                else {
221                        return "on disk";
222                }
223        }
224
225}