/*
* 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.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import org.xsocket.DataConverter;
/**
* 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 String dashBoundary;
private final String closeDelimiter;
private final Map<String, MultipartFormDataPart> parts = new HashMap<String, MultipartFormDataPart>();
private boolean isModifyable;
private AtomicBoolean isComplete = new AtomicBoolean(false);
/**
* 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 InMemoryBodyDataSource(IHttpHeader.DEFAULT_ENCODING,HttpUtils.newMultimodeExecutor(), (ByteBuffer[]) null));
isModifyable = true;
super.setContentType("multipart/form-data; boundary=" + boundary);
setContentLength(0);
}
private MultipartFormDataRequest(IHttpRequestHeader header, String boundary, NonBlockingBodyDataSource body) throws IOException {
super(header);
setBodyDataSource(body);
this.boundary = boundary;
this.dashBoundary = "--" + boundary;
closeDelimiter = "\r\n" + dashBoundary + "--";
}
/**
* return true, if all parts is received completly
*
* @return true, if all parts is received completly
*/
boolean isComplete() {
return isComplete.get();
}
void addPart(MultipartFormDataPart part) throws IOException {
synchronized (parts) {
parts.put(part.getDispositionParam("name"), part);
}
}
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(dashBoundary + "\r\n");
sb.append("Content-Disposition: form-data; name=\"" + name + "\"\r\n");
sb.append("Content-Type: " + contentType + "; charset=" + 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(dashBoundary + "\r\n");
sb.append("Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + file.getName() + "\"\r\n");
String contentType = HttpUtils.getContentTypeByFileExtension(file);
if (contentType.startsWith("text")) {
contentType = contentType + "; charset=" + getCharacterEncoding();
}
sb.append("Content-Type: " + contentType + "\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 close 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 ((closeDelimiter + "\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(buf);
}
}
}
for (ByteBuffer buffer : part) {
size += buffer.remaining();
}
getNonBlockingBody().append(part);
// append close boundary
StringBuilder sb = new StringBuilder(closeDelimiter + "\r\n");
byte[] bound = sb.toString().getBytes("US-ASCII");
getNonBlockingBody().append(ByteBuffer.wrap(bound));
size = size + bound.length;
// set new content length
setContentLength(size);
}
}