/*
* Copyright (c) xlightweb.org, 2008 - 2009. All rights reserved.
*
* This library 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.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Please refer to the LGPL license at: http://www.gnu.org/copyleft/lesser.txt
* The latest copy of this software may be found on http://www.xlightweb.org/
*/
package org.xlightweb;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.MalformedURLException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.Map.Entry;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.xlightweb.AbstractHttpConnection.IMultimodeExecutor;
import org.xlightweb.MultipartFormDataPart.IPartReadListener;
import org.xsocket.DataConverter;
import org.xsocket.Execution;
/**
* A multipart/form-data request, which supports file uploads. Example:
*
* <pre>
* MultipartFormDataRequest req = new MultipartFormDataRequest(url);
* req.addPart("file", file);
* req.addPart("description", "text/plain", "A unsigned ...");
*
* IHttpResponse resp = httpClient.call(req);
* // ...
* </pre>
*
* see <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
*
* @author grro@xlightweb.org
*/
public class MultipartFormDataRequest extends HttpRequest {
private static final Logger LOG = Logger.getLogger(MultipartFormDataRequest.class.getName());
private final String boundary;
private final Map<String, MultipartFormDataPart> parts = new HashMap<String, MultipartFormDataPart>();
private boolean isModifyable;
private AtomicBoolean isComplete = new AtomicBoolean(false);
private AtomicReference<IPartHandler> handlerRef = new AtomicReference<IPartHandler>(null);
private AtomicReference<PartHandlerInfo> handlerInfo = new AtomicReference<PartHandlerInfo>(null);
// callback execution support
private final IMultimodeExecutor executor;
/**
* constructor
*
* @param url the url string
* @throws MalformedURLException if the url is malformed
*/
public MultipartFormDataRequest(String url) throws MalformedURLException, IOException {
this(new HttpRequestHeader("POST", url), UUID.randomUUID().toString(), new NonBlockingBodyDataSource(BodyType.IN_MEMORY, AbstractHeader.DEFAULT_ENCODING));
isModifyable = true;
super.setContentType("multipart/form-data; boundary=" + boundary);
setContentLength(0);
getNonBlockingBody().setComplete(true);
}
/**
* constructor
*
* @param request the request to wrap
* @throws IOException if an exception occurs
*/
MultipartFormDataRequest(IHttpRequest request) throws IOException {
this(request.getRequestHeader(), parseBoundary(request.getRequestHeader()), request.getNonBlockingBody());
isModifyable = false;
if (!isMultipartFormDataRequest(request)) {
throw new IOException("request is not a multipart/form-data request ");
}
NonBlockingBodyDataSource bodyDataSource = request.getNonBlockingBody();
MultpartRequestBodyDataHandler dh = new MultpartRequestBodyDataHandler();
bodyDataSource.setDataHandler(dh);
dh.onData(bodyDataSource);
}
private MultipartFormDataRequest(IHttpRequestHeader header, String boundary, NonBlockingBodyDataSource body) throws IOException {
super(header);
AbstractHttpConnection con = body.getConnection();
if (con != null) {
this.executor = body.getConnection().getExecutor();
} else {
this.executor = new DefaultMultimodeExecutor();
}
setBodyDataSource(body);
this.boundary = boundary;
}
/**
* return true, if all parts is received completly
*
* @return true, if all parts is received completly
*/
boolean isComplete() {
return isComplete.get();
}
private static String parseBoundary(IHttpRequestHeader requestHeader) throws IOException {
String contentType= requestHeader.getContentType();
String attr = contentType.substring("multipart/form-data".length(), contentType.length()).trim();
String[] atts = attr.split(";");
for (String att : atts) {
att = att.trim();
if (att.toLowerCase().startsWith("boundary=")) {
return att.substring("boundary=".length(), att.length()).trim();
}
}
throw new IOException("request " + requestHeader + " does not declares the boundary");
}
void setPartHandler(IPartHandler handler) throws IOException {
handlerRef.set(handler);
handlerInfo.set(HttpUtils.getPartHandlerInfo(handler));
List<MultipartFormDataPart> partsCopy = new ArrayList<MultipartFormDataPart>();
synchronized (parts) {
partsCopy.addAll(parts.values());
}
for (MultipartFormDataPart part : partsCopy) {
onPart(part);
}
}
void addPart(MultipartFormDataPart part) throws IOException {
synchronized (parts) {
parts.put(part.getDispositionParam("name"), part);
}
onPart(part);
}
private void onPart(final MultipartFormDataPart part) throws IOException {
IPartHandler handler = handlerRef.get();
if (handler != null) {
if (handlerInfo.get().isHandlerInvokeOnMessageReceived()) {
IBodyCompleteListener cl = new IBodyCompleteListener() {
@Execution(Execution.NONTHREADED)
public void onComplete() throws IOException {
if (handlerInfo.get().isHandlerMultithreaded()) {
executor.processMultithreaded(new PartHandlerCaller(part));
} else {
executor.processNonthreaded(new PartHandlerCaller(part));
}
}
};
part.getNonBlockingBody().addCompleteListener(cl);
} else {
if (handlerInfo.get().isHandlerMultithreaded()) {
executor.processMultithreaded(new PartHandlerCaller(part));
} else {
executor.processNonthreaded(new PartHandlerCaller(part));
}
}
}
}
private final class PartHandlerCaller implements Runnable {
private final MultipartFormDataPart part;
public PartHandlerCaller(MultipartFormDataPart part) {
this.part = part;
}
public void run() {
IPartHandler handler = handlerRef.get();
if (handler != null) {
try {
handler.onPart(part);
} catch (IOException ioe) {
part.destroy();
throw new RuntimeException(ioe);
}
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void setContentType(String type) {
LOG.warning("current content type " + getContentType() + " will be overriden by " + type);
super.setContentType(type);
}
IPart getPart(String name) throws IOException {
throwExceptionIfnotComplete();
return parts.get(name);
}
Map<String, IPart> getPartMap() throws IOException {
throwExceptionIfnotComplete();
HashMap<String, IPart> result = new HashMap<String, IPart>();
for (Entry<String, MultipartFormDataPart> entry : parts.entrySet()) {
result.put(entry.getKey(), entry.getValue());
}
return result;
}
Set<String> getPartnameSet() throws IOException {
throwExceptionIfnotComplete();
return Collections.unmodifiableSet(parts.keySet());
}
private void throwExceptionIfnotComplete() throws IOException {
if (!isComplete.get()) {
LOG.warning("request " + getRequestHeader() + " is not received completly (hint: set @InvokeOn(InvokeOn.MESSAGE_RECEIVED) or uses a IBodyCompleteListener)");
throw new IOException("request is not received completly (hint: set @InvokeOn(InvokeOn.MESSAGE_RECEIVED) or uses a IBodyCompleteListener)");
}
}
static boolean isMultipartFormDataRequest(IHttpRequest request) {
if (!request.hasBody()) {
return false;
}
String contentType = request.getContentType();
if ((contentType != null) && (contentType.startsWith("multipart/form-data"))) {
return true;
}
return false;
}
/**
* adds a part
*
* @param name the part name
* @param content the content
* @throws IOException if an exception occurs
*/
public void addPart(String name, String content) throws IOException {
addPart(name, content, "text/plain", getNonBlockingBody().getEncoding());
}
/**
* adds a part
*
* @param name the part name
* @param contentType the content type
* @param content the content
* @throws IOException if an exception occurs
*/
public void addPart(String name, String contentType, String content) throws IOException {
addPart(name, content, contentType, HttpUtils.parseEncoding(contentType, getNonBlockingBody().getEncoding()));
}
private void addPart(String name, String content, String contentType, String encoding) throws IOException {
// create part header
StringBuilder sb = new StringBuilder();
sb.append("--" + boundary + "\r\n");
sb.append("Content-Disposition: form-data; name=\"" + name + "\"\r\n");
sb.append("Content-Type: " + contentType + "; " + encoding + "\r\n");
sb.append("\r\n");
byte[] header = sb.toString().getBytes("US-ASCII");
// create content
ByteBuffer buffer = DataConverter.toByteBuffer(content, encoding);
// add part
addPart(DataConverter.toByteBuffer(header), buffer);
}
/**
* adds a part
*
* @param name the part name
* @param file the file
* @throws IOException if an exception occurs
*/
public void addPart(String name, File file) throws IOException {
// create part header
StringBuilder sb = new StringBuilder();
sb.append("--" + boundary + "\r\n");
sb.append("Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + file.getName() + "\"\r\n");
sb.append("Content-Type: " + getContentTypeByFileExtension(file) + "\r\n");
sb.append("\r\n");
byte[] header = sb.toString().getBytes("US-ASCII");
// create content
RandomAccessFile raf = new RandomAccessFile(file, "r");
FileChannel fc = raf.getChannel();
ByteBuffer buffer = ByteBuffer.allocate((int) fc.size());
fc.read(buffer);
fc.close();
raf.close();
buffer.flip();
// add part
addPart(DataConverter.toByteBuffer(header), buffer);
}
private void addPart(ByteBuffer... part) throws IOException {
if (!isModifyable) {
throw new IOException("modifying a recevied message is not supported");
}
int size = getNonBlockingBody().available();
if (size < 0) {
size = 0;
}
// remove final boundary if exists
if (size > 0) {
ByteBuffer[] oldContent = getNonBlockingBody().readByteBufferByLength(size);
size = 0;
for (int i = 0; i < oldContent.length; i++) {
ByteBuffer buf = null;
if (i == (oldContent.length - 1)) {
if (("\r\n--" + boundary + "--\r\n").equals(DataConverter.toString(oldContent[i].duplicate(), "US-ASCII"))) {
buf = DataConverter.toByteBuffer("\r\n", "US-ASCII");
} else {
buf = oldContent[i];
}
} else {
buf = oldContent[i];
}
if (buf != null) {
size += buf.remaining();
getNonBlockingBody().append(true, buf);
}
}
}
for (ByteBuffer buffer : part) {
size += buffer.remaining();
}
getNonBlockingBody().append(true, part, null);
// append final boundary
StringBuilder sb = new StringBuilder("\r\n--" + boundary + "--\r\n");
byte[] bound = sb.toString().getBytes("US-ASCII");
getNonBlockingBody().append(true, ByteBuffer.wrap(bound));
size = size + bound.length;
// set new content length
setContentLength(size);
getNonBlockingBody().setComplete(true);
}
@Execution(Execution.NONTHREADED)
private final class MultpartRequestBodyDataHandler implements IBodyDataHandler, IPartReadListener {
private static final int STATE_PRE_BOUNDARY = 0;
private static final int STATE_READ_HEADER = 5;
private static final int STATE_COMPLETE = 10;
private int state = 0;
public boolean onData(NonBlockingBodyDataSource bodyDataSource) throws BufferUnderflowException {
try {
if (isComplete.get()) {
return true;
}
switch (state) {
case STATE_PRE_BOUNDARY:
String leadingString = getNonBlockingBody().readStringByDelimiter("--" + boundary + "\r");
if ((leadingString.trim().length() > 0) && (LOG.isLoggable(Level.FINE))) {
LOG.fine("first part has leading chars " + leadingString + " chars will be ignored");
}
state = STATE_READ_HEADER;
onData(bodyDataSource);
break;
case STATE_READ_HEADER:
String header = null;
try {
header = getNonBlockingBody().readStringByDelimiter("\r\n\r\n");
} catch (BufferUnderflowException bue) {
header = getNonBlockingBody().readStringByDelimiter("\r\r");
}
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("header read: " + header);
}
String[] headerlines = header.split("\r");
for (int i = 0; i < headerlines.length; i++) {
if (headerlines[i].startsWith("\n")) {
headerlines[i] = headerlines[i].substring(1, headerlines[i].length());
}
headerlines[i] = headerlines[i].trim();
}
new MultipartFormDataPart(this, boundary, headerlines, getNonBlockingBody());
break;
default:
break;
}
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by parsing multipart request " + ioe.toString());
}
}
return true;
}
public void onPartRead(MultipartFormDataPart part) {
try {
state = STATE_PRE_BOUNDARY;
addPart(part);
getNonBlockingBody().setDataHandler(this);
onData(getNonBlockingBody());
} catch (IOException ioe) {
onException(ioe);
}
}
public void onLastPartRead(MultipartFormDataPart part) {
try {
state = STATE_COMPLETE;
isComplete.set(true);
addPart(part);
} catch (IOException ioe) {
onException(ioe);
}
}
public void onException(IOException ioe) {
try {
getNonBlockingBody().destroy();
} catch (IOException e) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occred by destroying non blcoking body " + e.toString());
}
}
}
}
private static class DefaultMultimodeExecutor implements IMultimodeExecutor {
private static final Executor defaultExecutor = Executors.newCachedThreadPool();
public void processMultithreaded(Runnable task) {
defaultExecutor.execute(task);
}
public void processNonthreaded(Runnable task) {
task.run();
}
}
}