/*
* Copyright (c) 2002-2012 Alibaba Group Holding Limited.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.citrus.service.upload.impl.cfu;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import com.alibaba.citrus.util.StringUtil;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemHeaders;
import org.apache.commons.fileupload.FileItemHeadersSupport;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.DeferredFileOutputStream;
/**
* 改进自<code>commons-fileupload-1.2.1</code>的同名类。
* <p>
* 解决了如下问题:
* </p>
* <ol>
* <li>原<code>DiskFileItem</code>类(以下简称原类)在解析form field的值时,会利用
* <code>content-type</code>头部指定的<code>charset</code>值来决定其字符集编码。例如,下面的
* <code>multipart/form-data</code>请求片段指定了myparam field值的字符集编码为
* <code>UTF-8</code>:
* <p/>
* <pre>
* ----HttpUnit-part0-aSgQ2M
* Content-Disposition: form-data; name="myparam"
* Content-Type: text/plain; charset=UTF-8
* </pre>
* <p/>
* 然而,除了单元测试所用的<code>httpunit/servletunit</code>以外,几乎没有浏览器会在这里指定
* <code>content-type</code>以及 <code>charset</code>。因此原类的
* <code>getString()</code>总是得不到解码正确的字符串。</li>
* <li>原类将内容的长度超过<code>sizeThreshold</code>的字段 —— 无论普通的form fields或是文件字段 ——
* 均存入文件 。这是一种优化。然而在某些情况下,我们希望关闭这种优化 —— 将 <code>sizeThreshold</code>设置成
* <code>0</code> ,以便让所有上传文件无论大小都被存入磁盘。然而仍然希望普通的form fields被保存在内存里。</li>
* <li>创建文件时,希望能自动创建目录。</li>
* </ol>
* <p>
* 具体改进了如下内容:
* </p>
* <ul>
* <li>利用传入的<code>charset</code>参数,而不是<code>content-type</code>来解码form
* field。但该参数对于文件型字段无效。</li>
* <li>删除<code>getCharSet()</code>方法,添加<code>getCharset()</code>和
* <code>setCharset()</code>方法。</li>
* <li>修改<code>getString()</code>方法,对form field使用指定的<code>charset</code>来解码。</li>
* <li>添加<code>keepFormFieldInMemory</code>属性。</li>
* <li>改进<code>getOutputStream()</code>方法,,当
* <code>keepFormFieldInMemory == true</code>时,不将form fields写入文件,即将
* <code>threshold</code>设置成<code>Integer.MAX_VALUE</code>。</li>
* <li>利用<code>File.createTempFile()</code>来生成临时文件,删除原<code>getTempFile()</code>
* 方法,及相关的<code>getUniqueId()</code>方法、<code>counter</code> field、
* <code>tempFile</code> field。</li>
* <li>改进write()方法,当文件目录不存在时,创建之。</li>
* <li>改进toString()方法,使之返回文件名,这种形式是为了方便页面引用<code>FileItem</code>对象。</li>
* </ul>
*
* @author Michael Zhou
*/
public abstract class AbstractFileItem implements FileItem, FileItemHeadersSupport {
private static final long serialVersionUID = 486705336474235297L;
/**
* Default content charset to be used when no explicit charset parameter is
* provided by the sender. Media subtypes of the "text" type are defined to
* have a default charset value of "ISO-8859-1" when received via HTTP.
*/
public static final String DEFAULT_CHARSET = "ISO-8859-1";
// ----------------------------------------------------------- Data members
/** UID used in unique file name generation. */
private static final String UID = new java.rmi.server.UID().toString().replace(':', '_').replace('-', '_');
/** The name of the form field as provided by the browser. */
private String fieldName;
/**
* The content type passed by the browser, or <code>null</code> if not
* defined.
*/
private String contentType;
/** Whether or not this item is a simple form field. */
private boolean isFormField;
/** The original filename in the user's filesystem. */
private String fileName;
/**
* The size of the item, in bytes. This is used to cache the size when a
* file item is moved from its original location.
*/
private long size = -1;
/** The threshold above which uploads will be stored on disk. */
private int sizeThreshold;
private boolean keepFormFieldInMemory;
/** The directory in which uploaded files will be stored, if stored on disk. */
private File repository;
/** Cached contents of the file. */
private byte[] cachedContent;
/** Output stream for this item. */
protected transient DeferredFileOutputStream dfos;
/** File to allow for serialization of the content of this item. */
private File dfosFile;
/** The file items headers. */
private FileItemHeaders headers;
/** 用于解码字段值的字符集编码。 */
private String charset;
// ----------------------------------------------------------- Constructors
/**
* Constructs a new <code>DiskFileItem</code> instance.
*
* @param fieldName The name of the form field.
* @param contentType The content type passed by the browser or
* <code>null</code> if not specified.
* @param isFormField Whether or not this item is a plain form field, as
* opposed to a file upload.
* @param fileName The original filename in the user's filesystem, or
* <code>null</code> if not specified.
* @param sizeThreshold The threshold, in bytes, below which items will be
* retained in memory and above which they will be stored as a
* file.
* @param repository The data repository, which is the directory in which
* files will be created, should the item size exceed the
* threshold.
* @para keepFormFieldInMemory
*/
public AbstractFileItem(String fieldName, String contentType, boolean isFormField, String fileName,
int sizeThreshold, boolean keepFormFieldInMemory, File repository) {
// 设置默认值
if (sizeThreshold < 0) {
sizeThreshold = 0;
}
if (sizeThreshold == 0) {
keepFormFieldInMemory = true;
}
if (repository == null) {
repository = new File(System.getProperty("java.io.tmpdir"));
}
if (!repository.exists()) {
repository.mkdirs();
}
this.fieldName = fieldName;
this.contentType = contentType;
this.isFormField = isFormField;
this.fileName = fileName;
this.sizeThreshold = sizeThreshold;
this.keepFormFieldInMemory = keepFormFieldInMemory;
this.repository = repository;
}
// ------------------------------- Methods from javax.activation.DataSource
/**
* Returns an {@link java.io.InputStream InputStream} that can be used to
* retrieve the contents of the file.
*
* @return An {@link java.io.InputStream InputStream} that can be used to
* retrieve the contents of the file.
* @throws IOException if an error occurs.
*/
public InputStream getInputStream() throws IOException {
if (!isInMemory()) {
return new FileInputStream(dfos.getFile());
}
if (cachedContent == null) {
cachedContent = dfos.getData();
}
return new ByteArrayInputStream(cachedContent);
}
/**
* Returns the content type passed by the agent or <code>null</code> if not
* defined.
*
* @return The content type passed by the agent or <code>null</code> if not
* defined.
*/
public String getContentType() {
return contentType;
}
/** 取得当前field的字符集编码。 */
public String getCharset() {
return charset;
}
/** 设置当前field的字符集编码。 */
public void setCharset(String charset) {
this.charset = charset;
}
/**
* Returns the original filename in the client's filesystem.
*
* @return The original filename in the client's filesystem.
*/
public String getName() {
return fileName;
}
// ------------------------------------------------------- FileItem methods
/**
* Provides a hint as to whether or not the file contents will be read from
* memory.
*
* @return <code>true</code> if the file contents will be read from memory;
* <code>false</code> otherwise.
*/
public boolean isInMemory() {
if (cachedContent != null) {
return true;
}
return dfos.isInMemory();
}
/**
* Returns the size of the file.
*
* @return The size of the file, in bytes.
*/
public long getSize() {
if (size >= 0) {
return size;
} else if (cachedContent != null) {
return cachedContent.length;
} else if (dfos.isInMemory()) {
return dfos.getData().length;
} else {
return dfos.getFile().length();
}
}
/**
* Returns the contents of the file as an array of bytes. If the contents of
* the file were not yet cached in memory, they will be loaded from the disk
* storage and cached.
*
* @return The contents of the file as an array of bytes.
*/
public byte[] get() {
if (isInMemory()) {
if (cachedContent == null) {
cachedContent = dfos.getData();
}
return cachedContent;
}
byte[] fileData = new byte[(int) getSize()];
FileInputStream fis = null;
try {
fis = new FileInputStream(dfos.getFile());
fis.read(fileData);
} catch (IOException e) {
fileData = null;
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
// ignore
}
}
}
return fileData;
}
/**
* Returns the contents of the file as a String, using the specified
* encoding. This method uses {@link #get()} to retrieve the contents of the
* file.
*
* @param charset The charset to use.
* @return The contents of the file, as a string.
* @throws UnsupportedEncodingException if the requested character encoding
* is not available.
*/
public String getString(final String charset) throws UnsupportedEncodingException {
return new String(get(), charset);
}
/**
* 取得字段或文件的内容。
* <p>
* 对于form field,将使用传入的<code>charset</code>所指定的字符集编码。
* </p>
* <p>
* 对于文件,使用固定的<code>ISO-8859-1</code>字符集编码。如果想以其它编码来读取文件文本,可使用{@link
* getString(charset)} 方法。
* </p>
*
* @return 字段值或文件文本
*/
public String getString() {
byte[] rawdata = get();
String charset = null;
if (isFormField()) {
charset = getCharset();
}
if (charset == null) {
charset = DEFAULT_CHARSET;
}
try {
return new String(rawdata, charset);
} catch (UnsupportedEncodingException e) {
try {
return new String(rawdata, DEFAULT_CHARSET);
} catch (UnsupportedEncodingException ee) {
return new String(rawdata);
}
}
}
/**
* A convenience method to write an uploaded item to disk. The client code
* is not concerned with whether or not the item is stored in memory, or on
* disk in a temporary location. They just want to write the uploaded item
* to a file.
* <p/>
* This implementation first attempts to rename the uploaded item to the
* specified destination file, if the item was originally written to disk.
* Otherwise, the data will be copied to the specified file.
* <p/>
* This method is only guaranteed to work <em>once</em>, the first time it
* is invoked for a particular item. This is because, in the event that the
* method renames a temporary file, that file will no longer be available to
* copy or rename again at a later time.
*
* @param file The <code>File</code> into which the uploaded item should be
* stored.
* @throws Exception if an error occurs.
*/
public void write(File file) throws Exception {
// 自动创建目录
if (file != null) {
file.getParentFile().mkdirs();
}
if (isInMemory()) {
FileOutputStream fout = null;
try {
fout = new FileOutputStream(file);
fout.write(get());
} finally {
if (fout != null) {
fout.close();
}
}
} else {
File outputFile = getStoreLocation();
if (outputFile != null) {
// Save the length of the file
size = outputFile.length();
/*
* The uploaded file is being stored on disk in a temporary
* location so move it to the desired file.
*/
if (!outputFile.renameTo(file)) {
BufferedInputStream in = null;
BufferedOutputStream out = null;
try {
in = new BufferedInputStream(new FileInputStream(outputFile));
out = new BufferedOutputStream(new FileOutputStream(file));
IOUtils.copy(in, out);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
// ignore
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
// ignore
}
}
}
}
} else {
/*
* For whatever reason we cannot write the file to disk.
*/
throw new FileUploadException("Cannot write uploaded file to disk!");
}
}
}
/**
* Deletes the underlying storage for a file item, including deleting any
* associated temporary disk file. Although this storage will be deleted
* automatically when the <code>FileItem</code> instance is garbage
* collected, this method can be used to ensure that this is done at an
* earlier time, thus preserving system resources.
*/
public void delete() {
cachedContent = null;
File outputFile = getStoreLocation();
if (outputFile != null && outputFile.exists()) {
outputFile.delete();
}
}
/**
* Returns the name of the field in the multipart form corresponding to this
* file item.
*
* @return The name of the form field.
* @see #setFieldName(java.lang.String)
*/
public String getFieldName() {
return fieldName;
}
/**
* Sets the field name used to reference this file item.
*
* @param fieldName The name of the form field.
* @see #getFieldName()
*/
public void setFieldName(String fieldName) {
this.fieldName = fieldName;
}
/**
* Determines whether or not a <code>FileItem</code> instance represents a
* simple form field.
*
* @return <code>true</code> if the instance represents a simple form field;
* <code>false</code> if it represents an uploaded file.
* @see #setFormField(boolean)
*/
public boolean isFormField() {
return isFormField;
}
/**
* Specifies whether or not a <code>FileItem</code> instance represents a
* simple form field.
*
* @param state <code>true</code> if the instance represents a simple form
* field; <code>false</code> if it represents an uploaded file.
* @see #isFormField()
*/
public void setFormField(boolean state) {
isFormField = state;
}
/**
* Returns an {@link java.io.OutputStream OutputStream} that can be used for
* storing the contents of the file.
*
* @return An {@link java.io.OutputStream OutputStream} that can be used for
* storing the contensts of the file.
* @throws IOException if an error occurs.
*/
public OutputStream getOutputStream() throws IOException {
if (dfos == null) {
int sizeThreshold;
if (keepFormFieldInMemory && isFormField()) {
sizeThreshold = Integer.MAX_VALUE;
} else {
sizeThreshold = this.sizeThreshold;
}
dfos = new DeferredFileOutputStream(sizeThreshold, "upload_" + UID, ".tmp", repository);
}
return dfos;
}
// --------------------------------------------------------- Public methods
/**
* Returns the {@link java.io.File} object for the <code>FileItem</code>'s
* data's temporary location on the disk. Note that for
* <code>FileItem</code>s that have their data stored in memory, this method
* will return <code>null</code>. When handling large files, you can use
* {@link java.io.File#renameTo(java.io.File)} to move the file to new
* location without copying the data, if the source and destination
* locations reside within the same logical volume.
*
* @return The data file, or <code>null</code> if the data is stored in
* memory.
*/
public File getStoreLocation() {
return dfos == null ? null : dfos.getFile();
}
// ------------------------------------------------------ Protected methods
// -------------------------------------------------------- Private methods
/**
* Returns a string representation of this object.
*
* @return a string representation of this object.
*/
@Override
public String toString() {
return StringUtil.defaultIfEmpty(getName(), getString());
}
// -------------------------------------------------- Serialization methods
/**
* Writes the state of this object during serialization.
*
* @param out The stream to which the state should be written.
* @throws IOException if an error occurs.
*/
private void writeObject(ObjectOutputStream out) throws IOException {
// Read the data
if (dfos.isInMemory()) {
cachedContent = get();
} else {
cachedContent = null;
dfosFile = dfos.getFile();
}
// write out values
out.defaultWriteObject();
}
/**
* Reads the state of this object during deserialization.
*
* @param in The stream from which the state should be read.
* @throws IOException if an error occurs.
* @throws ClassNotFoundException if class cannot be found.
*/
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// read values
in.defaultReadObject();
OutputStream output = getOutputStream();
if (cachedContent != null) {
output.write(cachedContent);
} else {
FileInputStream input = new FileInputStream(dfosFile);
IOUtils.copy(input, output);
dfosFile.delete();
dfosFile = null;
}
output.close();
cachedContent = null;
}
/**
* Returns the file item headers.
*
* @return The file items headers.
*/
public FileItemHeaders getHeaders() {
return headers;
}
/**
* Sets the file item headers.
*
* @param pHeaders The file items headers.
*/
public void setHeaders(FileItemHeaders pHeaders) {
headers = pHeaders;
}
}