org.wings.session.MultipartRequest Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2000,2005 wingS development team.
*
* This file is part of wingS (http://wingsframework.org).
*
* wingS is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2.1
* of the License, or (at your option) any later version.
*
* Please see COPYING for the complete licence.
*/
package org.wings.session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wings.UploadFilterManager;
import org.wings.util.LocaleCharSet;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.net.URLEncoder;
import java.util.*;
/**
* A utility class to handle multipart/form-data requests,
* the kind of requests that support file uploads. This class can
* receive arbitrarily large files (up to an artificial limit you can set),
* and fairly efficiently too. And it knows and works around several browser
* bugs that don't know how to upload files correctly.
*
* A client can upload files using an HTML form with the following structure.
* Note that not all browsers support file uploads.
*
* <FORM ACTION="/servlet/Handler" METHOD=POST
* ENCTYPE="multipart/form-data">
* What is your name? <INPUT TYPE=TEXT NAME=submitter> <BR>
* Which file to upload? <INPUT TYPE=FILE NAME=file> <BR>
* <INPUT TYPE=SUBMIT>
* </FORM>
*
*
* The full file upload specification is contained in experimental RFC 1867,
* available at
* http://ds.internic.net/rfc/rfc1867.txt.
*
* @author Holger Engels
*/
public final class MultipartRequest extends HttpServletRequestWrapper {
private final transient static Logger log = LoggerFactory.getLogger(MultipartRequest.class);
private static final int DEFAULT_MAX_POST_SIZE = 1024 * 1024; // 1 Meg
private int maxSize;
private boolean urlencodedRequest;
private Map parameters; // name - value
private Map files; // name - UploadedFile
private Map parameterMap; // name - values
/**
* @param request the servlet request
* @throws IOException if the uploaded content is larger than 1 Megabyte
* or there's a problem reading or parsing the request
*/
public MultipartRequest(HttpServletRequest request) throws IOException {
this(request, DEFAULT_MAX_POST_SIZE);
}
/**
* @param request the servlet request
* @param maxPostSize the maximum size of the POST content
* @throws IOException if the uploaded content is larger than
* maxPostSize or there's a problem reading or parsing the request
*/
public MultipartRequest(HttpServletRequest request,
int maxPostSize) throws IOException {
super(request);
if (request == null)
throw new IllegalArgumentException("request cannot be null");
if (maxPostSize <= 0)
throw new IllegalArgumentException("maxPostSize must be positive");
maxSize = maxPostSize;
processRequest(request);
}
/**
* Returns the names of all the parameters as an Enumeration of
* Strings. It returns an empty Enumeration if there are no parameters.
*
* @return the names of all the parameters as an Enumeration of Strings
*/
@Override
public Enumeration getParameterNames() {
if (urlencodedRequest) return super.getParameterNames();
final Iterator iter = parameters.keySet().iterator();
return new MyEnumeration(iter);
}
/**
* Returns the names of all the uploaded files as an Enumeration of
* Strings. It returns an empty Enumeration if there are no uploaded
* files. Each file name is the name specified by the form, not by
* the user.
*
* @return the names of all the uploaded files as an Enumeration of Strings
*/
public Iterator getFileNames() {
if (urlencodedRequest) return Collections.emptySet().iterator();
return files.keySet().iterator();
}
@Override
public String getParameter(String name) {
if (urlencodedRequest)
return super.getParameter(name);
List v = parameters.get(name);
if ( v == null || v.isEmpty() )
return null;
return v.get( 0 );
}
@Override
public String[] getParameterValues(String name) {
if (urlencodedRequest) return super.getParameterValues(name);
List v = parameters.get(name);
if (v == null) return null;
String result[] = new String[v.size()];
return (String[]) v.toArray(result);
}
@Override
public Map getParameterMap() {
if (urlencodedRequest) return super.getParameterMap();
if (parameterMap == null) {
parameterMap = new HashMap<>();
for (Map.Entry entry : parameters.entrySet()) {
List list = entry.getValue();
String[] values = (String[]) list.toArray(new String[list.size()]);
parameterMap.put(entry.getKey(), values);
}
}
return parameterMap;
}
/**
* Returns the filename of the specified file, or null if the
* file was not included in the upload. The filename is the name
* specified by the user. It is not the name under which the file is
* actually saved.
*
* @param name the file name
* @return the filesystem name of the file
*/
public String getFileName(String name) {
try {
return files.get(name).getFileName();
} catch (Exception e) {
return null;
}
}
/**
* Returns the fileid of the specified file, or null if the
* file was not included in the upload. The fileid is the name
* under which the file is saved in the filesystem.
*
* @param name the file name
* @return the filesystem name of the file
*/
public String getFileId(String name) {
try {
return files.get(name).getId();
} catch (Exception e) {
return null;
}
}
/**
* Returns the content type of the specified file (as supplied by the
* client browser), or null if the file was not included in the upload.
*
* @param name the file name
* @return the content type of the file
*/
public String getContentType(String name) {
try {
return files.get(name).getContentType();
} catch (Exception e) {
return null;
}
}
/**
* Returns a File object for the specified file saved on the server's
* filesystem, or null if the file was not included in the upload.
*
* @param name the file name
* @return a File object for the named file
*/
public File getFile(String name) {
try {
return files.get(name).getFile();
} catch (Exception e) {
return null;
}
}
/**
* Indicates if this class was successfully able to parse request as multipart request.
*/
public final boolean isMultipart() {
return !urlencodedRequest;
}
/**
* Store exception as request parameter.
*/
protected void setException(String param, Exception ex) {
if (!urlencodedRequest) {
// I'm not 100% sure if it's ok to comment out the following line!
// However, if we delete all parameters that have been set before
// the occurence of the exception, the only component that will get
// triggered during event processing is the filechooser. But since
// a filechooser provides no ability to register any listeners, the
// developer has no chance to get informed about the exception in
// the application code. There is only one reason I can imaging why
// someone set this line: if other components have been placed below
// the filechooser on the GUI, their parts won't get processed by
// the MultipartRequest, the according parameters won't be set and
// therefore no event processing of such components is done. If we
// process components above the exception raising filechooser but
// not components below it, we might end up in an inconsitent state.
// Anyway, I think it's the better solution to leave it out here!!!
//
// -- stephan
//
// parameters.clear();
files.clear();
}
putParameter(param, "exception");
putParameter(param, ex.getMessage());
}
/**
* Parses passed request and stores contained parameters.
*
* @throws IOException On unrecoverable parsing bugs due to old Tomcat version.
*/
protected void processRequest(HttpServletRequest req)
throws IOException {
String type = req.getContentType();
if (type == null || !type.toLowerCase().startsWith("multipart/form-data")) {
urlencodedRequest = true;
return;
}
urlencodedRequest = false;
parameters = new HashMap<>();
files = new HashMap<>();
for (Map.Entry stringEntry : req.getParameterMap().entrySet()) {
Map.Entry entry = (Map.Entry) stringEntry;
parameters.put((String) entry.getKey(),
new ArrayList(Arrays.asList((String[]) entry.getValue())));
}
String boundaryToken = extractBoundaryToken(type);
if (boundaryToken == null) {
/*
* this could happen due to a bug in Tomcat 3.2.2 in combination
* with Opera.
* Opera sends the boundary on a separate line, which is perfectly
* correct regarding the way header may be constructed
* (multiline headers). Alas, Tomcat fails to read the header in
* the content type line and thus we cannot read it.. haven't
* checked later versions of Tomcat, but upgrading is
* definitly needed, since we cannot do anything about it here.
* (JServ works fine, BTW.) (Henner)
*/
throw new IOException("Separation boundary was not specified (BUG in Tomcat 3.* with Opera?)");
}
MultipartInputStream mimeStream = null;
String currentParam = null;
File uploadFile = null;
OutputStream fileStream = null;
try {
mimeStream = new MultipartInputStream(req.getInputStream(), req.getContentLength(), maxSize);
int last = -1;
int currentTransformByte = 0;
int currentPos = 0;
int currentByte = 0;
HashMap headers = null;
StringBuilder content = new StringBuilder();
while (currentByte != -1) {
// Read MIME part header line
boolean done = false;
ByteArrayOutputStream headerByteArray = new ByteArrayOutputStream();
while ((currentByte = mimeStream.read()) != -1 && !done) {
headerByteArray.write(currentByte);
done = (last == '\n' && currentByte == '\r');
last = currentByte;
}
if (currentByte == -1)
break;
headers = parseHeader(headerByteArray.toString(req.getCharacterEncoding()));
headerByteArray.reset();
currentParam = (String) headers.get("name");
if (headers.size() == 1) { // .. it's not a file
byte[] bytes = new byte[req.getContentLength()];
currentPos = 0;
while ((currentByte = mimeStream.read()) != -1) {
bytes[currentPos] = (byte) currentByte;
currentPos++;
if (currentPos >= boundaryToken.length()) {
int i;
for (i = 0; i < boundaryToken.length(); i++) {
if (boundaryToken.charAt(boundaryToken.length() - i - 1) != bytes[currentPos - i - 1]) {
i = 0;
break;
}
}
if (i == boundaryToken.length()) { // end of part ..
ByteArrayInputStream bais = new ByteArrayInputStream(bytes, 0, currentPos - boundaryToken.length() - 4);
InputStreamReader ir;
if (req.getCharacterEncoding() != null)
// It's common behaviour of browsers to encode their form input in the character
// encoding of the page, though they don't declare the used characterset explicetly
// for backward compatibility.
ir = new InputStreamReader(bais, req.getCharacterEncoding());
else
ir = new InputStreamReader(bais);
content.setLength(0);
while ((currentTransformByte = ir.read()) != -1) {
content.append((char) currentTransformByte);
}
putParameter(currentParam, content.toString());
break;
}
}
}
} else { // .. it's a file
String filename = (String) headers.get("filename");
if (filename != null && filename.length() != 0) {
// The filename may contain a full path. Cut to just the filename.
int slash = Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\'));
if (slash > -1) {
filename = filename.substring(slash + 1);
}
String name = (String) headers.get("name");
String contentType = (String) headers.get("content-type");
try {
uploadFile = File.createTempFile("wings_uploaded",
"tmp");
} catch (IOException e) {
log.error("couldn't create temp file in '"
+ System.getProperty("java.io.tmpdir")
+ "' (CATALINA_TMPDIR set correctly?)",
e);
throw e;
}
UploadedFile upload = new UploadedFile(filename,
contentType,
uploadFile);
fileStream = new FileOutputStream(uploadFile);
fileStream = UploadFilterManager.createFilterInstance(name, fileStream);
AccessibleByteArrayOutputStream byteArray = new AccessibleByteArrayOutputStream();
int blength = boundaryToken.length();
while ((currentByte = mimeStream.read()) != -1) {
byteArray.write(currentByte);
int i;
for (i = 0; i < blength; i++) {
if (boundaryToken.charAt(blength - i - 1) != byteArray.charAt(-i - 1)) {
i = 0;
if (byteArray.size() > 512 + blength + 2)
byteArray.writeTo(fileStream, 512);
break;
}
}
if (i == blength) // end of part ..
break;
}
byte[] bytes = byteArray.toByteArray();
fileStream.write(bytes, 0, bytes.length - blength - 4);
fileStream.close();
files.put(name, upload);
putParameter(name, upload.toString());
} else { // workaround for some netscape bug
int blength = boundaryToken.length();
while ((currentByte = mimeStream.read()) != -1) {
content.append((char) currentByte);
if (content.length() >= blength) {
int i;
for (i = 0; i < blength; i++) {
if (boundaryToken.charAt(blength - i - 1) != content.charAt(content.length() - i - 1)) {
i = 0;
break;
}
}
if (i == blength)
break;
}
}
}
}
currentByte = mimeStream.read();
if (currentByte == '\r' && mimeStream.read() != '\n')
log.error("No line return char?");
if (currentByte == '-' && mimeStream.read() != '-')
log.error("?? No clue");
}
} catch (IOException ex) {
// cleanup and store the exception for notification of SFileChooser
log.warn("upload", ex);
if (uploadFile != null) uploadFile.delete();
setException(currentParam, ex);
} finally {
try { fileStream.close(); } catch (Exception ign) {}
try { mimeStream.close(); } catch (Exception ign) {}
}
}
private static class AccessibleByteArrayOutputStream extends ByteArrayOutputStream {
public byte charAt(int index) {
if (count + index < 0) {
log.warn("count: " + count + ", index: " + index + ", buffer: " + new String(buf));
return -1;
}
if (index < 0)
return buf[count + index];
if (index < count)
return buf[index];
return -1;
}
public byte[] getBuffer() {
return buf;
}
public void writeTo(OutputStream out, int num)
throws IOException {
out.write(buf, 0, num);
System.arraycopy(buf, num, buf, 0, count - num);
count = count - num;
}
}
private static HashMap parseHeader(String header) {
StringTokenizer stLines = new StringTokenizer(header, "\r\n", false);
String[] headerLines = new String[stLines.countTokens()];
// Get all the header lines
int lastHeader = -1;
while (stLines.hasMoreTokens()) {
String hLine = stLines.nextToken();
if (hLine.length() == 0) continue;
/* if the first character is a space, then
* this line is a header continuation.
* (opera sends multiline headers..)
*/
if (lastHeader >= 0 && Character.isWhitespace(hLine.charAt(0)))
headerLines[lastHeader] += hLine;
else
headerLines[++lastHeader] = hLine;
}
HashMap nameValuePairs = new HashMap();
for (int i = 0; i <= lastHeader; ++i) {
String currentHeader = headerLines[i];
if (currentHeader.startsWith("Content-Type")) {
String contentType = currentHeader
.substring(currentHeader.indexOf(':') + 1);
int semiColonPos = contentType.indexOf(';');
if (semiColonPos != -1)
contentType = contentType.substring(0, semiColonPos);
nameValuePairs.put("content-type", contentType.trim());
continue;
}
if (!currentHeader.startsWith("Content-Disposition"))
continue;
StringTokenizer stTokens = new StringTokenizer(currentHeader, ";", false);
// Get all the tokens from each line
if (stTokens.countTokens() > 1) {
stTokens.nextToken(); // Skip fist Token Content-Disposition: form-data
StringTokenizer stnameValue = new StringTokenizer(stTokens.nextToken(), "=", false);
nameValuePairs.put(stnameValue.nextToken().trim(), trim(stnameValue.nextToken(), "\""));
// This is a file
if (stTokens.hasMoreTokens()) {
stnameValue = new StringTokenizer(stTokens.nextToken(), "=", false);
String formType = stnameValue.nextToken().trim(); // String Object default function
String filePath = trim(stnameValue.nextToken(), "\""); // Our own trim function.
// If is a DOS file get rid of drive letter and colon "e:"
if (filePath.contains(":"))
filePath = filePath.substring((filePath.indexOf(':') + 1));
// get rid of PATH
filePath = filePath.substring(filePath.lastIndexOf(File.separator) + 1);
nameValuePairs.put(formType, filePath);
}
}
}
return nameValuePairs;
}
/**
* This method gets the substring enclosed in trimChar ; "string" returns string
*/
private static String trim(String source, String trimChar) {
//Blank space from both sides
source = source.trim();
// Make sure a substring is enclosed between specified characters
String target = "";
if (source.contains(trimChar) && (source.lastIndexOf(trimChar) >= (source.indexOf(trimChar) + 1)))
// Remove double character from both sides
target = source.substring(source.indexOf(trimChar) + 1, source.lastIndexOf(trimChar));
return target;
}
private static class MultipartInputStream extends InputStream {
ServletInputStream istream = null;
int len, pos, maxLength;
public MultipartInputStream(ServletInputStream istream, int len, int maxLength) {
this.istream = istream;
this.len = len;
this.pos = 0;
this.maxLength = maxLength;
}
/**
* @return bytes available in stream.
*/
@Override
public int available() throws IOException {
return len - pos - 1;
}
/**
* @return Next byte in Request.
* @throws IOException
*/
@Override
public int read() throws IOException {
if (pos >= maxLength)
throw new IOException("Size (" + len + ") exceeds maxlength " + maxLength);
if (pos >= len)
return -1;
pos++;
return istream.read();
}
@Override
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte b[], int off, int num) throws IOException {
if (off > 0)
istream.skip(off);
if (pos >= len)
return -1;
if (num > len - pos)
num = len - pos;
num = istream.read(b, 0, num);
pos += num;
if (pos >= maxLength)
throw new IOException("Size (" + len + ") exceeds maxlength " + maxLength);
return num;
}
@Override
public long skip(long num) throws IOException {
if (pos >= len)
return -1;
if (num > len - pos)
num = len - pos;
num = istream.skip(num);
pos += num;
if (pos >= maxLength)
throw new IOException("Size (" + len + ") exceeds maxlength " + maxLength);
return num;
}
@Override
public void close() throws IOException {
//Ignore closing of the input stream ..
}
}
/**
* Stores a parameter identified in this request.
*/
protected void putParameter(String name, String value) {
ArrayList v = (ArrayList) parameters.get(name);
// there is no Parameter yet; create one
if (v == null) {
v = new ArrayList(2);
parameters.put(name, v);
}
v.add(value);
}
/**
* Extracts and returns the boundary token from a line.
*/
private static String extractBoundaryToken(String line) {
int index = line.indexOf("boundary=");
if (index == -1) {
return null;
}
String boundary = line.substring(index + 9); // 9 for "boundary="
// The real boundary is always preceeded by an extra "--"
//boundary = "--" + boundary;
return boundary;
}
/**
* Extracts and returns the content type from a line, or null if the line was empty.
*
* @throws IOException if the line is malformatted.
*/
private static String extractContentType(String line) throws IOException {
// Convert the line to a lowercase string
String origline = line;
line = origline.toLowerCase();
// Get the content type, if any
String contentType = null;
if (line.startsWith("content-type")) {
int start = line.indexOf(' ');
if (start == -1) {
throw new IOException("Content type corrupt: " + origline);
}
contentType = line.substring(start + 1);
} else if (line.length() != 0) { // no content type, so should be empty
throw new IOException("Malformed line after disposition: " + origline);
}
return contentType;
}
private static long uniqueId = 0;
private static final synchronized String uniqueId() {
uniqueId++;
return System.currentTimeMillis() + "." + uniqueId;
}
private static class MyEnumeration implements Enumeration {
private final Iterator iter;
public MyEnumeration(Iterator iter) {
this.iter = iter;
}
@Override
public boolean hasMoreElements() {
return iter.hasNext();
}
@Override
public Object nextElement() {
return iter.next();
}
}
/**
* A class to hold information about an uploaded file.
*/
class UploadedFile {
private String fileName;
private String type;
private File uploadedFile;
UploadedFile(String fileName, String type, File f) {
this.uploadedFile = f;
this.fileName = fileName;
this.type = type;
}
/**
* @return Path of uploaded file
*/
public String getDir() {
if (uploadedFile != null)
return uploadedFile.getParentFile().getPath();
else
return null;
}
/**
* @return Filename passed by browser
*/
public String getFileName() {
return fileName;
}
/**
* @return MIME type passed by browser
*/
public String getContentType() {
return type;
}
/**
* @return Uploaded file
*/
public File getFile() {
return uploadedFile;
}
/**
* @return Uploaded file name
*/
public String getId() {
if (uploadedFile != null)
return uploadedFile.getName();
else
return null;
}
/**
* create a URL-encoded form of this uploaded file, that contains
* all parameters important for this file. The parameters returned
* are 'dir', 'name', 'type' and 'id'
*
* - 'dir' contains the directory in the filesystem, the file
* has been stored into.
* - 'name' contains the filename as provided by the user
* - 'type' contains the mime-type of this file.
* - 'id' contains the internal name of the file in the
* filesystem.
*
*/
public String toString() {
String encoding = getRequest().getCharacterEncoding() != null ? getRequest().getCharacterEncoding() : LocaleCharSet.DEFAULT_ENCODING;
try {
StringBuilder buffer = new StringBuilder();
buffer.append("dir=");
buffer.append(URLEncoder.encode(getDir(), encoding));
if (fileName != null) {
buffer.append("&name=");
buffer.append(URLEncoder.encode(fileName, encoding));
}
if (type != null) {
buffer.append("&type=");
buffer.append(URLEncoder.encode(type, encoding));
}
buffer.append("&id=");
buffer.append(URLEncoder.encode(getId(), encoding));
return buffer.toString();
} catch (UnsupportedEncodingException e) {
log.error(getClass().getName(), e);
return null;
}
}
}
}